Analyze deck on tappedout
Fix small bug in deckstats interface
This commit is contained in:
ctrlaltca 2016-09-02 07:34:56 +02:00 committed by Zach H
parent 680277ad6a
commit 7f28a7e844
10 changed files with 310 additions and 21 deletions

View file

@ -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

View file

@ -3,6 +3,8 @@
#include <QDebug>
#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<InnerDecklistNode *>(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<QString, DecklistCardNode*> cardsByType;
QMap<QString, int> cardTotalByType;
int cardTotal = 0;
for (int j = 0; j < zoneNode->size(); j++) {
DecklistCardNode *card =
dynamic_cast<DecklistCardNode *>(
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 <DecklistCardNode*> cards = cardsByType.values(cardType);
saveToStream_DeckZoneCards(out, zoneNode, cards);
out << "\n";
}
}
void DeckLoader::saveToStream_DeckZoneCards(QTextStream &out, const InnerDecklistNode *zoneNode, QList <DecklistCardNode*> 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";
}
}

View file

@ -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 <DecklistCardNode*> cards);
};
#endif

View file

@ -30,7 +30,7 @@ void DeckStatsInterface::queryFinished(QNetworkReply *reply)
reply->deleteLater();
QRegExp rx("<meta property=\"og:url\" content=\"([^\"]+)\"/>");
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;

View file

@ -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();

View file

@ -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;

View file

@ -0,0 +1,125 @@
#include "tappedout_interface.h"
#include "decklist.h"
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QRegExp>
#include <QMessageBox>
#include <QDesktopServices>
#include <QUrlQuery>
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("<div class=\"alert alert-danger.*<ul>(.*)</ul>");
rx.setMinimal(true);
int found = rx.indexIn(data);
if(found >= 0)
{
QString errors = rx.cap(1);
QRegExp rx2("<li>(.*)</li>");
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);
}

View file

@ -0,0 +1,34 @@
#ifndef TAPPEDOUT_INTERFACE_H
#define TAPPEDOUT_INTERFACE_H
#include "carddatabase.h"
#include "decklist.h"
#include <QObject>
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

View file

@ -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;
}

View file

@ -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(); }