#include "deck_list.h" #include "abstract_deck_list_node.h" #include "deck_list_card_node.h" #include "inner_deck_list_node.h" #include #include #include #include #include #include #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 &_moveList) : name(_name), moveList(_moveList) { } void SideboardPlan::setMoveList(const QList &_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 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 i(sideboardPlans); while (i.hasNext()) delete i.next().value(); } QList DeckList::getCurrentSideboardPlan() { SideboardPlan *current = sideboardPlans.value(QString(), 0); if (!current) return QList(); else return current->getMoveList(); } void DeckList::setCurrentSideboardPlan(const QList &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 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 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(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 &result) { for (int i = 0; i < item->size(); ++i) { auto *node = dynamic_cast(item->at(i)); if (node) { result.insert(node->getName()); } else { getCardListHelper(dynamic_cast(item->at(i)), result); } } } void DeckList::getCardRefListHelper(InnerDecklistNode *item, QList &result) { for (int i = 0; i < item->size(); ++i) { auto *node = dynamic_cast(item->at(i)); if (node) { result.append(node->toCardRef()); } else { getCardRefListHelper(dynamic_cast(item->at(i)), result); } } } QStringList DeckList::getCardList() const { QSet result; getCardListHelper(root, result); return result.values(); } QList DeckList::getCardRefList() const { QList 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(root->at(i)); if (node->getName() != DECK_ZONE_SIDE) { continue; } for (int j = 0; j < node->size(); j++) { auto *card = dynamic_cast(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(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(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 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(root->at(i)); for (int j = 0; j < node->size(); j++) { if (hashZones.contains(node->getName())) // Mainboard or Sideboard { auto *card = dynamic_cast(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 &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(root->at(i)); for (int j = 0; j < node->size(); j++) { DecklistCardNode *card = dynamic_cast(node->at(j)); func(node, card); } } }