Add the option to load decklists from Archidekt, Deckstats, Moxfield, TappedOut in deck editor and lobby (#6030)

* Add the option to load decklists from Archidekt, Deckstats, Moxfield, TappedOut in deck editor and lobby.

Took 3 hours 34 minutes

Took 9 seconds


Took 12 seconds

* Properly set quantities.

Took 11 minutes

* Warnings.

Took 5 minutes

* Static regexes.

Co-authored-by: RickyRister <42636155+RickyRister@users.noreply.github.com>

* Category loggings and better warnings.

Took 18 minutes


Took 42 seconds

* use loadFromStream_Plain instead of manually adding CardNodes to the DeckList.

Took 30 minutes

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Co-authored-by: RickyRister <42636155+RickyRister@users.noreply.github.com>
This commit is contained in:
BruebachL 2025-07-15 05:12:25 +02:00 committed by GitHub
parent 83b90d472f
commit e05dad4267
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 444 additions and 3 deletions

View file

@ -0,0 +1,61 @@
#include "deck_link_to_api_transformer.h"
#include <QRegularExpression>
namespace DeckLinkToApiTransformer
{
static const QString TAPPEDOUT_BASE = "https://tappedout.net/mtg-decks/";
static const QString TAPPEDOUT_SUFFIX = "/?fmt=txt";
static const QString ARCHIDEKT_BASE = "https://archidekt.com/api/decks/";
static const QString ARCHIDEKT_SUFFIX = "/?format=json";
static const QString MOXFIELD_BASE = "https://api.moxfield.com/v2/decks/all/";
static const QString MOXFIELD_SUFFIX = "/";
static const QString DECKSTATS_SUFFIX = "?include_comments=1&export_mtgarena=1";
bool parseDeckUrl(const QString &url, ParsedDeckInfo &outInfo)
{
static QRegularExpression rxTappedOut("tappedout\\.net/(?:mtg-decks/)?([^/?#]+)");
static QRegularExpression rxArchidekt("archidekt\\.com/decks/(\\d+)");
static QRegularExpression rxMoxfield("moxfield\\.com/decks/([a-zA-Z0-9_-]+)");
static QRegularExpression rxDeckstats("deckstats\\.net/decks/(\\d+/[a-zA-Z0-9_-]+)");
QRegularExpressionMatch match;
if ((match = rxTappedOut.match(url)).hasMatch()) {
QString slug = match.captured(1);
outInfo = ParsedDeckInfo{.baseUrl = TAPPEDOUT_BASE,
.deckID = slug,
.fullUrl = TAPPEDOUT_BASE + slug + TAPPEDOUT_SUFFIX,
.provider = DeckProvider::TappedOut};
return true;
} else if ((match = rxArchidekt.match(url)).hasMatch()) {
QString deckID = match.captured(1);
outInfo = ParsedDeckInfo{.baseUrl = ARCHIDEKT_BASE,
.deckID = deckID,
.fullUrl = ARCHIDEKT_BASE + deckID + ARCHIDEKT_SUFFIX,
.provider = DeckProvider::Archidekt};
return true;
} else if ((match = rxMoxfield.match(url)).hasMatch()) {
QString deckID = match.captured(1);
outInfo = ParsedDeckInfo{.baseUrl = MOXFIELD_BASE,
.deckID = deckID,
.fullUrl = MOXFIELD_BASE + deckID + MOXFIELD_SUFFIX,
.provider = DeckProvider::Moxfield};
return true;
} else if ((match = rxDeckstats.match(url)).hasMatch()) {
QString deckPath = match.captured(1);
outInfo = ParsedDeckInfo{.baseUrl = "https://deckstats.net/decks/",
.deckID = deckPath,
.fullUrl = "https://deckstats.net/decks/" + deckPath + DECKSTATS_SUFFIX,
.provider = DeckProvider::Deckstats};
return true;
}
return false;
}
} // namespace DeckLinkToApiTransformer

View file

@ -0,0 +1,31 @@
#ifndef DECK_LINK_TO_API_TRANSFORMER_H
#define DECK_LINK_TO_API_TRANSFORMER_H
#include <QString>
enum class DeckProvider
{
TappedOut,
Archidekt,
Moxfield,
Deckstats,
Unknown
};
struct ParsedDeckInfo
{
QString baseUrl;
QString deckID;
QString fullUrl;
DeckProvider provider;
};
namespace DeckLinkToApiTransformer
{
// Returns true if the input URL is recognized and fills outInfo.
bool parseDeckUrl(const QString &url, ParsedDeckInfo &outInfo);
} // namespace DeckLinkToApiTransformer
#endif // DECK_LINK_TO_API_TRANSFORMER_H

View file

@ -0,0 +1,112 @@
#ifndef INTERFACE_JSON_DECK_PARSER_H
#define INTERFACE_JSON_DECK_PARSER_H
#include "../../../deck/deck_loader.h"
#include <QJsonArray>
#include <QJsonObject>
class IJsonDeckParser
{
public:
virtual ~IJsonDeckParser() = default;
virtual DeckLoader *parse(const QJsonObject &obj) = 0;
};
class ArchidektJsonParser : public IJsonDeckParser
{
public:
DeckLoader *parse(const QJsonObject &obj) override
{
DeckLoader *list = new DeckLoader();
QString deckName = obj.value("name").toString();
QString deckDescription = obj.value("description").toString();
list->setName(deckName);
list->setComments(deckDescription);
QString outputText;
QTextStream outStream(&outputText);
for (auto entry : obj.value("cards").toArray()) {
auto quantity = entry.toObject().value("quantity").toInt();
auto card = entry.toObject().value("card").toObject();
auto oracleCard = card.value("oracleCard").toObject();
QString cardName = oracleCard.value("name").toString();
QString setName = card.value("edition").toObject().value("editioncode").toString().toUpper();
QString collectorNumber = card.value("collectorNumber").toString();
outStream << quantity << ' ' << cardName << " (" << setName << ") " << collectorNumber << '\n';
}
list->loadFromStream_Plain(outStream, false);
list->resolveSetNameAndNumberToProviderID();
return list;
}
};
class MoxfieldJsonParser : public IJsonDeckParser
{
public:
DeckLoader *parse(const QJsonObject &obj) override
{
DeckLoader *list = new DeckLoader();
QString deckName = obj.value("name").toString();
QString deckDescription = obj.value("description").toString();
list->setName(deckName);
list->setComments(deckDescription);
QString outputText;
QTextStream outStream(&outputText);
for (auto entry : obj.value("mainboard").toObject()) {
auto quantity = entry.toObject().value("quantity").toInt();
auto card = entry.toObject().value("card").toObject();
QString cardName = card.value("name").toString();
QString setName = card.value("set").toString().toUpper();
QString collectorNumber = card.value("cn").toString();
outStream << quantity << ' ' << cardName << " (" << setName << ") " << collectorNumber << '\n';
}
outStream << '\n';
for (auto entry : obj.value("sideboard").toObject()) {
auto quantity = entry.toObject().value("quantity").toInt();
auto card = entry.toObject().value("card").toObject();
QString cardName = card.value("name").toString();
QString setName = card.value("set").toString().toUpper();
QString collectorNumber = card.value("cn").toString();
outStream << quantity << ' ' << cardName << " (" << setName << ") " << collectorNumber << '\n';
}
list->loadFromStream_Plain(outStream, false);
list->resolveSetNameAndNumberToProviderID();
QJsonObject commandersObj = obj.value("commanders").toObject();
if (!commandersObj.isEmpty()) {
for (auto it = commandersObj.begin(); it != commandersObj.end(); ++it) {
QJsonObject cardData = it.value().toObject().value("card").toObject();
QString commanderName = cardData.value("name").toString();
QString setName = cardData.value("set").toString().toUpper();
QString collectorNumber = cardData.value("cn").toString();
QString providerId = cardData.value("scryfall_id").toString();
list->setBannerCard(QPair<QString, QString>(commanderName, providerId));
list->addCard(commanderName, DECK_ZONE_MAIN, -1, setName, collectorNumber, providerId);
}
}
return list;
}
};
#endif // INTERFACE_JSON_DECK_PARSER_H