From 7f28a7e8441b9c8361d9d9c3757d8c4964242cfb Mon Sep 17 00:00:00 2001 From: ctrlaltca Date: Fri, 2 Sep 2016 07:34:56 +0200 Subject: [PATCH] Fix #601; Fix #725 (#2126) Analyze deck on tappedout Fix small bug in deckstats interface --- cockatrice/CMakeLists.txt | 1 + cockatrice/src/deck_loader.cpp | 88 +++++++++++++++++ cockatrice/src/deck_loader.h | 8 ++ cockatrice/src/deckstats_interface.cpp | 2 +- cockatrice/src/tab_deck_editor.cpp | 33 +++++-- cockatrice/src/tab_deck_editor.h | 7 +- cockatrice/src/tappedout_interface.cpp | 125 +++++++++++++++++++++++++ cockatrice/src/tappedout_interface.h | 34 +++++++ common/decklist.cpp | 27 ++++-- common/decklist.h | 6 +- 10 files changed, 310 insertions(+), 21 deletions(-) create mode 100644 cockatrice/src/tappedout_interface.cpp create mode 100644 cockatrice/src/tappedout_interface.h diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index d4c7845f..5ab8f99e 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -80,6 +80,7 @@ SET(cockatrice_SOURCES src/tab_logs.cpp src/replay_timeline_widget.cpp src/deckstats_interface.cpp + src/tappedout_interface.cpp src/chatview.cpp src/userlist.cpp src/userinfobox.cpp diff --git a/cockatrice/src/deck_loader.cpp b/cockatrice/src/deck_loader.cpp index 02866c95..9dd50310 100644 --- a/cockatrice/src/deck_loader.cpp +++ b/cockatrice/src/deck_loader.cpp @@ -3,6 +3,8 @@ #include #include "deck_loader.h" #include "decklist.h" +#include "carddatabase.h" +#include "main.h" const QStringList DeckLoader::fileNameFilters = QStringList() << QObject::tr("Common deck formats (*.cod *.dec *.txt *.mwDeck)") @@ -108,3 +110,89 @@ DeckLoader::FileFormat DeckLoader::getFormatFromName(const QString &fileName) } return PlainTextFormat; } + +bool DeckLoader::saveToStream_Plain(QTextStream &out) +{ + saveToStream_DeckHeader(out); + + // loop zones + for (int i = 0; i < getRoot()->size(); i++) { + const InnerDecklistNode *zoneNode = + dynamic_cast(getRoot()->at(i)); + + saveToStream_DeckZone(out, zoneNode); + + // end of zone + out << "\n"; + } + + return true; +} + +void DeckLoader::saveToStream_DeckHeader(QTextStream &out) +{ + if(!getName().isEmpty()) + out << "// " << getName() << "\n\n"; + if(!getComments().isEmpty()) + { + QStringList commentRows = getComments().split(QRegExp("\n|\r\n|\r")); + foreach(QString row, commentRows) + out << "// " << row << "\n"; + out << "\n"; + } +} + +void DeckLoader::saveToStream_DeckZone(QTextStream &out, const InnerDecklistNode *zoneNode) +{ + // group cards by card type and count the subtotals + QMultiMap cardsByType; + QMap cardTotalByType; + int cardTotal = 0; + + for (int j = 0; j < zoneNode->size(); j++) { + DecklistCardNode *card = + dynamic_cast( + zoneNode->at(j) + ); + + CardInfo *info = db->getCard(card->getName()); + QString cardType = info ? info->getMainCardType() : "unknown"; + + cardsByType.insert(cardType, card); + + if(cardTotalByType.contains(cardType)) + cardTotalByType[cardType] += card->getNumber(); + else + cardTotalByType[cardType] = card->getNumber(); + + cardTotal += card->getNumber(); + } + + out << "// " << cardTotal << " " << zoneNode->getVisibleName() << "\n"; + + // print cards to stream + foreach(QString cardType, cardsByType.uniqueKeys()) + { + + out << "// " << cardTotalByType[cardType] << " " << cardType << "\n"; + QList cards = cardsByType.values(cardType); + + saveToStream_DeckZoneCards(out, zoneNode, cards); + + out << "\n"; + } +} + +void DeckLoader::saveToStream_DeckZoneCards(QTextStream &out, const InnerDecklistNode *zoneNode, QList cards) +{ + // QMultiMap sorts values in reverse order + for(int i = cards.size() - 1; i >= 0; --i) + { + DecklistCardNode* card = cards[i]; + + if (zoneNode->getName() == "side") + out << "SB: "; + + out << card->getNumber() << " " << card->getName() << "\n"; + } +} diff --git a/cockatrice/src/deck_loader.h b/cockatrice/src/deck_loader.h index 70c481d1..bd5d6ed3 100644 --- a/cockatrice/src/deck_loader.h +++ b/cockatrice/src/deck_loader.h @@ -28,6 +28,14 @@ public: bool loadFromFile(const QString &fileName, FileFormat fmt); bool loadFromRemote(const QString &nativeString, int remoteDeckId); bool saveToFile(const QString &fileName, FileFormat fmt); + + // overload + bool saveToStream_Plain(QTextStream &out); + +protected: + void saveToStream_DeckHeader(QTextStream &out); + void saveToStream_DeckZone(QTextStream &out, const InnerDecklistNode *zoneNode); + void saveToStream_DeckZoneCards(QTextStream &out, const InnerDecklistNode *zoneNode, QList cards); }; #endif diff --git a/cockatrice/src/deckstats_interface.cpp b/cockatrice/src/deckstats_interface.cpp index a5884669..be24a257 100644 --- a/cockatrice/src/deckstats_interface.cpp +++ b/cockatrice/src/deckstats_interface.cpp @@ -30,7 +30,7 @@ void DeckStatsInterface::queryFinished(QNetworkReply *reply) reply->deleteLater(); QRegExp rx(""); - if (!rx.indexIn(data)) { + if (-1 == rx.indexIn(data)) { QMessageBox::critical(0, tr("Error"), tr("The reply from the server could not be parsed.")); deleteLater(); return; diff --git a/cockatrice/src/tab_deck_editor.cpp b/cockatrice/src/tab_deck_editor.cpp index 541bb18d..740c6a65 100644 --- a/cockatrice/src/tab_deck_editor.cpp +++ b/cockatrice/src/tab_deck_editor.cpp @@ -34,6 +34,7 @@ #include "priceupdater.h" #include "tab_supervisor.h" #include "deckstats_interface.h" +#include "tappedout_interface.h" #include "abstractclient.h" #include "pending_command.h" #include "pb/response.pb.h" @@ -226,8 +227,15 @@ void TabDeckEditor::createMenus() aPrintDeck = new QAction(QString(), this); connect(aPrintDeck, SIGNAL(triggered()), this, SLOT(actPrintDeck())); - aAnalyzeDeck = new QAction(QString(), this); - connect(aAnalyzeDeck, SIGNAL(triggered()), this, SLOT(actAnalyzeDeck())); + aAnalyzeDeckDeckstats = new QAction(QString(), this); + connect(aAnalyzeDeckDeckstats, SIGNAL(triggered()), this, SLOT(actAnalyzeDeckDeckstats())); + + aAnalyzeDeckTappedout = new QAction(QString(), this); + connect(aAnalyzeDeckTappedout, SIGNAL(triggered()), this, SLOT(actAnalyzeDeckTappedout())); + + analyzeDeckMenu = new QMenu(this); + analyzeDeckMenu->addAction(aAnalyzeDeckDeckstats); + analyzeDeckMenu->addAction(aAnalyzeDeckTappedout); aClose = new QAction(QString(), this); connect(aClose, SIGNAL(triggered()), this, SLOT(closeRequest())); @@ -250,7 +258,7 @@ void TabDeckEditor::createMenus() deckMenu->addAction(aSaveDeckToClipboard); deckMenu->addSeparator(); deckMenu->addAction(aPrintDeck); - deckMenu->addAction(aAnalyzeDeck); + deckMenu->addMenu(analyzeDeckMenu); deckMenu->addSeparator(); deckMenu->addAction(aClearFilterOne); deckMenu->addAction(aClearFilterAll); @@ -444,7 +452,7 @@ void TabDeckEditor::refreshShortcuts() aSaveDeckAs->setShortcuts(settingsCache->shortcuts().getShortcut("TabDeckEditor/aSaveDeckAs")); aLoadDeckFromClipboard->setShortcuts(settingsCache->shortcuts().getShortcut("TabDeckEditor/aLoadDeckFromClipboard")); aPrintDeck->setShortcuts(settingsCache->shortcuts().getShortcut("TabDeckEditor/aPrintDeck")); - aAnalyzeDeck->setShortcuts(settingsCache->shortcuts().getShortcut("TabDeckEditor/aAnalyzeDeck")); + aAnalyzeDeckDeckstats->setShortcuts(settingsCache->shortcuts().getShortcut("TabDeckEditor/aAnalyzeDeck")); aClose->setShortcuts(settingsCache->shortcuts().getShortcut("TabDeckEditor/aClose")); aResetLayout->setShortcuts(settingsCache->shortcuts().getShortcut("TabDeckEditor/aResetLayout")); aClearFilterAll->setShortcuts(settingsCache->shortcuts().getShortcut("TabDeckEditor/aClearFilterAll")); @@ -536,7 +544,11 @@ void TabDeckEditor::retranslateUi() aLoadDeckFromClipboard->setText(tr("Load deck from cl&ipboard...")); aSaveDeckToClipboard->setText(tr("Save deck to clip&board")); aPrintDeck->setText(tr("&Print deck...")); - aAnalyzeDeck->setText(tr("&Analyze deck on deckstats.net")); + + analyzeDeckMenu->setTitle(tr("&Analyze deck online")); + aAnalyzeDeckDeckstats->setText(tr("deckstats.net")); + aAnalyzeDeckTappedout->setText(tr("tappedout.net")); + aClose->setText(tr("&Close")); aAddCard->setText(tr("Add card to &maindeck")); @@ -751,7 +763,7 @@ void TabDeckEditor::actPrintDeck() dlg->exec(); } -void TabDeckEditor::actAnalyzeDeck() +void TabDeckEditor::actAnalyzeDeckDeckstats() { DeckStatsInterface *interface = new DeckStatsInterface( *databaseModel->getDatabase(), @@ -760,6 +772,15 @@ void TabDeckEditor::actAnalyzeDeck() interface->analyzeDeck(deckModel->getDeckList()); } +void TabDeckEditor::actAnalyzeDeckTappedout() +{ + TappedOutInterface *interface = new TappedOutInterface( + *databaseModel->getDatabase(), + this + ); // it deletes itself when done + interface->analyzeDeck(deckModel->getDeckList()); +} + void TabDeckEditor::actClearFilterAll() { databaseDisplayModel->clearFilterAll(); diff --git a/cockatrice/src/tab_deck_editor.h b/cockatrice/src/tab_deck_editor.h index 1ce79695..5bd9d5bc 100644 --- a/cockatrice/src/tab_deck_editor.h +++ b/cockatrice/src/tab_deck_editor.h @@ -52,7 +52,8 @@ class TabDeckEditor : public Tab { void actLoadDeckFromClipboard(); void actSaveDeckToClipboard(); void actPrintDeck(); - void actAnalyzeDeck(); + void actAnalyzeDeckDeckstats(); + void actAnalyzeDeckTappedout(); void actClearFilterAll(); void actClearFilterOne(); @@ -112,8 +113,8 @@ private: QTreeView *filterView; QWidget *filterBox; - QMenu *deckMenu, *viewMenu, *cardInfoDockMenu, *deckDockMenu, *filterDockMenu; - QAction *aNewDeck, *aLoadDeck, *aSaveDeck, *aSaveDeckAs, *aLoadDeckFromClipboard, *aSaveDeckToClipboard, *aPrintDeck, *aAnalyzeDeck, *aClose; + QMenu *deckMenu, *viewMenu, *cardInfoDockMenu, *deckDockMenu, *filterDockMenu, *analyzeDeckMenu; + QAction *aNewDeck, *aLoadDeck, *aSaveDeck, *aSaveDeckAs, *aLoadDeckFromClipboard, *aSaveDeckToClipboard, *aPrintDeck, *aAnalyzeDeckDeckstats, *aAnalyzeDeckTappedout, *aClose; QAction *aClearFilterAll, *aClearFilterOne; QAction *aAddCard, *aAddCardToSideboard, *aRemoveCard, *aIncrement, *aDecrement;// *aUpdatePrices; QAction *aResetLayout; diff --git a/cockatrice/src/tappedout_interface.cpp b/cockatrice/src/tappedout_interface.cpp new file mode 100644 index 00000000..a06b56a8 --- /dev/null +++ b/cockatrice/src/tappedout_interface.cpp @@ -0,0 +1,125 @@ +#include "tappedout_interface.h" +#include "decklist.h" +#include +#include +#include +#include +#include +#include +#include + +TappedOutInterface::TappedOutInterface( + CardDatabase &_cardDatabase, + QObject *parent +) : QObject(parent), cardDatabase(_cardDatabase) +{ + manager = new QNetworkAccessManager(this); + connect(manager, SIGNAL(finished(QNetworkReply *)), this, SLOT(queryFinished(QNetworkReply *))); +} + +void TappedOutInterface::queryFinished(QNetworkReply *reply) +{ + if (reply->error() != QNetworkReply::NoError) { + QMessageBox::critical(0, tr("Error"), reply->errorString()); + reply->deleteLater(); + deleteLater(); + return; + } + + int httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if(reply->hasRawHeader("Location")) + { + /* + * If the reply contains a "Location" header, a relative URL to the deck on TappedOut + * can be extracted from the header. The http status is a 302 "redirect". + */ + QString deckUrl = reply->rawHeader("Location"); + qDebug() << "Tappedout: good reply, http status" << httpStatus << "location" << deckUrl; + QDesktopServices::openUrl("http://tappedout.net" + deckUrl); + } else { + /* + * Otherwise, the deck has not been parsed correctly. Error messages can be extracted + * from the html. Css pseudo selector for errors: $("div.alert-danger > ul > li") + */ + QString data(reply->readAll()); + QString errorMessage = tr("Unable to analyze the deck."); + + QRegExp rx("
(.*)"); + rx.setMinimal(true); + int found = rx.indexIn(data); + if(found >= 0) + { + QString errors = rx.cap(1); + QRegExp rx2("
  • (.*)
  • "); + rx2.setMinimal(true); + + found = rx2.indexIn(errors); + int captures = rx2.captureCount(); + for(int i = 1; i <= captures; i++) + { + errorMessage += QString("\n") + rx2.cap(i).remove(QRegExp("<[^>]*>")).simplified(); + } + + } + + qDebug() << "Tappedout: bad reply, http status" << httpStatus << "size" << data.size() << "message" << errorMessage; + + QMessageBox::critical(0, tr("Error"), errorMessage); + } + + reply->deleteLater(); + deleteLater(); +} + +void TappedOutInterface::getAnalyzeRequestData(DeckList *deck, QByteArray *data) +{ + DeckList mainboard, sideboard; + copyDeckSplitMainAndSide(*deck, mainboard, sideboard); + + QUrl params; + QUrlQuery urlQuery; + urlQuery.addQueryItem("name", deck->getName()); + urlQuery.addQueryItem("mainboard", mainboard.writeToString_Plain(false)); + urlQuery.addQueryItem("sideboard", sideboard.writeToString_Plain(false)); + params.setQuery(urlQuery); + data->append(params.query(QUrl::EncodeReserved)); +} + +void TappedOutInterface::analyzeDeck(DeckList *deck) +{ + QByteArray data; + getAnalyzeRequestData(deck, &data); + + QNetworkRequest request(QUrl("http://tappedout.net/mtg-decks/paste/")); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + + manager->post(request, data); +} + +struct CopyMainOrSide { + CardDatabase &cardDatabase; + DeckList &mainboard, &sideboard; + + CopyMainOrSide(CardDatabase &_cardDatabase, DeckList &_mainboard, DeckList &_sideboard) + : cardDatabase(_cardDatabase), mainboard(_mainboard), sideboard(_sideboard) {}; + + void operator()(const InnerDecklistNode *node, const DecklistCardNode *card) const + { + CardInfo * dbCard = cardDatabase.getCard(card->getName()); + if (!dbCard || dbCard->getIsToken()) + return; + + DecklistCardNode *addedCard; + if(node->getName() == "side") + addedCard = sideboard.addCard(card->getName(), node->getName()); + else + addedCard = mainboard.addCard(card->getName(), node->getName()); + addedCard->setNumber(card->getNumber()); + } +}; + +void TappedOutInterface::copyDeckSplitMainAndSide(const DeckList &source, DeckList &mainboard, DeckList &sideboard) +{ + CopyMainOrSide copyMainOrSide(cardDatabase, mainboard, sideboard); + source.forEachCard(copyMainOrSide); +} diff --git a/cockatrice/src/tappedout_interface.h b/cockatrice/src/tappedout_interface.h new file mode 100644 index 00000000..143a6ba6 --- /dev/null +++ b/cockatrice/src/tappedout_interface.h @@ -0,0 +1,34 @@ +#ifndef TAPPEDOUT_INTERFACE_H +#define TAPPEDOUT_INTERFACE_H + +#include "carddatabase.h" +#include "decklist.h" +#include + +class QByteArray; +class QNetworkAccessManager; +class QNetworkReply; +class DeckList; + +/** + * TappedOutInterface exists in order to support the "Analyze on TappedOut" feature. + * An http POST request is sent and the result is retrieved from the reply. Parsing + * logic is implemented in TappedOutInterface::queryFinished(). + */ + +class TappedOutInterface : public QObject { + Q_OBJECT +private: + QNetworkAccessManager *manager; + + CardDatabase &cardDatabase; + void copyDeckSplitMainAndSide(const DeckList &source, DeckList& mainboard, DeckList& sideboard); +private slots: + void queryFinished(QNetworkReply *reply); + void getAnalyzeRequestData(DeckList *deck, QByteArray *data); +public: + TappedOutInterface(CardDatabase &_cardDatabase, QObject *parent = 0); + void analyzeDeck(DeckList *deck); +}; + +#endif diff --git a/common/decklist.cpp b/common/decklist.cpp index 478ffb28..35da7266 100644 --- a/common/decklist.cpp +++ b/common/decklist.cpp @@ -514,10 +514,20 @@ bool DeckList::loadFromStream_Plain(QTextStream &in) bool inSideboard = false; int okRows = 0; + bool titleFound = false; while (!in.atEnd()) { QString line = in.readLine().simplified(); if (line.startsWith("//")) + { + if(!titleFound) + { + name = line.mid(2).trimmed(); + titleFound = true; + } else if(okRows == 0) { + comments += line.mid(2).trimmed() + "\n"; + } continue; + } InnerDecklistNode *zone; if (line.startsWith("Sideboard", Qt::CaseInsensitive)) { @@ -600,14 +610,15 @@ bool DeckList::loadFromFile_Plain(QIODevice *device) struct WriteToStream { QTextStream &stream; + bool prefixSideboardCards; - WriteToStream(QTextStream &_stream) : stream(_stream) {} + WriteToStream(QTextStream &_stream, bool _prefixSideboardCards) : stream(_stream), prefixSideboardCards(_prefixSideboardCards) {} void operator()( const InnerDecklistNode *node, const DecklistCardNode *card ) { - if (node->getName() == "side") { + if (prefixSideboardCards && node->getName() == "side") { stream << "SB: "; } stream << QString("%1 %2\n").arg( @@ -618,24 +629,24 @@ struct WriteToStream { } }; -bool DeckList::saveToStream_Plain(QTextStream &out) +bool DeckList::saveToStream_Plain(QTextStream &out, bool prefixSideboardCards) { - WriteToStream writeToStream(out); + WriteToStream writeToStream(out, prefixSideboardCards); forEachCard(writeToStream); return true; } -bool DeckList::saveToFile_Plain(QIODevice *device) +bool DeckList::saveToFile_Plain(QIODevice *device, bool prefixSideboardCards) { QTextStream out(device); - return saveToStream_Plain(out); + return saveToStream_Plain(out, prefixSideboardCards); } -QString DeckList::writeToString_Plain() +QString DeckList::writeToString_Plain(bool prefixSideboardCards) { QString result; QTextStream out(&result); - saveToStream_Plain(out); + saveToStream_Plain(out, prefixSideboardCards); return result; } diff --git a/common/decklist.h b/common/decklist.h index b3140edb..d9e0e220 100644 --- a/common/decklist.h +++ b/common/decklist.h @@ -153,9 +153,9 @@ public: bool saveToFile_Native(QIODevice *device); bool loadFromStream_Plain(QTextStream &stream); bool loadFromFile_Plain(QIODevice *device); - bool saveToStream_Plain(QTextStream &stream); - bool saveToFile_Plain(QIODevice *device); - QString writeToString_Plain(); + bool saveToStream_Plain(QTextStream &stream, bool prefixSideboardCards); + bool saveToFile_Plain(QIODevice *device, bool prefixSideboardCards=true); + QString writeToString_Plain(bool prefixSideboardCards=true); void cleanList(); bool isEmpty() const { return root->isEmpty() && name.isEmpty() && comments.isEmpty() && sideboardPlans.isEmpty(); }