#include "decklist.h" #include #include #include #include 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(); } AbstractDecklistNode::AbstractDecklistNode(InnerDecklistNode *_parent) : parent(_parent), sortMethod(Default) { if (parent) { parent->append(this); } } int AbstractDecklistNode::depth() const { if (parent) { return parent->depth() + 1; } else { return 0; } } InnerDecklistNode::InnerDecklistNode(InnerDecklistNode *other, InnerDecklistNode *_parent) : AbstractDecklistNode(_parent), name(other->getName()) { for (int i = 0; i < other->size(); ++i) { auto *inner = dynamic_cast(other->at(i)); if (inner) { new InnerDecklistNode(inner, this); } else { new DecklistCardNode(dynamic_cast(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(); } DecklistCardNode::DecklistCardNode(DecklistCardNode *other, InnerDecklistNode *_parent) : AbstractDecklistCardNode(_parent), name(other->getName()), number(other->getNumber()) { } AbstractDecklistNode *InnerDecklistNode::findChild(const QString &name) { for (int i = 0; i < size(); i++) { if (at(i)->getName() == name) { return at(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(at(i)); if (node) { result += node->recursiveCount(countTotalCards); } else if (countTotalCards) { result += dynamic_cast(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(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(other); if (other2) { return (getName() > other2->getName()); } else { return false; } } 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(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(other); if (other2) { return (getName() > other2->getName()); } else { return true; } } class InnerDecklistNode::compareFunctor { private: Qt::SortOrder order; public: explicit compareFunctor(Qt::SortOrder _order) : order(_order) { } inline bool operator()(QPair a, QPair b) const { return (order == Qt::AscendingOrder) ^ (a.second->compare(b.second)); } }; bool InnerDecklistNode::readElement(QXmlStreamReader *xml) { while (!xml->atEnd()) { xml->readNext(); const QString childName = xml->name().toString(); if (xml->isStartElement()) { if (childName == "zone") { InnerDecklistNode *newZone = new InnerDecklistNode(xml->attributes().value("name").toString(), this); newZone->readElement(xml); } else if (childName == "card") { DecklistCardNode *newCard = new DecklistCardNode(xml->attributes().value("name").toString(), xml->attributes().value("number").toString().toInt(), this); 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 } bool AbstractDecklistCardNode::readElement(QXmlStreamReader *xml) { while (!xml->atEnd()) { xml->readNext(); if (xml->isEndElement() && xml->name() == "card") return false; } return true; } void AbstractDecklistCardNode::writeElement(QXmlStreamWriter *xml) { xml->writeEmptyElement("card"); xml->writeAttribute("number", QString::number(getNumber())); xml->writeAttribute("name", getName()); } QVector> InnerDecklistNode::sort(Qt::SortOrder order) { QVector> result(size()); // Initialize temporary list with contents of current list QVector> tempList(size()); for (int i = size() - 1; i >= 0; --i) { tempList[i].first = i; tempList[i].second = at(i); } // Sort temporary list compareFunctor cmp(order); qSort(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; } DeckList::DeckList() { root = new InnerDecklistNode; } // TODO: http://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), deckHash(other.deckHash) { 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())); } updateDeckHash(); } 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 == "deckname") name = xml->readElementText(); else if (childName == "comments") comments = 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) { xml->writeStartElement("cockatrice_deck"); xml->writeAttribute("version", "1"); xml->writeTextElement("deckname", name); xml->writeTextElement("comments", comments); for (int i = 0; i < root->size(); i++) root->at(i)->writeElement(xml); QMapIterator i(sideboardPlans); while (i.hasNext()) i.next().value()->write(xml); xml->writeEndElement(); } 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() != "cockatrice_deck") return false; while (!xml->atEnd()) { xml->readNext(); if (!readElement(xml)) break; } } } updateDeckHash(); 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() { 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; } bool DeckList::loadFromStream_Plain(QTextStream &in) { cleanList(); QVector inputs; // QTextStream -> QVector bool priorEntryIsBlank = true, isAtBeginning = true; int blankLines = 0; while (!in.atEnd()) { QString line = in.readLine().simplified().toLower(); /* * Removes all blank lines at start of inputs * Ex: ("", "", "", "Card1", "Card2") => ("Card1", "Card2") * * This will also concise multiple blank lines in a row to just one blank * Ex: ("Card1", "Card2", "", "", "", "Card3") => ("Card1", "Card2", "", "Card3") */ if (line.isEmpty()) { if (priorEntryIsBlank || isAtBeginning) { continue; } priorEntryIsBlank = true; blankLines++; } else { isAtBeginning = false; priorEntryIsBlank = false; } inputs.push_back(line); } /* * Removes blank line at end of inputs (if applicable) * Ex: ("Card1", "Card2", "") => ("Card1", "Card2") * NOTE: Any duplicates were taken care of above, so there can be * at most one blank line at the very end */ if (!inputs.empty() && inputs.last().isEmpty()) { blankLines--; inputs.erase(inputs.end() - 1); } // If "Sideboard" line appears in inputs, then blank lines mean nothing if (inputs.contains("sideboard")) { blankLines = 2; } bool inSideboard = false, titleFound = false, isSideboard; int okRows = 0; foreach (QString line, inputs) { // This is a comment line, ignore it if (line.startsWith("//")) { if (!titleFound) // Set the title to the first comment { name = line.mid(2).trimmed(); titleFound = true; } else if (okRows == 0) // We haven't processed any cards yet { comments += line.mid(2).trimmed() + "\n"; } continue; } // If we have a blank line and it's the _ONLY_ blank line in the paste // Then we assume it means to start the sideboard section of the paste. // If we have the word "Sideboard" appear on any line, then that will // also indicate the start of the sideboard. if ((line.isEmpty() && blankLines == 1) || line.startsWith("sideboard")) { inSideboard = true; continue; // The line isn't actually a card } isSideboard = inSideboard; if (line.startsWith("sb:")) { line = line.mid(3).trimmed(); isSideboard = true; } if (line.trimmed().isEmpty()) { continue; // The line was " " instead of "\n" } // Filter out MWS edition symbols and basic land extras QRegExp rx("\\[.*\\]\\s?"); line.remove(rx); rx.setPattern("\\s?\\(.*\\)"); line.remove(rx); // Filter out post card name editions rx.setPattern("\\|.*$"); line.remove(rx); // If the user inputs "Quicksilver Elemental" then it will cut it off // 1x Squishy Treaker int i = line.indexOf(' '); int cardNameStart = i + 1; if (i > 0) { // If the count ends with an 'x', ignore it. For example, // "4x Storm Crow" will count 4 correctly. if (line.at(i - 1) == 'x') { i--; } else if (!line.at(i - 1).isDigit()) { // If the user inputs "Quicksilver Elemental" then it will work as 1x of that card cardNameStart = 0; } } bool ok; int number = line.left(i).toInt(&ok); if (!ok) { number = 1; // If input is "cardName" assume it's "1x cardName" } QString cardName = line.mid(cardNameStart); // Common differences between Cockatrice's card names // and what's commonly used in decklists rx.setPattern("’"); cardName.replace(rx, "'"); rx.setPattern("Æ"); cardName.replace(rx, "Ae"); rx.setPattern("\\s*[|/]{1,2}\\s*"); cardName.replace(rx, " // "); // Replace only if the ampersand is preceded by a non-capital letter, // as would happen with acronyms. So 'Fire & Ice' is replaced but not // 'R&D' or 'R & D'. // Qt regexes don't support lookbehind so we capture and replace instead. rx.setPattern("([^A-Z])\\s*&\\s*"); if (rx.indexIn(cardName) != -1) { cardName.replace(rx, QString("%1 // ").arg(rx.cap(1))); } // We need to get the name of the card from the database, // but we can't do that until we get the "real" name // (name stored in database for the card) // and establish a card info that is of the card, then it's // a simple getting the _real_ name of the card // (i.e. "STOrm, CrOW" => "Storm Crow") cardName = getCompleteCardName(cardName); // Look for the correct card zone of where to place the new card QString zoneName = getCardZoneFromName(cardName, isSideboard ? DECK_ZONE_SIDE : DECK_ZONE_MAIN); okRows++; new DecklistCardNode(cardName, number, getZoneObjFromName(zoneName)); } updateDeckHash(); return (okRows > 0); } 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); } struct WriteToStream { QTextStream &stream; bool prefixSideboardCards; bool slashTappedOutSplitCards; WriteToStream(QTextStream &_stream, bool _prefixSideboardCards, bool _slashTappedOutSplitCards) : stream(_stream), prefixSideboardCards(_prefixSideboardCards), slashTappedOutSplitCards(_slashTappedOutSplitCards) { } void operator()(const InnerDecklistNode *node, const DecklistCardNode *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("//", "/")); } } }; bool DeckList::saveToStream_Plain(QTextStream &out, bool prefixSideboardCards, bool slashTappedOutSplitCards) { WriteToStream writeToStream(out, prefixSideboardCards, slashTappedOutSplitCards); 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; } void DeckList::cleanList() { root->clearTree(); setName(); setComments(); deckHash = QString(); emit deckHashChanged(); } void DeckList::getCardListHelper(InnerDecklistNode *item, QSet &result) const { 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); } } } QStringList DeckList::getCardList() const { QSet result; getCardListHelper(root, result); return result.toList(); } 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) { auto *zoneNode = dynamic_cast(root->findChild(zoneName)); if (zoneNode == nullptr) { zoneNode = new InnerDecklistNode(zoneName, root); } auto *node = new DecklistCardNode(cardName, 1, zoneNode); updateDeckHash(); 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) { updateDeckHash(); } 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) { updateDeckHash(); } return true; } } } return false; } void DeckList::updateDeckHash() { QStringList cardList; bool isValidDeckList = true; 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()); } } else if (!optionalZones.contains(node->getName())) // Not a valid zone -> cheater? { isValidDeckList = false; // Deck is invalid } } } 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]; deckHash = (isValidDeckList) ? QString::number(number, 32).rightJustified(8, '0') : "INVALID"; emit deckHashChanged(); }