Initial commit of mtgonline project

This commit is contained in:
2026-07-18 04:57:40 +00:00
commit 86c12376f8
1870 changed files with 547994 additions and 0 deletions
@@ -0,0 +1,40 @@
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)
set(HEADERS
libcockatrice/deck_list/tree/abstract_deck_list_card_node.h
libcockatrice/deck_list/tree/abstract_deck_list_node.h
libcockatrice/deck_list/tree/deck_list_card_node.h
libcockatrice/deck_list/tree/inner_deck_list_node.h
libcockatrice/deck_list/deck_list.h
libcockatrice/deck_list/deck_list_history_manager.h
libcockatrice/deck_list/deck_list_node_tree.h
libcockatrice/deck_list/deck_list_memento.h
libcockatrice/deck_list/sideboard_plan.h
)
if(Qt6_FOUND)
qt6_wrap_cpp(MOC_SOURCES ${HEADERS})
elseif(Qt5_FOUND)
qt5_wrap_cpp(MOC_SOURCES ${HEADERS})
endif()
add_library(
libcockatrice_deck_list STATIC
${MOC_SOURCES}
libcockatrice/deck_list/tree/abstract_deck_list_card_node.cpp
libcockatrice/deck_list/tree/abstract_deck_list_node.cpp
libcockatrice/deck_list/tree/deck_list_card_node.cpp
libcockatrice/deck_list/tree/inner_deck_list_node.cpp
libcockatrice/deck_list/deck_list.cpp
libcockatrice/deck_list/deck_list_history_manager.cpp
libcockatrice/deck_list/deck_list_node_tree.cpp
libcockatrice/deck_list/sideboard_plan.cpp
)
add_dependencies(libcockatrice_deck_list libcockatrice_protocol)
target_include_directories(libcockatrice_deck_list PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(libcockatrice_deck_list PUBLIC libcockatrice_protocol libcockatrice_utility ${QT_CORE_MODULE})
@@ -0,0 +1,516 @@
#include "deck_list.h"
#include "deck_list_memento.h"
#include "tree/abstract_deck_list_node.h"
#include "tree/deck_list_card_node.h"
#include "tree/inner_deck_list_node.h"
#include <QCryptographicHash>
#include <QDebug>
#include <QFile>
#include <QRegularExpression>
#include <QSet>
#include <QTextStream>
#include <algorithm>
#if QT_VERSION < 0x050600
// qHash on QRegularExpression was added in 5.6, FIX IT
uint qHash(const QRegularExpression &key, uint seed) noexcept
{
return qHash(key.pattern(), seed); // call qHash on pattern QString instead
}
#endif
static const QString CURRENT_SIDEBOARD_PLAN_KEY = "";
bool DeckList::Metadata::isEmpty() const
{
return name.isEmpty() && comments.isEmpty() && bannerCard.isEmpty() && tags.isEmpty();
}
DeckList::DeckList()
{
}
DeckList::DeckList(const QString &nativeString)
{
loadFromString_Native(nativeString);
}
DeckList::DeckList(const Metadata &metadata,
const DecklistNodeTree &tree,
const QMap<QString, SideboardPlan> &sideboardPlans)
: metadata(metadata), sideboardPlans(sideboardPlans), tree(tree)
{
}
QList<MoveCard_ToZone> DeckList::getCurrentSideboardPlan() const
{
if (!sideboardPlans.contains(CURRENT_SIDEBOARD_PLAN_KEY)) {
return {};
}
return sideboardPlans.value(CURRENT_SIDEBOARD_PLAN_KEY).getMoveList();
}
void DeckList::setCurrentSideboardPlan(const QList<MoveCard_ToZone> &plan)
{
sideboardPlans[CURRENT_SIDEBOARD_PLAN_KEY].setMoveList(plan);
}
bool DeckList::readElement(QXmlStreamReader *xml)
{
const QString childName = xml->name().toString();
if (xml->isStartElement()) {
if (childName == "lastLoadedTimestamp") {
metadata.lastLoadedTimestamp = xml->readElementText();
} else if (childName == "deckname") {
metadata.name = xml->readElementText();
} else if (childName == "format") {
metadata.gameFormat = xml->readElementText();
} else if (childName == "comments") {
metadata.comments = xml->readElementText();
} else if (childName == "bannerCard") {
QString providerId = xml->attributes().value("providerId").toString();
QString cardName = xml->readElementText();
metadata.bannerCard = {cardName, providerId};
} else if (childName == "tags") {
metadata.tags.clear(); // Clear existing tags
while (xml->readNextStartElement()) {
if (xml->name().toString() == "tag") {
metadata.tags.append(xml->readElementText());
}
}
} else if (childName == "zone") {
tree.readZoneElement(xml);
} else if (childName == "sideboard_plan") {
SideboardPlan newSideboardPlan;
if (newSideboardPlan.readElement(xml)) {
sideboardPlans.insert(newSideboardPlan.getName(), newSideboardPlan);
}
}
} else if (xml->isEndElement() && (childName == "cockatrice_deck")) {
return false;
}
return true;
}
static void writeMetadata(QXmlStreamWriter *xml, const DeckList::Metadata &metadata)
{
xml->writeTextElement("lastLoadedTimestamp", metadata.lastLoadedTimestamp);
xml->writeTextElement("deckname", metadata.name);
xml->writeTextElement("format", metadata.gameFormat);
xml->writeStartElement("bannerCard");
xml->writeAttribute("providerId", metadata.bannerCard.providerId);
xml->writeCharacters(metadata.bannerCard.name);
xml->writeEndElement();
xml->writeTextElement("comments", metadata.comments);
// Write tags
xml->writeStartElement("tags");
for (const QString &tag : metadata.tags) {
xml->writeTextElement("tag", tag);
}
xml->writeEndElement();
}
void DeckList::write(QXmlStreamWriter *xml) const
{
xml->writeStartElement("cockatrice_deck");
xml->writeAttribute("version", "1");
writeMetadata(xml, metadata);
// Write zones
tree.write(xml);
// Write sideboard plans
for (auto &sideboardPlan : sideboardPlans.values()) {
sideboardPlan.write(xml);
}
xml->writeEndElement(); // Close "cockatrice_deck"
}
bool DeckList::loadFromXml(QXmlStreamReader *xml)
{
if (xml->error()) {
qDebug() << "Error loading deck from xml: " << xml->errorString();
return false;
}
cleanList();
while (!xml->atEnd()) {
xml->readNext();
if (xml->isStartElement()) {
if (xml->name().toString() != "cockatrice_deck") {
return false;
}
while (!xml->atEnd()) {
xml->readNext();
if (!readElement(xml)) {
break;
}
}
}
}
refreshDeckHash();
if (xml->error()) {
qDebug() << "Error loading deck from xml: " << xml->errorString();
return false;
}
return true;
}
bool DeckList::loadFromString_Native(const QString &nativeString)
{
QXmlStreamReader xml(nativeString);
return loadFromXml(&xml);
}
QString DeckList::writeToString_Native() const
{
QString result;
QXmlStreamWriter xml(&result);
xml.writeStartDocument();
write(&xml);
xml.writeEndDocument();
return result;
}
bool DeckList::loadFromFile_Native(QIODevice *device)
{
QXmlStreamReader xml(device);
return loadFromXml(&xml);
}
bool DeckList::saveToFile_Native(QIODevice *device) const
{
QXmlStreamWriter xml(device);
xml.setAutoFormatting(true);
xml.writeStartDocument();
write(&xml);
xml.writeEndDocument();
return true;
}
/**
* Clears the decklist and loads in a new deck from text
*
* @param in The text to load
* @param preserveMetadata If true, don't clear the existing metadata
* @param cardNameNormalizer Function that takes the parsed card name string in the text and
* @return False if the input was empty, true otherwise.
*/
bool DeckList::loadFromStream_Plain(QTextStream &in,
bool preserveMetadata,
const std::function<QString(const QString &)> &cardNameNormalizer)
{
const QRegularExpression reCardLine(R"(^\s*[\w\[\(\{].*$)", QRegularExpression::UseUnicodePropertiesOption);
const QRegularExpression reEmpty("^\\s*$");
const QRegularExpression reComment(R"([\w\[\(\{].*$)", QRegularExpression::UseUnicodePropertiesOption);
const QRegularExpression reSBMark("^\\s*sb:\\s*(.+)", QRegularExpression::CaseInsensitiveOption);
const QRegularExpression reSBComment("^sideboard\\b.*$", QRegularExpression::CaseInsensitiveOption);
const QRegularExpression reDeckComment("^((main)?deck(list)?|mainboard)\\b",
QRegularExpression::CaseInsensitiveOption);
// Regex for advanced card parsing
const QRegularExpression reMultiplier(R"(^[xX\(\[]*(\d+)[xX\*\)\]]* ?(.+))");
// Regex for extracting set code and collector number with attached symbols
const QRegularExpression reHyphenFormat(R"(\((\w{3,})\)\s+(\w{3,})-(\d+[^\w\s]*))");
const QRegularExpression reRegularFormat(R"(\((\w{3,})\)\s+(\d+[^\w\s]*))");
cleanList(preserveMetadata);
auto inputs = in.readAll().trimmed().split('\n');
auto max_line = inputs.size();
// Start at the first empty line before the first card line
auto deckStart = inputs.indexOf(reCardLine);
if (deckStart == -1) {
if (inputs.indexOf(reComment) == -1) {
return false; // Input is empty
}
deckStart = max_line;
} else {
deckStart = inputs.lastIndexOf(reEmpty, deckStart);
if (deckStart == -1) {
deckStart = 0;
}
}
// find sideboard position, if marks are used this won't be needed
int sBStart = -1;
if (inputs.indexOf(reSBMark, deckStart) == -1) {
sBStart = inputs.indexOf(reSBComment, deckStart);
if (sBStart == -1) {
sBStart = inputs.indexOf(reEmpty, deckStart + 1);
if (sBStart == -1) {
sBStart = max_line;
}
auto nextCard = inputs.indexOf(reCardLine, sBStart + 1);
if (inputs.indexOf(reEmpty, nextCard + 1) != -1) {
sBStart = max_line;
}
}
}
int index = 0;
QRegularExpressionMatch match;
// Parse name and comments
while (index < deckStart) {
const auto &current = inputs.at(index++);
if (!current.contains(reEmpty)) {
match = reComment.match(current);
metadata.name = match.captured();
break;
}
}
while (index < deckStart) {
const auto &current = inputs.at(index++);
if (!current.contains(reEmpty)) {
match = reComment.match(current);
metadata.comments += match.captured() + '\n';
}
}
metadata.comments.chop(1);
// Discard empty lines
while (index < max_line && inputs.at(index).contains(reEmpty)) {
++index;
}
// Discard line if it starts with deck or mainboard, all cards until the sideboard starts are in the mainboard
if (inputs.at(index).contains(reDeckComment)) {
++index;
}
// Parse decklist
for (; index < max_line; ++index) {
// check if line is a card
match = reCardLine.match(inputs.at(index));
if (!match.hasMatch()) {
continue;
}
QString cardName = match.captured().simplified();
bool sideboard = false;
// Sideboard detection
if (sBStart < 0) {
match = reSBMark.match(cardName);
if (match.hasMatch()) {
sideboard = true;
cardName = match.captured(1);
}
} else {
if (index == sBStart) {
continue;
}
sideboard = index > sBStart;
}
// Extract set code, collector number, and foil
QString setCode;
QString collectorNumber;
bool isFoil = false;
// Check for foil status at the end of the card name
if (cardName.endsWith("*F*", Qt::CaseInsensitive)) {
isFoil = true;
cardName.chop(3); // Remove the "*F*" from the card name
}
Q_UNUSED(isFoil);
// Attempt to match the hyphen-separated format (PLST-2094)
match = reHyphenFormat.match(cardName);
if (match.hasMatch()) {
setCode = match.captured(2).toUpper();
collectorNumber = match.captured(3);
cardName = cardName.left(match.capturedStart()).trimmed();
} else {
// Attempt to match the regular format (PLST) 2094
match = reRegularFormat.match(cardName);
if (match.hasMatch()) {
setCode = match.captured(1).toUpper();
collectorNumber = match.captured(2);
cardName = cardName.left(match.capturedStart()).trimmed();
}
}
// check if a specific amount is mentioned
int amount = 1;
match = reMultiplier.match(cardName);
if (match.hasMatch()) {
amount = match.captured(1).toInt();
cardName = match.captured(2);
}
// Normalize the card name
cardName = cardNameNormalizer(cardName);
// Determine the zone (mainboard/sideboard)
QString zoneName = sideboard ? DECK_ZONE_SIDE : DECK_ZONE_MAIN;
// make new entry in decklist
tree.addCard(cardName, amount, zoneName, -1, setCode, collectorNumber);
}
refreshDeckHash();
return true;
}
bool DeckList::loadFromFile_Plain(QIODevice *device, const std::function<QString(const QString &)> &cardNameNormalizer)
{
QTextStream in(device);
return loadFromStream_Plain(in, false, cardNameNormalizer);
}
bool DeckList::saveToStream_Plain(QTextStream &stream, bool prefixSideboardCards, bool slashTappedOutSplitCards) const
{
auto writeToStream = [&stream, prefixSideboardCards, slashTappedOutSplitCards](const auto node, const auto card) {
if (prefixSideboardCards && node->getName() == DECK_ZONE_SIDE) {
stream << "SB: ";
}
if (!slashTappedOutSplitCards) {
stream << QString("%1 %2\n").arg(card->getNumber()).arg(card->getName());
} else {
stream << QString("%1 %2\n").arg(card->getNumber()).arg(card->getName().replace("//", "/"));
}
};
forEachCard(writeToStream);
return true;
}
bool DeckList::saveToFile_Plain(QIODevice *device, bool prefixSideboardCards, bool slashTappedOutSplitCards) const
{
QTextStream out(device);
return saveToStream_Plain(out, prefixSideboardCards, slashTappedOutSplitCards);
}
QString DeckList::writeToString_Plain(bool prefixSideboardCards, bool slashTappedOutSplitCards) const
{
QString result;
QTextStream out(&result);
saveToStream_Plain(out, prefixSideboardCards, slashTappedOutSplitCards);
return result;
}
/**
* Clears all cards and other data from the decklist
*
* @param preserveMetadata If true, only clear the cards
*/
void DeckList::cleanList(bool preserveMetadata)
{
tree.clear();
if (!preserveMetadata) {
metadata = {};
}
refreshDeckHash();
}
QStringList DeckList::getCardList(const QSet<QString> &restrictToZones) const
{
auto nodes = tree.getCardNodes(restrictToZones);
QStringList result;
std::transform(nodes.cbegin(), nodes.cend(), std::back_inserter(result), [](auto node) { return node->getName(); });
return result;
}
QList<CardRef> DeckList::getCardRefList(const QSet<QString> &restrictToZones) const
{
auto nodes = tree.getCardNodes(restrictToZones);
QList<CardRef> result;
std::transform(nodes.cbegin(), nodes.cend(), std::back_inserter(result),
[](auto node) { return node->toCardRef(); });
return result;
}
QList<const DecklistCardNode *> DeckList::getCardNodes(const QSet<QString> &restrictToZones) const
{
return tree.getCardNodes(restrictToZones);
}
QList<const InnerDecklistNode *> DeckList::getZoneNodes(const QSet<QString> &restrictToZones) const
{
return tree.getZoneNodes(restrictToZones);
}
int DeckList::getSideboardSize() const
{
auto cards = tree.getCardNodes({DECK_ZONE_SIDE});
int size = 0;
for (auto card : cards) {
size += card->getNumber();
}
return size;
}
DecklistCardNode *DeckList::addCard(const QString &cardName,
const QString &zoneName,
int position,
const QString &cardSetName,
const QString &cardSetCollectorNumber,
const QString &cardProviderId,
bool formatLegal)
{
auto node =
tree.addCard(cardName, 1, zoneName, position, cardSetName, cardSetCollectorNumber, cardProviderId, formatLegal);
refreshDeckHash();
return node;
}
/**
* Gets the deck hash.
* The hash is computed on the first call to this method, and is cached until the decklist is modified.
*
* @return The deck hash
*/
QString DeckList::getDeckHash() const
{
if (!cachedDeckHash.isEmpty()) {
return cachedDeckHash;
}
cachedDeckHash = tree.computeDeckHash();
return cachedDeckHash;
}
/**
* Invalidates the cached deckHash.
*/
void DeckList::refreshDeckHash()
{
cachedDeckHash = QString();
}
/**
* Calls a given function on each card in the deck.
*/
void DeckList::forEachCard(const std::function<void(InnerDecklistNode *, DecklistCardNode *)> &func) const
{
tree.forEachCard(func);
}
DeckListMemento DeckList::createMemento(const QString &reason) const
{
return DeckListMemento(writeToString_Native(), reason);
}
void DeckList::restoreMemento(const DeckListMemento &m)
{
cleanList();
loadFromString_Native(m.getMemento());
}
@@ -0,0 +1,259 @@
/**
* @file deck_list.h
* @brief Defines the DeckList class, which manages a full
* deck structure including cards, zones, sideboard plans, and
* serialization to/from multiple formats. This is a logic class which
* does not care about Qt or user facing views.
* See @c DeckListModel for the actual Qt Model to be used for views
*/
#ifndef DECKLIST_H
#define DECKLIST_H
#include "deck_list_memento.h"
#include "deck_list_node_tree.h"
#include "sideboard_plan.h"
#include "tree/inner_deck_list_node.h"
#include <QMap>
#include <QVector>
#include <QtCore/QXmlStreamReader>
#include <libcockatrice/utility/card_ref.h>
class AbstractDecklistNode;
class DecklistCardNode;
class CardDatabase;
class QIODevice;
class QTextStream;
class InnerDecklistNode;
/**
* @class DeckList
* @ingroup Decks
* @brief Represents a complete deck, including metadata, zones, cards,
* and sideboard plans.
*
* A DeckList is a wrapper around an `InnerDecklistNode` tree,
* enriched with metadata like deck name, comments, tags, banner card,
* and multiple sideboard plans.
*
* ### Core responsibilities:
* - Store and manage the root node tree (zones → groups → cards).
* - Provide deck-level metadata (name, comments, tags, banner).
* - Support multiple sideboard plans (meta-game strategies).
* - Provide import/export in multiple formats:
* - Cockatrice native XML format.
* - Plain-text list format.
* - Provide hashing for deck identity (deck hash).
*
* ### Ownership:
* - Owns the `DecklistNodeTree`.
* - Owns `SideboardPlan` instances stored in `sideboardPlans`.
*
* ### Example workflow:
* ```
* DeckList deck;
* deck.setName("Mono Red Aggro");
* deck.addCard("Lightning Bolt", "main");
* deck.addTag("Aggro");
* deck.saveToFile_Native(device);
* ```
*/
class DeckList
{
public:
struct Metadata
{
QString name; ///< User-defined deck name.
QString comments; ///< Free-form comments or notes.
QString gameFormat; ///< The name of the game format this deck contains legal cards for
CardRef bannerCard; ///< Optional representative card for the deck.
QStringList tags; ///< User-defined tags for deck classification.
QString lastLoadedTimestamp; ///< Timestamp string of last load.
/**
* @brief Checks if all values (except for lastLoadedTimestamp) in the metadata is empty.
*/
bool isEmpty() const;
};
private:
Metadata metadata; ///< Deck metadata that is stored in the deck file
QMap<QString, SideboardPlan> sideboardPlans; ///< Named sideboard plans.
DecklistNodeTree tree; ///< The deck tree (zones + cards).
/**
* @brief Cached deck hash, recalculated lazily.
* An empty string indicates the cache is invalid.
*/
mutable QString cachedDeckHash;
public:
/** @name Metadata setters */
///@{
void setName(const QString &_name = QString())
{
metadata.name = _name;
}
void setComments(const QString &_comments = QString())
{
metadata.comments = _comments;
}
void setTags(const QStringList &_tags = QStringList())
{
metadata.tags = _tags;
}
void addTag(const QString &_tag)
{
metadata.tags.append(_tag);
}
void clearTags()
{
metadata.tags.clear();
}
void setBannerCard(const CardRef &_bannerCard = {})
{
metadata.bannerCard = _bannerCard;
}
void setLastLoadedTimestamp(const QString &_lastLoadedTimestamp = QString())
{
metadata.lastLoadedTimestamp = _lastLoadedTimestamp;
}
void setGameFormat(const QString &_gameFormat = QString())
{
metadata.gameFormat = _gameFormat;
}
///@}
/** @brief Construct an empty deck. */
explicit DeckList();
/** @brief Construct from a serialized native-format string. */
explicit DeckList(const QString &nativeString);
/** @brief Construct from components. */
DeckList(const Metadata &metadata,
const DecklistNodeTree &tree,
const QMap<QString, SideboardPlan> &sideboardPlans = {});
/**
* @brief Gets a pointer to the underlying node tree.
* Note: DO NOT call this method unless the object needs to have access to the underlying model.
* For now, only the DeckListModel should be calling this.
*/
DecklistNodeTree *getTree()
{
return &tree;
}
/**
* @name Metadata getters
* The individual metadata getters still exist for backwards compatibility.
*/
///@{
//! \todo Figure out when we can remove them.
const Metadata &getMetadata() const
{
return metadata;
}
QString getName() const
{
return metadata.name;
}
QString getComments() const
{
return metadata.comments;
}
QStringList getTags() const
{
return metadata.tags;
}
CardRef getBannerCard() const
{
return metadata.bannerCard;
}
QString getLastLoadedTimestamp() const
{
return metadata.lastLoadedTimestamp;
}
QString getGameFormat() const
{
return metadata.gameFormat;
}
///@}
bool isBlankDeck() const
{
return metadata.isEmpty() && getCardList().isEmpty();
}
/** @name Sideboard plans */
///@{
QList<MoveCard_ToZone> getCurrentSideboardPlan() const;
void setCurrentSideboardPlan(const QList<MoveCard_ToZone> &plan);
const QMap<QString, SideboardPlan> &getSideboardPlans() const
{
return sideboardPlans;
}
///@}
/** @name Serialization (XML) */
///@{
bool readElement(QXmlStreamReader *xml);
void write(QXmlStreamWriter *xml) const;
bool loadFromXml(QXmlStreamReader *xml);
bool loadFromString_Native(const QString &nativeString);
QString writeToString_Native() const;
bool loadFromFile_Native(QIODevice *device);
bool saveToFile_Native(QIODevice *device) const;
///@}
/** @name Serialization (Plain text) */
///@{
bool loadFromStream_Plain(QTextStream &stream,
bool preserveMetadata,
const std::function<QString(const QString &)> &cardNameNormalizer);
bool loadFromFile_Plain(QIODevice *device, const std::function<QString(const QString &)> &cardNameNormalizer);
bool saveToStream_Plain(QTextStream &stream, bool prefixSideboardCards, bool slashTappedOutSplitCards) const;
bool
saveToFile_Plain(QIODevice *device, bool prefixSideboardCards = true, bool slashTappedOutSplitCards = false) const;
QString writeToString_Plain(bool prefixSideboardCards = true, bool slashTappedOutSplitCards = false) const;
///@}
/** @name Deck manipulation */
///@{
void cleanList(bool preserveMetadata = false);
bool isEmpty() const
{
return tree.isEmpty() && metadata.isEmpty() && sideboardPlans.isEmpty();
}
QStringList getCardList(const QSet<QString> &restrictToZones = {}) const;
QList<CardRef> getCardRefList(const QSet<QString> &restrictToZones = {}) const;
QList<const DecklistCardNode *> getCardNodes(const QSet<QString> &restrictToZones = {}) const;
QList<const InnerDecklistNode *> getZoneNodes(const QSet<QString> &restrictToZones = {}) const;
int getSideboardSize() const;
DecklistCardNode *addCard(const QString &cardName,
const QString &zoneName,
int position = -1,
const QString &cardSetName = QString(),
const QString &cardSetCollectorNumber = QString(),
const QString &cardProviderId = QString(),
const bool formatLegal = true);
///@}
/** @name Deck identity */
///@{
QString getDeckHash() const;
void refreshDeckHash();
///@}
/**
* @brief Apply a function to every card in the deck tree.
*
* @param func Function taking (zone node, card node).
*/
void forEachCard(const std::function<void(InnerDecklistNode *, DecklistCardNode *)> &func) const;
DeckListMemento createMemento(const QString &reason) const;
void restoreMemento(const DeckListMemento &m);
};
#endif
@@ -0,0 +1,55 @@
#include "deck_list_history_manager.h"
void DeckListHistoryManager::save(const DeckListMemento &memento)
{
undoStack.push(memento);
redoStack.clear();
emit undoRedoStateChanged();
}
void DeckListHistoryManager::clear()
{
undoStack.clear();
redoStack.clear();
emit undoRedoStateChanged();
}
void DeckListHistoryManager::undo(DeckList *deck)
{
if (undoStack.isEmpty()) {
return;
}
// Peek at the memento we are going to restore
const DeckListMemento &mementoToRestore = undoStack.top();
// Save current state for redo
DeckListMemento currentState = deck->createMemento(mementoToRestore.getReason());
redoStack.push(currentState);
// Pop the last state from undo stack and restore it
DeckListMemento memento = undoStack.pop();
deck->restoreMemento(memento);
emit undoRedoStateChanged();
}
void DeckListHistoryManager::redo(DeckList *deck)
{
if (redoStack.isEmpty()) {
return;
}
// Peek at the memento we are going to restore
const DeckListMemento &mementoToRestore = redoStack.top();
// Save current state for undo
DeckListMemento currentState = deck->createMemento(mementoToRestore.getReason());
undoStack.push(currentState);
// Pop the next state from redo stack and restore it
DeckListMemento memento = redoStack.pop();
deck->restoreMemento(memento);
emit undoRedoStateChanged();
}
@@ -0,0 +1,54 @@
#ifndef COCKATRICE_DECK_LIST_HISTORY_MANAGER_H
#define COCKATRICE_DECK_LIST_HISTORY_MANAGER_H
#include "deck_list.h"
#include "deck_list_memento.h"
#include <QObject>
#include <QStack>
class DeckListHistoryManager : public QObject
{
Q_OBJECT
signals:
void undoRedoStateChanged();
public:
explicit DeckListHistoryManager(QObject *parent = nullptr) : QObject(parent)
{
}
void save(const DeckListMemento &memento);
void clear();
[[nodiscard]] bool canUndo() const
{
return !undoStack.isEmpty();
}
[[nodiscard]] bool canRedo() const
{
return !redoStack.isEmpty();
}
void undo(DeckList *deck);
void redo(DeckList *deck);
[[nodiscard]] QStack<DeckListMemento> getRedoStack() const
{
return redoStack;
}
[[nodiscard]] QStack<DeckListMemento> getUndoStack() const
{
return undoStack;
}
private:
QStack<DeckListMemento> undoStack;
QStack<DeckListMemento> redoStack;
};
#endif // COCKATRICE_DECK_LIST_HISTORY_MANAGER_H
@@ -0,0 +1,28 @@
#ifndef COCKATRICE_DECK_LIST_MEMENTO_H
#define COCKATRICE_DECK_LIST_MEMENTO_H
#include <QString>
class DeckListMemento
{
public:
DeckListMemento() = default;
explicit DeckListMemento(const QString &memento, const QString &reason = QString())
: memento(memento), reason(reason)
{
}
[[nodiscard]] QString getMemento() const
{
return memento;
}
[[nodiscard]] QString getReason() const
{
return reason;
}
private:
QString memento;
QString reason;
};
#endif // COCKATRICE_DECK_LIST_MEMENTO_H
@@ -0,0 +1,187 @@
#include "deck_list_node_tree.h"
#include "tree/deck_list_card_node.h"
#include <QCryptographicHash>
#include <QSet>
DecklistNodeTree::DecklistNodeTree() : root(new InnerDecklistNode())
{
}
DecklistNodeTree::DecklistNodeTree(const DecklistNodeTree &other) : root(new InnerDecklistNode(other.root))
{
}
DecklistNodeTree &DecklistNodeTree::operator=(const DecklistNodeTree &other)
{
if (this != &other) {
delete root;
root = new InnerDecklistNode(other.root);
}
return *this;
}
DecklistNodeTree::~DecklistNodeTree()
{
delete root;
}
bool DecklistNodeTree::isEmpty() const
{
return root->isEmpty();
}
void DecklistNodeTree::clear()
{
root->clearTree();
}
QList<const DecklistCardNode *> DecklistNodeTree::getCardNodes(const QSet<QString> &restrictToZones) const
{
QList<const DecklistCardNode *> result;
for (auto *zoneNode : getZoneNodes(restrictToZones)) {
for (auto *cardNode : *zoneNode) {
auto *cardCardNode = dynamic_cast<DecklistCardNode *>(cardNode);
if (cardCardNode) {
result.append(cardCardNode);
}
}
}
return result;
}
QList<const InnerDecklistNode *> DecklistNodeTree::getZoneNodes(const QSet<QString> &restrictToZones) const
{
QList<const InnerDecklistNode *> zones;
for (auto *node : *root) {
InnerDecklistNode *currentZone = dynamic_cast<InnerDecklistNode *>(node);
if (!currentZone) {
continue;
}
if (!restrictToZones.isEmpty() && !restrictToZones.contains(currentZone->getName())) {
continue;
}
zones.append(currentZone);
}
return zones;
}
QString DecklistNodeTree::computeDeckHash() const
{
auto mainDeckNodes = getCardNodes({DECK_ZONE_MAIN});
auto sideDeckNodes = getCardNodes({DECK_ZONE_SIDE});
static auto nodesToCardList = [](const QList<const DecklistCardNode *> &nodes, const QString &prefix = {}) {
QStringList result;
for (auto node : nodes) {
for (int i = 0; i < node->getNumber(); ++i) {
result.append(prefix + node->getName().toLower());
}
}
return result;
};
QStringList cardList = nodesToCardList(mainDeckNodes) + nodesToCardList(sideDeckNodes, "SB:");
cardList.sort();
QByteArray deckHashArray = QCryptographicHash::hash(cardList.join(";").toUtf8(), QCryptographicHash::Sha1);
quint64 number = (((quint64)(unsigned char)deckHashArray[0]) << 32) +
(((quint64)(unsigned char)deckHashArray[1]) << 24) +
(((quint64)(unsigned char)deckHashArray[2] << 16)) +
(((quint64)(unsigned char)deckHashArray[3]) << 8) + (quint64)(unsigned char)deckHashArray[4];
return QString::number(number, 32).rightJustified(8, '0');
}
void DecklistNodeTree::write(QXmlStreamWriter *xml) const
{
for (int i = 0; i < root->size(); i++) {
root->at(i)->writeElement(xml);
}
}
void DecklistNodeTree::readZoneElement(QXmlStreamReader *xml)
{
QString zoneName = xml->attributes().value("name").toString();
InnerDecklistNode *newZone = getZoneObjFromName(zoneName);
newZone->readElement(xml);
}
DecklistCardNode *DecklistNodeTree::addCard(const QString &cardName,
int amount,
const QString &zoneName,
int position,
const QString &cardSetName,
const QString &cardSetCollectorNumber,
const QString &cardProviderId,
const bool formatLegal)
{
auto *zoneNode = getZoneObjFromName(zoneName);
auto *node = new DecklistCardNode(cardName, amount, zoneNode, position, cardSetName, cardSetCollectorNumber,
cardProviderId, formatLegal);
return node;
}
bool DecklistNodeTree::deleteNode(AbstractDecklistNode *node, InnerDecklistNode *rootNode)
{
if (node == root) {
return true;
}
if (rootNode == nullptr) {
rootNode = root;
}
int index = rootNode->indexOf(node);
if (index != -1) {
delete rootNode->takeAt(index);
if (rootNode->empty()) {
deleteNode(rootNode, rootNode->getParent());
}
return true;
}
for (int i = 0; i < rootNode->size(); i++) {
auto *inner = dynamic_cast<InnerDecklistNode *>(rootNode->at(i));
if (inner) {
if (deleteNode(node, inner)) {
return true;
}
}
}
return false;
}
void DecklistNodeTree::forEachCard(const std::function<void(InnerDecklistNode *, DecklistCardNode *)> &func) const
{
// Support for this is only possible if the internal structure
// doesn't get more complicated.
for (int i = 0; i < root->size(); i++) {
InnerDecklistNode *node = dynamic_cast<InnerDecklistNode *>(root->at(i));
for (int j = 0; j < node->size(); j++) {
DecklistCardNode *card = dynamic_cast<DecklistCardNode *>(node->at(j));
func(node, card);
}
}
}
/**
* Gets the InnerDecklistNode that is the root node for the given zone, creating a new node if it doesn't exist.
*/
InnerDecklistNode *DecklistNodeTree::getZoneObjFromName(const QString &zoneName) const
{
for (int i = 0; i < root->size(); i++) {
auto *node = dynamic_cast<InnerDecklistNode *>(root->at(i));
if (node->getName() == zoneName) {
return node;
}
}
return new InnerDecklistNode(zoneName, root);
}
@@ -0,0 +1,92 @@
#ifndef COCKATRICE_DECKLIST_NODE_TREE_H
#define COCKATRICE_DECKLIST_NODE_TREE_H
#include "libcockatrice/utility/card_ref.h"
#include "tree/deck_list_card_node.h"
#include "tree/inner_deck_list_node.h"
#include <QSet>
class DecklistNodeTree
{
InnerDecklistNode *root; ///< Root of the deck tree (zones + cards).
public:
/** @brief Constructs an empty DecklistNodeTree. */
explicit DecklistNodeTree();
/** @brief Copy constructor. Deep copies the tree. */
explicit DecklistNodeTree(const DecklistNodeTree &other);
/** @brief Copy-assignment operator. Deep copies the tree. */
DecklistNodeTree &operator=(const DecklistNodeTree &other);
virtual ~DecklistNodeTree();
/**
* @brief Gets a pointer to the underlying root node.
* Note: DO NOT call this method unless the object needs to have access to the underlying model.
* For now, only the DeckListModel should be calling this.
*/
InnerDecklistNode *getRoot() const
{
return root;
}
bool isEmpty() const;
/**
* @brief Deletes all nodes except the root.
*/
void clear();
/**
* Gets all card nodes in the tree
* @param restrictToZones Only get the nodes in these zones
* @return A QList containing all the card nodes in the zone.
*/
QList<const DecklistCardNode *> getCardNodes(const QSet<QString> &restrictToZones = {}) const;
/**
* Gets all zone nodes in the tree
* @param restrictToZones If not empty, only get the zone nodes with these names.
* @return A QList containing all the zone nodes in the tree.
*/
QList<const InnerDecklistNode *> getZoneNodes(const QSet<QString> &restrictToZones = {}) const;
/**
* @brief Computes the deck hash
*/
QString computeDeckHash() const;
/**
*@brief Writes the contents of the deck to xml
*/
void write(QXmlStreamWriter *xml) const;
/**
* @brief Reads a "zone" section of the xml to this tree
*/
void readZoneElement(QXmlStreamReader *xml);
DecklistCardNode *addCard(const QString &cardName,
int amount,
const QString &zoneName,
int position,
const QString &cardSetName = QString(),
const QString &cardSetCollectorNumber = QString(),
const QString &cardProviderId = QString(),
const bool formatLegal = true);
bool deleteNode(AbstractDecklistNode *node, InnerDecklistNode *rootNode = nullptr);
/**
* @brief Apply a function to every card in the deck tree. This can modify the cards.
*
* @param func Function taking (zone node, card node).
*/
void forEachCard(const std::function<void(InnerDecklistNode *, DecklistCardNode *)> &func) const;
private:
// Helpers for traversing the tree
InnerDecklistNode *getZoneObjFromName(const QString &zoneName) const;
};
#endif // COCKATRICE_DECKLIST_NODE_TREE_H
@@ -0,0 +1,61 @@
#include "sideboard_plan.h"
#include <QXmlStreamReader>
SideboardPlan::SideboardPlan(const QString &_name, const QList<MoveCard_ToZone> &_moveList)
: name(_name), moveList(_moveList)
{
}
void SideboardPlan::setMoveList(const QList<MoveCard_ToZone> &_moveList)
{
moveList = _moveList;
}
bool SideboardPlan::readElement(QXmlStreamReader *xml)
{
while (!xml->atEnd()) {
xml->readNext();
const QString childName = xml->name().toString();
if (xml->isStartElement()) {
if (childName == "name") {
name = xml->readElementText();
} else if (childName == "move_card_to_zone") {
MoveCard_ToZone m;
while (!xml->atEnd()) {
xml->readNext();
const QString childName2 = xml->name().toString();
if (xml->isStartElement()) {
if (childName2 == "card_name") {
m.set_card_name(xml->readElementText().toStdString());
} else if (childName2 == "start_zone") {
m.set_start_zone(xml->readElementText().toStdString());
} else if (childName2 == "target_zone") {
m.set_target_zone(xml->readElementText().toStdString());
}
} else if (xml->isEndElement() && (childName2 == "move_card_to_zone")) {
moveList.append(m);
break;
}
}
}
} else if (xml->isEndElement() && (childName == "sideboard_plan")) {
return true;
}
}
return false;
}
void SideboardPlan::write(QXmlStreamWriter *xml) const
{
xml->writeStartElement("sideboard_plan");
xml->writeTextElement("name", name);
for (auto &i : moveList) {
xml->writeStartElement("move_card_to_zone");
xml->writeTextElement("card_name", QString::fromStdString(i.card_name()));
xml->writeTextElement("start_zone", QString::fromStdString(i.start_zone()));
xml->writeTextElement("target_zone", QString::fromStdString(i.target_zone()));
xml->writeEndElement();
}
xml->writeEndElement();
}
@@ -0,0 +1,70 @@
#ifndef COCKATRICE_SIDEBOARD_PLAN_H
#define COCKATRICE_SIDEBOARD_PLAN_H
#include <QList>
#include <libcockatrice/protocol/pb/move_card_to_zone.pb.h>
class QXmlStreamWriter;
class QXmlStreamReader;
/**
* @class SideboardPlan
* @ingroup Decks
* @brief Represents a predefined sideboarding strategy for a deck.
*
* Sideboard plans store a named list of card movements that should be applied
* between the mainboard and sideboard for a specific matchup. Each movement
* is expressed using a `MoveCard_ToZone` protobuf message.
*
* ### Responsibilities:
* - Store the plan name and list of moves.
* - Support XML serialization/deserialization.
*
* ### Typical usage:
* A deck can contain multiple sideboard plans (e.g., "vs Aggro", "vs Control"),
* each describing how to transform the main deck into its intended configuration.
*/
class SideboardPlan
{
private:
QString name; ///< Human-readable name of this plan.
QList<MoveCard_ToZone> moveList; ///< List of move instructions for this plan.
public:
/**
* @brief Construct a new SideboardPlan.
* @param _name The plan name.
* @param _moveList Initial list of card move instructions.
*/
explicit SideboardPlan(const QString &_name = "", const QList<MoveCard_ToZone> &_moveList = {});
/**
* @brief Read a SideboardPlan from an XML stream.
* @param xml XML reader positioned at the plan element.
* @return true if parsing succeeded.
*/
bool readElement(QXmlStreamReader *xml);
/**
* @brief Write this SideboardPlan to XML.
* @param xml Stream to append the serialized element to.
*/
void write(QXmlStreamWriter *xml) const;
/** @return The plan name. */
[[nodiscard]] QString getName() const
{
return name;
}
/** @return Const reference to the move list. */
[[nodiscard]] const QList<MoveCard_ToZone> &getMoveList() const
{
return moveList;
}
/** @brief Replace the move list with a new one. */
void setMoveList(const QList<MoveCard_ToZone> &_moveList);
};
#endif // COCKATRICE_SIDEBOARD_PLAN_H
@@ -0,0 +1,63 @@
#include "abstract_deck_list_card_node.h"
bool AbstractDecklistCardNode::compare(AbstractDecklistNode *other) const
{
switch (sortMethod) {
case ByNumber:
return compareNumber(other);
case ByName:
return compareName(other);
default:
return false;
}
}
bool AbstractDecklistCardNode::compareNumber(AbstractDecklistNode *other) const
{
auto *other2 = dynamic_cast<AbstractDecklistCardNode *>(other);
if (other2) {
int n1 = getNumber();
int n2 = other2->getNumber();
return (n1 != n2) ? (n1 > n2) : compareName(other);
} else {
return true;
}
}
bool AbstractDecklistCardNode::compareName(AbstractDecklistNode *other) const
{
auto *other2 = dynamic_cast<AbstractDecklistCardNode *>(other);
if (other2) {
return (getName() > other2->getName());
} else {
return true;
}
}
bool AbstractDecklistCardNode::readElement(QXmlStreamReader *xml)
{
while (!xml->atEnd()) {
xml->readNext();
if (xml->isEndElement() && xml->name().toString() == "card") {
return false;
}
}
return true;
}
void AbstractDecklistCardNode::writeElement(QXmlStreamWriter *xml)
{
xml->writeEmptyElement("card");
xml->writeAttribute("number", QString::number(getNumber()));
xml->writeAttribute("name", getName());
if (!getCardSetShortName().isEmpty()) {
xml->writeAttribute("setShortName", getCardSetShortName());
}
if (!getCardCollectorNumber().isEmpty()) {
xml->writeAttribute("collectorNumber", getCardCollectorNumber());
}
if (!getCardProviderId().isEmpty()) {
xml->writeAttribute("uuid", getCardProviderId());
}
}
@@ -0,0 +1,155 @@
/**
* @file abstract_deck_list_card_node.h
* @brief Defines the AbstractDecklistCardNode base class, which adds
* card-specific behavior on top of AbstractDecklistNode.
*
* This class is the intermediate abstract base between the generic
* AbstractDecklistNode and concrete card entries such as DecklistCardNode
* or DecklistModelCardNode.
*/
#ifndef COCKATRICE_ABSTRACT_DECK_LIST_CARD_NODE_H
#define COCKATRICE_ABSTRACT_DECK_LIST_CARD_NODE_H
#include "abstract_deck_list_node.h"
/**
* @class AbstractDecklistCardNode
* @ingroup DeckModels
* @brief Abstract base class for all deck list nodes that represent
* actual card entries.
*
* While AbstractDecklistNode provides the general interface for all
* nodes in the deck tree (zones, groups, cards), this subclass refines
* the interface to cover properties specific to *cards*:
* - Quantity (number of copies).
* - Name.
* - Set code and collector number.
* - Provider ID.
*
* ### Role in the hierarchy:
* - Leaf-oriented abstract class; no children of its own.
* - Serves as the base for concrete implementations:
* - @c DecklistCardNode: Stores real card data in the deck tree.
* - @c DecklistModelCardNode: Wraps a DecklistCardNode for use
* in the Qt model layer.
*
* ### Responsibilities:
* - Defines getters/setters for all card-identifying attributes.
* - Provides comparison logic for sorting by name or number.
* - Implements XML serialization for saving/loading deck files.
*
* ### Ownership:
* - As with all nodes, owned by its parent InnerDecklistNode.
*/
class AbstractDecklistCardNode : public AbstractDecklistNode
{
public:
/**
* @brief Construct a new AbstractDecklistCardNode.
*
* @param _parent Optional parent node. If provided, this node
* will be inserted into the parents children list.
* @param position Index at which to insert into parents children.
* If -1, the node is appended to the end.
*/
explicit AbstractDecklistCardNode(InnerDecklistNode *_parent = nullptr, int position = -1)
: AbstractDecklistNode(_parent, position)
{
}
/** @return The number of copies of this card in the deck. */
[[nodiscard]] virtual int getNumber() const = 0;
/** @param _number Set the number of copies of this card. */
virtual void setNumber(int _number) = 0;
/** @return The display name of this card. */
[[nodiscard]] QString getName() const override = 0;
/** @param _name Set the display name of this card. */
virtual void setName(const QString &_name) = 0;
/** @return The provider identifier for this card (e.g., UUID). */
[[nodiscard]] virtual QString getCardProviderId() const override = 0;
/** @param _cardProviderId Set the provider identifier for this card. */
virtual void setCardProviderId(const QString &_cardProviderId) = 0;
/** @return The abbreviated set code (e.g., "NEO"). */
[[nodiscard]] virtual QString getCardSetShortName() const override = 0;
/** @param _cardSetShortName Set the abbreviated set code. */
virtual void setCardSetShortName(const QString &_cardSetShortName) = 0;
/** @return The collector number of the card within its set. */
[[nodiscard]] virtual QString getCardCollectorNumber() const override = 0;
/** @param _cardSetNumber Set the collector number. */
virtual void setCardCollectorNumber(const QString &_cardSetNumber) = 0;
/** @return The format legality of the card. */
virtual bool getFormatLegality() const = 0;
/** @param _formatLegal If the card is considered legal. */
virtual void setFormatLegality(const bool _formatLegal) = 0;
/**
* @brief Get the height of this node in the tree.
*
* For card nodes, height is always 0 because they are leaf nodes
* and do not contain children.
*
* @return 0
*/
[[nodiscard]] int height() const override
{
return 0;
}
/**
* @brief Compare this card node against another for sorting.
*
* Uses the nodes current @c sortMethod to determine how to compare:
* - ByName: Alphabetical comparison.
* - ByNumber: Numerical comparison.
* - Default: Falls back to implementation-defined behavior.
*
* @param other Another node to compare against.
* @return true if this node should sort before @p other.
*/
bool compare(AbstractDecklistNode *other) const override;
/**
* @brief Compare this card node to another by quantity.
* @param other Node to compare against.
* @return true if this nodes number < others number.
*/
bool compareNumber(AbstractDecklistNode *other) const;
/**
* @brief Compare this card node to another by name.
* @param other Node to compare against.
* @return true if this nodes name comes before others name.
*/
bool compareName(AbstractDecklistNode *other) const;
/**
* @brief Deserialize this nodes properties from XML.
* @param xml QXmlStreamReader positioned at the element.
* @return true if parsing succeeded.
*
* This supports loading deck files from Cockatrices XML format.
*/
bool readElement(QXmlStreamReader *xml) override;
/**
* @brief Serialize this nodes properties to XML.
* @param xml Writer to append this nodes XML element.
*
* This supports saving deck files to Cockatrices XML format.
*/
void writeElement(QXmlStreamWriter *xml) override;
};
#endif // COCKATRICE_ABSTRACT_DECK_LIST_CARD_NODE_H
@@ -0,0 +1,24 @@
#include "abstract_deck_list_node.h"
#include "inner_deck_list_node.h"
AbstractDecklistNode::AbstractDecklistNode(InnerDecklistNode *_parent, int position)
: parent(_parent), sortMethod(Default)
{
if (parent) {
if (position == -1) {
parent->append(this);
} else {
parent->insert(position, this);
}
}
}
int AbstractDecklistNode::depth() const
{
if (parent) {
return parent->depth() + 1;
} else {
return 0;
}
}
@@ -0,0 +1,185 @@
/**
* @file abstract_deck_list_node.h
* @brief Defines the AbstractDecklistNode base class used as the foundation
* for all nodes in the deck list tree (zones, groups, and cards).
*
* The deck list is modeled as a tree:
* - The invisible root node is managed by DeckListModel.
* - Top-level children are zones (e.g. Mainboard, Sideboard).
* - Zones contain grouping nodes (e.g. by type, color, or mana cost).
* - Grouping nodes contain card nodes.
*
* This abstract base class provides the interface and shared functionality
* for all node types. Concrete subclasses (InnerDecklistNode,
* DecklistCardNode, DecklistModelCardNode, etc.) implement the specifics.
*/
#ifndef COCKATRICE_ABSTRACT_DECK_LIST_NODE_H
#define COCKATRICE_ABSTRACT_DECK_LIST_NODE_H
#include <QtCore/QXmlStreamWriter>
/**
* @enum DeckSortMethod
* @ingroup DeckModels
* @brief Defines the different sort strategies a node may use
* to order its children.
*
* Sorting behavior is typically set by the DeckListModel when the user
* requests sorting in the UI.
*
* - ByNumber: Sort numerically (often by collector number).
* - ByName: Sort alphabetically by card name.
* - Default: No explicit sorting; insertion order is preserved.
*/
enum DeckSortMethod
{
ByNumber, ///< Sort by numeric properties (e.g. collector number).
ByName, ///< Sort by card name (locale-aware comparison).
Default ///< Leave in insertion order.
};
class InnerDecklistNode;
/**
* @class AbstractDecklistNode
* @ingroup DeckModels
* @brief Base class for all nodes in the deck list tree.
*
* This class defines the common interface for every node in the
* deck representation: zones, groupings, and cards.
*
* Responsibilities:
* - Maintain a pointer to its parent (if any).
* - Track the sorting method to be used for child nodes.
* - Provide a consistent interface for retrieving basic identifying
* properties (name, set, collector number, provider ID).
* - Define abstract methods for XML serialization, used when saving
* or loading deck files.
*
* Lifetime / Ownership:
* - Nodes are arranged hierarchically under @c InnerDecklistNode parents.
* - The parent takes ownership of its children; destruction cascades.
* - The DeckListModel holds the invisible root node, which in turn
* owns the entire hierarchy.
*
* Extension:
* - @c InnerDecklistNode is the concrete subclass representing
* "folders" in the tree (zones, groups).
* - @c DecklistCardNode and @c DecklistModelCardNode represent
* actual card entries.
*/
class AbstractDecklistNode
{
protected:
/**
* @brief Pointer to the parent node, or nullptr if this is the root.
*
* Ownership note: The parent is responsible for destroying this node
* when it is removed from the tree.
*/
InnerDecklistNode *parent;
/**
* @brief Current sorting strategy for this node's children.
*
* Sorting is applied recursively by the DeckListModel when
* the view requests it.
*/
DeckSortMethod sortMethod;
public:
/**
* @brief Construct a new AbstractDecklistNode and insert it into its parent.
*
* @param _parent Parent node. May be nullptr if this is the root.
* @param position Optional index at which to insert into the parent's
* children. If -1, the node is appended to the end.
*
* If a parent is provided, the constructor automatically appends
* or inserts this node into the parents child list.
*/
explicit AbstractDecklistNode(InnerDecklistNode *_parent = nullptr, int position = -1);
/** @brief Virtual destructor. Child classes must clean up their resources. */
virtual ~AbstractDecklistNode() = default;
/**
* @brief Set the sort method for this nodes children.
* @param method The sorting strategy to use.
*
* Subclasses may override if they need to apply additional logic.
*/
virtual void setSortMethod(DeckSortMethod method)
{
sortMethod = method;
}
/**
* @name Core identification properties
*
* These methods provide a standard way for the model to retrieve
* identifying information about a node, regardless of type.
* @{
*/
[[nodiscard]] virtual QString getName() const = 0;
[[nodiscard]] virtual QString getCardProviderId() const = 0;
[[nodiscard]] virtual QString getCardSetShortName() const = 0;
[[nodiscard]] virtual QString getCardCollectorNumber() const = 0;
/// @}
/**
* @brief Whether this node is the "deck header" (deck metadata).
*
* This distinguishes special nodes that represent deck-level
* information rather than cards or groupings.
*/
[[nodiscard]] virtual bool isDeckHeader() const = 0;
/** @return The parent node, or nullptr if this is the root. */
[[nodiscard]] InnerDecklistNode *getParent() const
{
return parent;
}
/**
* @brief Compute the depth of this node in the tree.
* @return Distance from the root (root = 0, children = 1, etc.).
*/
[[nodiscard]] int depth() const;
/**
* @brief Compute the "height" of this node.
*
* Height is defined by subclasses; it usually represents how
* many levels of descendants this node spans.
*
* For example:
* - A card node has height 1.
* - A group node containing cards has height 2.
*/
[[nodiscard]] virtual int height() const = 0;
/**
* @brief Compare this node against another for sorting.
*
* The semantics of comparison depend on the node type and the
* current @c sortMethod.
*
* @param other The node to compare against.
* @return true if this node should come before @p other.
*/
virtual bool compare(AbstractDecklistNode *other) const = 0;
/**
* @name XML serialization
* These methods support reading and writing decks from/to
* Cockatrice deck XML format.
* @{
*/
virtual bool readElement(QXmlStreamReader *xml) = 0;
virtual void writeElement(QXmlStreamWriter *xml) = 0;
/// @}
};
#endif // COCKATRICE_ABSTRACT_DECK_LIST_NODE_H
@@ -0,0 +1,8 @@
#include "deck_list_card_node.h"
DecklistCardNode::DecklistCardNode(DecklistCardNode *other, InnerDecklistNode *_parent)
: AbstractDecklistCardNode(_parent), name(other->getName()), number(other->getNumber()),
cardSetShortName(other->getCardSetShortName()), cardSetNumber(other->getCardCollectorNumber()),
cardProviderId(other->getCardProviderId())
{
}
@@ -0,0 +1,186 @@
/**
* @file deck_list_card_node.h
* @brief Defines the DecklistCardNode class, representing a single card entry
* in the deck list tree.
*
* DecklistCardNode is the concrete data-bearing node that corresponds to
* an individual card entry in a deck. It stores the cards name, quantity,
* set information, and provider ID. These nodes live inside an
* InnerDecklistNode (e.g., under Mainboard → Group → Card).
*/
#ifndef COCKATRICE_DECK_LIST_CARD_NODE_H
#define COCKATRICE_DECK_LIST_CARD_NODE_H
#include "abstract_deck_list_card_node.h"
#include <libcockatrice/utility/card_ref.h>
/**
* @class DecklistCardNode
* @ingroup DeckModels
* @brief Concrete node type representing an actual card entry in the deck.
*
* This class extends AbstractDecklistCardNode to hold all information
* needed to uniquely identify a card printing within the deck.
*
* ### Role in the hierarchy:
* - Child of an InnerDecklistNode (which groups cards by zone or criteria).
* - Leaf node in the deck tree; it does not contain further children.
*
* ### Data stored:
* - @c name: Cards display name.
* - @c number: Quantity of this card in the deck.
* - @c cardSetShortName: Abbreviation of the set (e.g., "NEO" for Neon Dynasty).
* - @c cardSetNumber: Collector number within the set.
* - @c cardProviderId: External provider identifier (e.g., UUID or MTGJSON ID).
*
* ### Usage:
* - Constructed directly when building a deck list from user input or file.
* - Used by DeckListModel to present cards in Qt views.
* - Convertible to @c CardRef for database lookups or cross-references.
*
* ### Ownership:
* - Owned by its parent InnerDecklistNode.
* - Destroyed automatically when its parent is destroyed.
*/
class DecklistCardNode : public AbstractDecklistCardNode
{
QString name; ///< Display name of the card.
int number; ///< Quantity of this card in the deck.
QString cardSetShortName; ///< Short set code (e.g., "NEO").
QString cardSetNumber; ///< Collector number within the set.
QString cardProviderId; ///< External provider identifier (e.g., UUID).
bool formatLegal; ///< Format legality
public:
/**
* @brief Construct a new DecklistCardNode.
*
* @param _name Display name of the card.
* @param _number Quantity of this card (default = 1).
* @param _parent Parent node in the tree (zone or group). May be nullptr.
* @param position Index to insert into parents children. -1 = append.
* @param _cardSetShortName Short set code (e.g., "NEO").
* @param _cardSetNumber Collector number within the set.
* @param _cardProviderId External provider ID (e.g., UUID).
* @param _formatLegality If the card is legal in the format
*
* On construction, if a parent is provided, this node is inserted into
* the parents children list automatically.
*/
explicit DecklistCardNode(QString _name = QString(),
int _number = 1,
InnerDecklistNode *_parent = nullptr,
int position = -1,
QString _cardSetShortName = QString(),
QString _cardSetNumber = QString(),
QString _cardProviderId = QString(),
bool _formatLegality = true)
: AbstractDecklistCardNode(_parent, position), name(std::move(_name)), number(_number),
cardSetShortName(std::move(_cardSetShortName)), cardSetNumber(std::move(_cardSetNumber)),
cardProviderId(std::move(_cardProviderId)), formatLegal(_formatLegality)
{
}
/**
* @brief Copy constructor with new parent assignment.
* @param other Existing DecklistCardNode to copy.
* @param _parent Parent node for the copy.
*
* Creates a deep copy of the card nodes properties, but attaches
* the new instance to a different parent in the tree.
*/
explicit DecklistCardNode(DecklistCardNode *other, InnerDecklistNode *_parent);
/** @return The quantity of this card. */
[[nodiscard]] int getNumber() const override
{
return number;
}
/** @param _number Set the quantity of this card. */
void setNumber(int _number) override
{
number = _number;
}
/** @return The display name of this card. */
[[nodiscard]] QString getName() const override
{
return name;
}
/** @param _name Set the display name of this card. */
void setName(const QString &_name) override
{
name = _name;
}
/** @return The provider identifier for this card. */
[[nodiscard]] QString getCardProviderId() const override
{
return cardProviderId;
}
/** @param _providerId Set the provider identifier for this card. */
void setCardProviderId(const QString &_providerId) override
{
cardProviderId = _providerId;
}
/** @return The short set code (e.g., "NEO"). */
[[nodiscard]] QString getCardSetShortName() const override
{
return cardSetShortName;
}
/** @param _cardSetShortName Set the short set code. */
void setCardSetShortName(const QString &_cardSetShortName) override
{
cardSetShortName = _cardSetShortName;
}
/** @return The collector number of this card within its set. */
[[nodiscard]] QString getCardCollectorNumber() const override
{
return cardSetNumber;
}
/** @param _cardSetNumber Set the collector number. */
void setCardCollectorNumber(const QString &_cardSetNumber) override
{
cardSetNumber = _cardSetNumber;
}
/** @return The format legality of the card. */
[[nodiscard]] bool getFormatLegality() const override
{
return formatLegal;
}
/** @param _formatLegal If the card is considered legal. */
void setFormatLegality(const bool _formatLegal) override
{
formatLegal = _formatLegal;
}
/** @return Always false; card nodes are not deck headers. */
[[nodiscard]] bool isDeckHeader() const override
{
return false;
}
/**
* @brief Convert this node to a CardRef.
*
* @return A CardRef with the cards name and provider ID, suitable
* for database lookups or comparison with other card sources.
*/
[[nodiscard]] CardRef toCardRef() const
{
return {name, cardProviderId};
}
};
#endif // COCKATRICE_DECK_LIST_CARD_NODE_H
@@ -0,0 +1,202 @@
#include "inner_deck_list_node.h"
#include "deck_list_card_node.h"
InnerDecklistNode::InnerDecklistNode(InnerDecklistNode *other, InnerDecklistNode *_parent)
: AbstractDecklistNode(_parent), name(other->getName())
{
for (int i = 0; i < other->size(); ++i) {
auto *inner = dynamic_cast<InnerDecklistNode *>(other->at(i));
if (inner) {
new InnerDecklistNode(inner, this);
} else {
new DecklistCardNode(dynamic_cast<DecklistCardNode *>(other->at(i)), this);
}
}
}
InnerDecklistNode::~InnerDecklistNode()
{
clearTree();
}
QString InnerDecklistNode::visibleNameFromName(const QString &_name)
{
if (_name == DECK_ZONE_MAIN) {
return QObject::tr("Maindeck");
} else if (_name == DECK_ZONE_SIDE) {
return QObject::tr("Sideboard");
} else if (_name == DECK_ZONE_TOKENS) {
return QObject::tr("Tokens");
} else {
return _name;
}
}
void InnerDecklistNode::setSortMethod(DeckSortMethod method)
{
sortMethod = method;
for (int i = 0; i < size(); i++) {
at(i)->setSortMethod(method);
}
}
QString InnerDecklistNode::getVisibleName() const
{
return visibleNameFromName(name);
}
void InnerDecklistNode::clearTree()
{
for (int i = 0; i < size(); i++) {
delete at(i);
}
clear();
}
AbstractDecklistNode *InnerDecklistNode::findChild(const QString &_name)
{
for (int i = 0; i < size(); i++) {
if (at(i)->getName() == _name) {
return at(i);
}
}
return nullptr;
}
AbstractDecklistNode *InnerDecklistNode::findCardChildByNameProviderIdAndNumber(const QString &_name,
const QString &_providerId,
const QString &_cardNumber)
{
for (const auto &i : *this) {
if (!i || i->getName() != _name) {
continue;
}
if (_cardNumber != "" && i->getCardCollectorNumber() != _cardNumber) {
continue;
}
if (_providerId != "" && i->getCardProviderId() != _providerId) {
continue;
}
return i;
}
return nullptr;
}
int InnerDecklistNode::height() const
{
return at(0)->height() + 1;
}
int InnerDecklistNode::recursiveCount(bool countTotalCards) const
{
int result = 0;
for (int i = 0; i < size(); i++) {
auto *node = dynamic_cast<InnerDecklistNode *>(at(i));
if (node) {
result += node->recursiveCount(countTotalCards);
} else if (countTotalCards) {
result += dynamic_cast<AbstractDecklistCardNode *>(at(i))->getNumber();
} else {
result++;
}
}
return result;
}
bool InnerDecklistNode::compare(AbstractDecklistNode *other) const
{
switch (sortMethod) {
case ByNumber:
return compareNumber(other);
case ByName:
return compareName(other);
default:
return false;
}
}
bool InnerDecklistNode::compareNumber(AbstractDecklistNode *other) const
{
auto *other2 = dynamic_cast<InnerDecklistNode *>(other);
if (other2) {
int n1 = recursiveCount(true);
int n2 = other2->recursiveCount(true);
return (n1 != n2) ? (n1 > n2) : compareName(other);
} else {
return false;
}
}
bool InnerDecklistNode::compareName(AbstractDecklistNode *other) const
{
auto *other2 = dynamic_cast<InnerDecklistNode *>(other);
if (other2) {
return (getName() > other2->getName());
} else {
return false;
}
}
bool InnerDecklistNode::readElement(QXmlStreamReader *xml)
{
while (!xml->atEnd()) {
xml->readNext();
const QString childName = xml->name().toString();
if (xml->isStartElement()) {
if (childName == "zone") {
auto *newZone = new InnerDecklistNode(xml->attributes().value("name").toString(), this);
newZone->readElement(xml);
} else if (childName == "card") {
auto *newCard = new DecklistCardNode(
xml->attributes().value("name").toString(), xml->attributes().value("number").toString().toInt(),
this, -1, xml->attributes().value("setShortName").toString(),
xml->attributes().value("collectorNumber").toString(), xml->attributes().value("uuid").toString());
newCard->readElement(xml);
}
} else if (xml->isEndElement() && (childName == "zone")) {
return false;
}
}
return true;
}
void InnerDecklistNode::writeElement(QXmlStreamWriter *xml)
{
xml->writeStartElement("zone");
xml->writeAttribute("name", name);
for (int i = 0; i < size(); i++) {
at(i)->writeElement(xml);
}
xml->writeEndElement(); // zone
}
QVector<QPair<int, int>> InnerDecklistNode::sort(Qt::SortOrder order)
{
QVector<QPair<int, int>> result(size());
// Initialize temporary list with contents of current list
QVector<QPair<int, AbstractDecklistNode *>> tempList(size());
for (int i = size() - 1; i >= 0; --i) {
tempList[i].first = i;
tempList[i].second = at(i);
}
// Sort temporary list
auto cmp = [order](const auto &a, const auto &b) {
return (order == Qt::AscendingOrder) ? (b.second->compare(a.second)) : (a.second->compare(b.second));
};
std::sort(tempList.begin(), tempList.end(), cmp);
// Map old indexes to new indexes and
// copy temporary list to the current one
for (int i = size() - 1; i >= 0; --i) {
result[i].first = tempList[i].first;
result[i].second = i;
replace(i, tempList[i].second);
}
return result;
}
@@ -0,0 +1,228 @@
/**
* @file inner_deck_list_node.h
* @brief Defines the InnerDecklistNode class, which represents
* structural nodes (zones and groups) in the deck tree.
*
* The deck tree consists of:
* - A root node (invisible).
* - Zones (Main, Sideboard, Tokens).
* - Optional grouping nodes (e.g., by type, color, or mana cost).
* - Card nodes as leaves.
*
* InnerDecklistNode implements the zone/group nodes and provides
* storage and management of child nodes.
*/
#ifndef COCKATRICE_INNER_DECK_LIST_NODE_H
#define COCKATRICE_INNER_DECK_LIST_NODE_H
#include "abstract_deck_list_node.h"
/** @brief Constant for the "main" deck zone name. */
#define DECK_ZONE_MAIN "main"
/** @brief Constant for the "sideboard" zone name. */
#define DECK_ZONE_SIDE "side"
/** @brief Constant for the "tokens" zone name. */
#define DECK_ZONE_TOKENS "tokens"
/**
* @class InnerDecklistNode
* @brief Represents a container node in the deck list hierarchy
* (zones and groupings).
*
* Unlike DecklistCardNode, which holds leaf card data, this class
* manages collections of child nodes, which may themselves be
* InnerDecklistNode or DecklistCardNode objects.
*
* ### Role in the hierarchy:
* - Root node (invisible): Holds zones.
* - Zone nodes: "main", "side", "tokens".
* - Grouping nodes: Created dynamically when grouping by type,
* color, or mana cost.
* - Card nodes: Always children of an InnerDecklistNode.
*
* ### Design notes:
* - Inherits from AbstractDecklistNode (tree interface) and
* QList<AbstractDecklistNode*> (storage of children).
* This allows direct QList-style manipulation of children while
* still presenting a polymorphic node interface.
*
* ### Responsibilities:
* - Store a display name.
* - Own and manage child nodes (insert, clear, find).
* - Provide recursive operations such as counting cards or computing height.
* - Implement sorting logic for reordering children.
* - Implement XML serialization for persistence.
*
* ### Ownership:
* - Owns all child nodes stored in the QList. The destructor
* recursively deletes children.
*/
class InnerDecklistNode : public AbstractDecklistNode, public QList<AbstractDecklistNode *>
{
QString name; ///< Internal identifier for this node (zone or group name).
public:
/**
* @brief Construct a new InnerDecklistNode.
*
* @param _name Internal name (e.g., "main", "side", "tokens", or group label).
* @param _parent Parent node (may be nullptr for the root).
* @param position Optional index for insertion into parent. -1 = append.
*/
explicit InnerDecklistNode(QString _name = QString(), InnerDecklistNode *_parent = nullptr, int position = -1)
: AbstractDecklistNode(_parent, position), name(std::move(_name))
{
}
/**
* @brief Copy constructor with parent reassignment.
* @param other Node to copy from (deep copy of children).
* @param _parent Parent node for the copy.
*/
explicit InnerDecklistNode(InnerDecklistNode *other, InnerDecklistNode *_parent = nullptr);
/**
* @brief Destructor. Recursively deletes all child nodes.
*/
~InnerDecklistNode() override;
/**
* @brief Set the sorting method for this node and all children.
* @param method Sort method to apply recursively.
*/
void setSortMethod(DeckSortMethod method) override;
/** @return The internal name of this node. */
[[nodiscard]] QString getName() const override
{
return name;
}
/** @param _name Set the internal name of this node. */
void setName(const QString &_name)
{
name = _name;
}
/**
* @brief Translate an internal name into a user-visible name.
*
* For example, the internal string "main" is presented as
* "Mainboard" in the UI.
*
* @param _name Internal identifier.
* @return Display-friendly string.
*/
static QString visibleNameFromName(const QString &_name);
/**
* @brief Get this nodes display-friendly name.
* @return Human-readable name (zone/group name).
*/
[[nodiscard]] virtual QString getVisibleName() const;
/** @return Always empty for container nodes. */
[[nodiscard]] QString getCardProviderId() const override
{
return "";
}
/** @return Always empty for container nodes. */
[[nodiscard]] QString getCardSetShortName() const override
{
return "";
}
/** @return Always empty for container nodes. */
[[nodiscard]] QString getCardCollectorNumber() const override
{
return "";
}
/** @return Always true; InnerDecklistNode represents deck structure. */
[[nodiscard]] bool isDeckHeader() const override
{
return true;
}
/**
* @brief Delete all children of this node, recursively.
*/
void clearTree();
/**
* @brief Find a direct child node by name.
* @param _name Name to match.
* @return Pointer to child node, or nullptr if not found.
*/
AbstractDecklistNode *findChild(const QString &_name);
/**
* @brief Find a child card node by name, provider ID, and collector number.
*
* Searches immediate children only.
*
* @param _name Card name to match.
* @param _providerId Optional provider ID to match.
* @param _cardNumber Optional collector number to match.
* @return Pointer to child node if found, nullptr otherwise.
*/
AbstractDecklistNode *findCardChildByNameProviderIdAndNumber(const QString &_name,
const QString &_providerId = "",
const QString &_cardNumber = "");
/**
* @brief Compute the height of this node.
* @return Maximum depth of descendants + 1.
*/
[[nodiscard]] int height() const override;
/**
* @brief Count cards recursively under this node.
* @param countTotalCards If true, sums up quantities of cards.
* If false, counts unique card nodes.
* @return Total count.
*/
[[nodiscard]] int recursiveCount(bool countTotalCards = false) const;
/**
* @brief Compare this node against another for sorting.
*
* Uses current @c sortMethod to determine the comparison.
*
* @param other Node to compare.
* @return true if this node should sort before @p other.
*/
bool compare(AbstractDecklistNode *other) const override;
/** @copydoc compare(AbstractDecklistNode*) const */
bool compareNumber(AbstractDecklistNode *other) const;
/** @copydoc compare(AbstractDecklistNode*) const */
bool compareName(AbstractDecklistNode *other) const;
/**
* @brief Sort this nodes children recursively.
*
* @param order Ascending or descending.
* @return A QVector of (oldIndex, newIndex) pairs indicating
* how children were reordered.
*/
QVector<QPair<int, int>> sort(Qt::SortOrder order = Qt::AscendingOrder);
/**
* @brief Deserialize this node and its children from XML.
* @param xml Reader positioned at this element.
* @return true if parsing succeeded.
*/
bool readElement(QXmlStreamReader *xml) override;
/**
* @brief Serialize this node and its children to XML.
* @param xml Writer to append elements to.
*/
void writeElement(QXmlStreamWriter *xml) override;
};
#endif // COCKATRICE_INNER_DECK_LIST_NODE_H