mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2025-12-20 22:32:47 -08:00
713 lines
23 KiB
C++
713 lines
23 KiB
C++
#include "deck_list.h"
|
||
|
||
#include "abstract_deck_list_node.h"
|
||
#include "deck_list_card_node.h"
|
||
#include "inner_deck_list_node.h"
|
||
|
||
#include <QCryptographicHash>
|
||
#include <QDebug>
|
||
#include <QFile>
|
||
#include <QRegularExpression>
|
||
#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
|
||
|
||
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)
|
||
{
|
||
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();
|
||
}
|
||
|
||
DeckList::DeckList()
|
||
{
|
||
root = new InnerDecklistNode;
|
||
}
|
||
|
||
// TODO: https://qt-project.org/doc/qt-4.8/qobject.html#no-copy-constructor-or-assignment-operator
|
||
DeckList::DeckList(const DeckList &other)
|
||
: QObject(), name(other.name), comments(other.comments), bannerCard(other.bannerCard),
|
||
lastLoadedTimestamp(other.lastLoadedTimestamp), tags(other.tags), cachedDeckHash(other.cachedDeckHash)
|
||
{
|
||
root = new InnerDecklistNode(other.getRoot());
|
||
|
||
QMapIterator<QString, SideboardPlan *> spIterator(other.getSideboardPlans());
|
||
while (spIterator.hasNext()) {
|
||
spIterator.next();
|
||
sideboardPlans.insert(spIterator.key(), new SideboardPlan(spIterator.key(), spIterator.value()->getMoveList()));
|
||
}
|
||
}
|
||
|
||
DeckList::DeckList(const QString &nativeString)
|
||
{
|
||
root = new InnerDecklistNode;
|
||
loadFromString_Native(nativeString);
|
||
}
|
||
|
||
DeckList::~DeckList()
|
||
{
|
||
delete root;
|
||
|
||
QMapIterator<QString, SideboardPlan *> i(sideboardPlans);
|
||
while (i.hasNext())
|
||
delete i.next().value();
|
||
}
|
||
|
||
QList<MoveCard_ToZone> DeckList::getCurrentSideboardPlan()
|
||
{
|
||
SideboardPlan *current = sideboardPlans.value(QString(), 0);
|
||
if (!current)
|
||
return QList<MoveCard_ToZone>();
|
||
else
|
||
return current->getMoveList();
|
||
}
|
||
|
||
void DeckList::setCurrentSideboardPlan(const QList<MoveCard_ToZone> &plan)
|
||
{
|
||
SideboardPlan *current = sideboardPlans.value(QString(), 0);
|
||
if (!current) {
|
||
current = new SideboardPlan;
|
||
sideboardPlans.insert(QString(), current);
|
||
}
|
||
|
||
current->setMoveList(plan);
|
||
}
|
||
|
||
bool DeckList::readElement(QXmlStreamReader *xml)
|
||
{
|
||
const QString childName = xml->name().toString();
|
||
if (xml->isStartElement()) {
|
||
if (childName == "lastLoadedTimestamp") {
|
||
lastLoadedTimestamp = xml->readElementText();
|
||
} else if (childName == "deckname") {
|
||
name = xml->readElementText();
|
||
} else if (childName == "comments") {
|
||
comments = xml->readElementText();
|
||
} else if (childName == "bannerCard") {
|
||
QString providerId = xml->attributes().value("providerId").toString();
|
||
QString cardName = xml->readElementText();
|
||
bannerCard = {cardName, providerId};
|
||
} else if (childName == "tags") {
|
||
tags.clear(); // Clear existing tags
|
||
while (xml->readNextStartElement()) {
|
||
if (xml->name().toString() == "tag") {
|
||
tags.append(xml->readElementText());
|
||
}
|
||
}
|
||
} else if (childName == "zone") {
|
||
InnerDecklistNode *newZone = getZoneObjFromName(xml->attributes().value("name").toString());
|
||
newZone->readElement(xml);
|
||
} else if (childName == "sideboard_plan") {
|
||
SideboardPlan *newSideboardPlan = new SideboardPlan;
|
||
if (newSideboardPlan->readElement(xml)) {
|
||
sideboardPlans.insert(newSideboardPlan->getName(), newSideboardPlan);
|
||
} else {
|
||
delete newSideboardPlan;
|
||
}
|
||
}
|
||
} else if (xml->isEndElement() && (childName == "cockatrice_deck")) {
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
void DeckList::write(QXmlStreamWriter *xml) const
|
||
{
|
||
xml->writeStartElement("cockatrice_deck");
|
||
xml->writeAttribute("version", "1");
|
||
xml->writeTextElement("lastLoadedTimestamp", lastLoadedTimestamp);
|
||
xml->writeTextElement("deckname", name);
|
||
xml->writeStartElement("bannerCard");
|
||
xml->writeAttribute("providerId", bannerCard.providerId);
|
||
xml->writeCharacters(bannerCard.name);
|
||
xml->writeEndElement();
|
||
xml->writeTextElement("comments", comments);
|
||
|
||
// Write tags
|
||
xml->writeStartElement("tags");
|
||
for (const QString &tag : tags) {
|
||
xml->writeTextElement("tag", tag);
|
||
}
|
||
xml->writeEndElement();
|
||
|
||
// Write zones
|
||
for (int i = 0; i < root->size(); i++) {
|
||
root->at(i)->writeElement(xml);
|
||
}
|
||
|
||
// Write sideboard plans
|
||
QMapIterator<QString, SideboardPlan *> i(sideboardPlans);
|
||
while (i.hasNext()) {
|
||
i.next().value()->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)
|
||
{
|
||
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
|
||
* @return False if the input was empty, true otherwise.
|
||
*/
|
||
bool DeckList::loadFromStream_Plain(QTextStream &in, bool preserveMetadata)
|
||
{
|
||
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\*\)\]]* ?(.+))");
|
||
const QRegularExpression reSplitCard(R"( ?\/\/ ?)");
|
||
const QRegularExpression reBrace(R"( ?[\[\{][^\]\}]*[\]\}] ?)"); // not nested
|
||
const QRegularExpression reRoundBrace(R"(^\([^\)]*\) ?)"); // () are only matched at start of string
|
||
const QRegularExpression reDigitBrace(R"( ?\(\d*\) ?)"); // () are matched if containing digits
|
||
const QRegularExpression reBraceDigit(
|
||
R"( ?\([\dA-Z]+\) *\d+$)"); // () are matched if containing setcode then a number
|
||
const QRegularExpression reDoubleFacedMarker(R"( ?\(Transform\) ?)");
|
||
|
||
// 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]*))");
|
||
|
||
const QHash<QRegularExpression, QString> differences{{QRegularExpression("’"), QString("'")},
|
||
{QRegularExpression("Æ"), QString("Ae")},
|
||
{QRegularExpression("æ"), QString("ae")},
|
||
{QRegularExpression(" ?[|/]+ ?"), QString(" // ")}};
|
||
|
||
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 ¤t = inputs.at(index++);
|
||
if (!current.contains(reEmpty)) {
|
||
match = reComment.match(current);
|
||
name = match.captured();
|
||
break;
|
||
}
|
||
}
|
||
while (index < deckStart) {
|
||
const auto ¤t = inputs.at(index++);
|
||
if (!current.contains(reEmpty)) {
|
||
match = reComment.match(current);
|
||
comments += match.captured() + '\n';
|
||
}
|
||
}
|
||
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);
|
||
}
|
||
|
||
// Handle advanced card types
|
||
if (cardName.contains(reSplitCard)) {
|
||
cardName = cardName.split(reSplitCard).join(" // ");
|
||
}
|
||
|
||
if (cardName.contains(reDoubleFacedMarker)) {
|
||
QStringList faces = cardName.split(reDoubleFacedMarker);
|
||
cardName = faces.first().trimmed();
|
||
}
|
||
|
||
// Remove unnecessary characters
|
||
cardName.remove(reBrace);
|
||
cardName.remove(reRoundBrace); // I'll be entirely honest here, these are split to accommodate just three cards
|
||
cardName.remove(reDigitBrace); // from un-sets that have a word in between round braces at the end
|
||
cardName.remove(reBraceDigit); // very specific format with the set code in () and collectors number after
|
||
|
||
// Normalize names
|
||
for (auto diff = differences.constBegin(); diff != differences.constEnd(); ++diff) {
|
||
cardName.replace(diff.key(), diff.value());
|
||
}
|
||
|
||
// Resolve complete card name, this function does nothing if the name is not found
|
||
cardName = getCompleteCardName(cardName);
|
||
|
||
// Determine the zone (mainboard/sideboard)
|
||
QString zoneName = getCardZoneFromName(cardName, sideboard ? DECK_ZONE_SIDE : DECK_ZONE_MAIN);
|
||
|
||
// make new entry in decklist
|
||
new DecklistCardNode(cardName, amount, getZoneObjFromName(zoneName), -1, setCode, collectorNumber);
|
||
}
|
||
|
||
refreshDeckHash();
|
||
return true;
|
||
}
|
||
|
||
InnerDecklistNode *DeckList::getZoneObjFromName(const QString &zoneName)
|
||
{
|
||
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);
|
||
}
|
||
|
||
bool DeckList::loadFromFile_Plain(QIODevice *device)
|
||
{
|
||
QTextStream in(device);
|
||
return loadFromStream_Plain(in, false);
|
||
}
|
||
|
||
bool DeckList::saveToStream_Plain(QTextStream &stream, bool prefixSideboardCards, bool slashTappedOutSplitCards)
|
||
{
|
||
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)
|
||
{
|
||
QTextStream out(device);
|
||
return saveToStream_Plain(out, prefixSideboardCards, slashTappedOutSplitCards);
|
||
}
|
||
|
||
QString DeckList::writeToString_Plain(bool prefixSideboardCards, bool slashTappedOutSplitCards)
|
||
{
|
||
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)
|
||
{
|
||
root->clearTree();
|
||
if (!preserveMetadata) {
|
||
setName();
|
||
setComments();
|
||
setTags();
|
||
}
|
||
refreshDeckHash();
|
||
}
|
||
|
||
void DeckList::getCardListHelper(InnerDecklistNode *item, QSet<QString> &result)
|
||
{
|
||
for (int i = 0; i < item->size(); ++i) {
|
||
auto *node = dynamic_cast<DecklistCardNode *>(item->at(i));
|
||
|
||
if (node) {
|
||
result.insert(node->getName());
|
||
} else {
|
||
getCardListHelper(dynamic_cast<InnerDecklistNode *>(item->at(i)), result);
|
||
}
|
||
}
|
||
}
|
||
|
||
void DeckList::getCardRefListHelper(InnerDecklistNode *item, QList<CardRef> &result)
|
||
{
|
||
for (int i = 0; i < item->size(); ++i) {
|
||
auto *node = dynamic_cast<DecklistCardNode *>(item->at(i));
|
||
|
||
if (node) {
|
||
result.append(node->toCardRef());
|
||
} else {
|
||
getCardRefListHelper(dynamic_cast<InnerDecklistNode *>(item->at(i)), result);
|
||
}
|
||
}
|
||
}
|
||
|
||
QStringList DeckList::getCardList() const
|
||
{
|
||
QSet<QString> result;
|
||
getCardListHelper(root, result);
|
||
return result.values();
|
||
}
|
||
|
||
QList<CardRef> DeckList::getCardRefList() const
|
||
{
|
||
QList<CardRef> result;
|
||
getCardRefListHelper(root, result);
|
||
return result;
|
||
}
|
||
|
||
int DeckList::getSideboardSize() const
|
||
{
|
||
int size = 0;
|
||
for (int i = 0; i < root->size(); ++i) {
|
||
auto *node = dynamic_cast<InnerDecklistNode *>(root->at(i));
|
||
if (node->getName() != DECK_ZONE_SIDE) {
|
||
continue;
|
||
}
|
||
|
||
for (int j = 0; j < node->size(); j++) {
|
||
auto *card = dynamic_cast<DecklistCardNode *>(node->at(j));
|
||
size += card->getNumber();
|
||
}
|
||
}
|
||
return size;
|
||
}
|
||
|
||
DecklistCardNode *DeckList::addCard(const QString &cardName,
|
||
const QString &zoneName,
|
||
const int position,
|
||
const QString &cardSetName,
|
||
const QString &cardSetCollectorNumber,
|
||
const QString &cardProviderId)
|
||
{
|
||
auto *zoneNode = dynamic_cast<InnerDecklistNode *>(root->findChild(zoneName));
|
||
if (zoneNode == nullptr) {
|
||
zoneNode = new InnerDecklistNode(zoneName, root);
|
||
}
|
||
|
||
auto *node =
|
||
new DecklistCardNode(cardName, 1, zoneNode, position, cardSetName, cardSetCollectorNumber, cardProviderId);
|
||
refreshDeckHash();
|
||
|
||
return node;
|
||
}
|
||
|
||
bool DeckList::deleteNode(AbstractDecklistNode *node, InnerDecklistNode *rootNode)
|
||
{
|
||
if (node == root) {
|
||
return true;
|
||
}
|
||
|
||
bool updateHash = false;
|
||
if (rootNode == nullptr) {
|
||
rootNode = root;
|
||
updateHash = true;
|
||
}
|
||
|
||
int index = rootNode->indexOf(node);
|
||
if (index != -1) {
|
||
delete rootNode->takeAt(index);
|
||
|
||
if (rootNode->empty()) {
|
||
deleteNode(rootNode, rootNode->getParent());
|
||
}
|
||
|
||
if (updateHash) {
|
||
refreshDeckHash();
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
for (int i = 0; i < rootNode->size(); i++) {
|
||
auto *inner = dynamic_cast<InnerDecklistNode *>(rootNode->at(i));
|
||
if (inner) {
|
||
if (deleteNode(node, inner)) {
|
||
if (updateHash) {
|
||
refreshDeckHash();
|
||
}
|
||
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
static QString computeDeckHash(const InnerDecklistNode *root)
|
||
{
|
||
QStringList cardList;
|
||
QSet<QString> hashZones, optionalZones;
|
||
|
||
hashZones << DECK_ZONE_MAIN << DECK_ZONE_SIDE; // Zones in deck to be included in hashing process
|
||
optionalZones << DECK_ZONE_TOKENS; // Optional zones in deck not included in hashing process
|
||
|
||
for (int i = 0; i < root->size(); i++) {
|
||
auto *node = dynamic_cast<InnerDecklistNode *>(root->at(i));
|
||
for (int j = 0; j < node->size(); j++) {
|
||
if (hashZones.contains(node->getName())) // Mainboard or Sideboard
|
||
{
|
||
auto *card = dynamic_cast<DecklistCardNode *>(node->at(j));
|
||
for (int k = 0; k < card->getNumber(); ++k) {
|
||
cardList.append((node->getName() == DECK_ZONE_SIDE ? "SB:" : "") + card->getName().toLower());
|
||
}
|
||
}
|
||
}
|
||
}
|
||
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');
|
||
}
|
||
|
||
/**
|
||
* 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 = computeDeckHash(root);
|
||
return cachedDeckHash;
|
||
}
|
||
|
||
/**
|
||
* Invalidates the cached deckHash and emits the deckHashChanged signal.
|
||
*/
|
||
void DeckList::refreshDeckHash()
|
||
{
|
||
cachedDeckHash = QString();
|
||
emit deckHashChanged();
|
||
}
|
||
|
||
/**
|
||
* Calls a given function on each card in the deck.
|
||
*/
|
||
void DeckList::forEachCard(const std::function<void(InnerDecklistNode *, DecklistCardNode *)> &func)
|
||
{
|
||
// 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);
|
||
}
|
||
}
|
||
} |