mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-06-24 15:43:54 -07:00
Extend decklist parsing (#5316)
* Extend the decklist parsing from clipboard to also support SetName, CollectorNumber and Foil Status. * Q_UNUSED foil for now but keep parsing logic for future PR's/compatibility. --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
parent
cc16b8779c
commit
81b85e97df
11 changed files with 140 additions and 28 deletions
|
|
@ -115,7 +115,7 @@ struct CopyMainOrSide
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
void TappedOutInterface::copyDeckSplitMainAndSide(const DeckList &source, DeckList &mainboard, DeckList &sideboard)
|
void TappedOutInterface::copyDeckSplitMainAndSide(DeckList &source, DeckList &mainboard, DeckList &sideboard)
|
||||||
{
|
{
|
||||||
CopyMainOrSide copyMainOrSide(cardDatabase, mainboard, sideboard);
|
CopyMainOrSide copyMainOrSide(cardDatabase, mainboard, sideboard);
|
||||||
source.forEachCard(copyMainOrSide);
|
source.forEachCard(copyMainOrSide);
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ private:
|
||||||
QNetworkAccessManager *manager;
|
QNetworkAccessManager *manager;
|
||||||
|
|
||||||
CardDatabase &cardDatabase;
|
CardDatabase &cardDatabase;
|
||||||
void copyDeckSplitMainAndSide(const DeckList &source, DeckList &mainboard, DeckList &sideboard);
|
void copyDeckSplitMainAndSide(DeckList &source, DeckList &mainboard, DeckList &sideboard);
|
||||||
private slots:
|
private slots:
|
||||||
void queryFinished(QNetworkReply *reply);
|
void queryFinished(QNetworkReply *reply);
|
||||||
void getAnalyzeRequestData(DeckList *deck, QByteArray *data);
|
void getAnalyzeRequestData(DeckList *deck, QByteArray *data);
|
||||||
|
|
|
||||||
|
|
@ -176,6 +176,42 @@ QString DeckLoader::exportDeckToDecklist()
|
||||||
return deckString;
|
return deckString;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This struct is here to support the forEachCard function call, defined in decklist.
|
||||||
|
// It requires a function to be called for each card, and it will set the providerId.
|
||||||
|
struct SetProviderId
|
||||||
|
{
|
||||||
|
// Main operator for struct, allowing the foreachcard to work.
|
||||||
|
SetProviderId()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void operator()(const InnerDecklistNode *node, DecklistCardNode *card) const
|
||||||
|
{
|
||||||
|
Q_UNUSED(node);
|
||||||
|
// Retrieve the providerId based on setName and collectorNumber
|
||||||
|
QString providerId =
|
||||||
|
CardDatabaseManager::getInstance()
|
||||||
|
->getSpecificSetForCard(card->getName(), card->getCardSetShortName(), card->getCardCollectorNumber())
|
||||||
|
.getProperty("uuid");
|
||||||
|
|
||||||
|
// Set the providerId on the card
|
||||||
|
card->setCardProviderId(providerId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function iterates through each card in the decklist and sets the providerId
|
||||||
|
* on each card based on its set name and collector number.
|
||||||
|
*/
|
||||||
|
void DeckLoader::resolveSetNameAndNumberToProviderID()
|
||||||
|
{
|
||||||
|
// Set up the struct to call.
|
||||||
|
SetProviderId setProviderId;
|
||||||
|
|
||||||
|
// Call the forEachCard method for each card in the deck
|
||||||
|
forEachCard(setProviderId);
|
||||||
|
}
|
||||||
|
|
||||||
DeckLoader::FileFormat DeckLoader::getFormatFromName(const QString &fileName)
|
DeckLoader::FileFormat DeckLoader::getFormatFromName(const QString &fileName)
|
||||||
{
|
{
|
||||||
if (fileName.endsWith(".cod", Qt::CaseInsensitive)) {
|
if (fileName.endsWith(".cod", Qt::CaseInsensitive)) {
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,8 @@ public:
|
||||||
bool saveToFile(const QString &fileName, FileFormat fmt);
|
bool saveToFile(const QString &fileName, FileFormat fmt);
|
||||||
QString exportDeckToDecklist();
|
QString exportDeckToDecklist();
|
||||||
|
|
||||||
|
void resolveSetNameAndNumberToProviderID();
|
||||||
|
|
||||||
// overload
|
// overload
|
||||||
bool saveToStream_Plain(QTextStream &out, bool addComments = true);
|
bool saveToStream_Plain(QTextStream &out, bool addComments = true);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ struct CopyIfNotAToken
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
void DeckStatsInterface::copyDeckWithoutTokens(const DeckList &source, DeckList &destination)
|
void DeckStatsInterface::copyDeckWithoutTokens(DeckList &source, DeckList &destination)
|
||||||
{
|
{
|
||||||
CopyIfNotAToken copyIfNotAToken(cardDatabase, destination);
|
CopyIfNotAToken copyIfNotAToken(cardDatabase, destination);
|
||||||
source.forEachCard(copyIfNotAToken);
|
source.forEachCard(copyIfNotAToken);
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ private:
|
||||||
* closest non-token card instead. So we construct a new deck which has no
|
* closest non-token card instead. So we construct a new deck which has no
|
||||||
* tokens.
|
* tokens.
|
||||||
*/
|
*/
|
||||||
void copyDeckWithoutTokens(const DeckList &source, DeckList &destination);
|
void copyDeckWithoutTokens(DeckList &source, DeckList &destination);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void queryFinished(QNetworkReply *reply);
|
void queryFinished(QNetworkReply *reply);
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ void DlgLoadDeckFromClipboard::actOK()
|
||||||
}
|
}
|
||||||
} else if (deckLoader->loadFromStream_Plain(stream)) {
|
} else if (deckLoader->loadFromStream_Plain(stream)) {
|
||||||
deckList = deckLoader;
|
deckList = deckLoader;
|
||||||
|
deckList->resolveSetNameAndNumberToProviderID();
|
||||||
accept();
|
accept();
|
||||||
} else {
|
} else {
|
||||||
QMessageBox::critical(this, tr("Error"), tr("Invalid deck list."));
|
QMessageBox::critical(this, tr("Error"), tr("Invalid deck list."));
|
||||||
|
|
|
||||||
|
|
@ -704,6 +704,32 @@ CardInfoPerSet CardDatabase::getSpecificSetForCard(const QString &cardName, cons
|
||||||
return CardInfoPerSet(nullptr);
|
return CardInfoPerSet(nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CardInfoPerSet CardDatabase::getSpecificSetForCard(const QString &cardName,
|
||||||
|
const QString &setShortName,
|
||||||
|
const QString &collectorNumber) const
|
||||||
|
{
|
||||||
|
CardInfoPtr cardInfo = getCard(cardName);
|
||||||
|
if (!cardInfo) {
|
||||||
|
return CardInfoPerSet(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
CardInfoPerSetMap setMap = cardInfo->getSets();
|
||||||
|
if (setMap.empty()) {
|
||||||
|
return CardInfoPerSet(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto &cardInfoPerSetList : setMap) {
|
||||||
|
for (auto &cardInfoForSet : cardInfoPerSetList) {
|
||||||
|
if (cardInfoForSet.getPtr()->getShortName() == setShortName &&
|
||||||
|
cardInfoForSet.getProperty("num") == collectorNumber) {
|
||||||
|
return cardInfoForSet;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return CardInfoPerSet(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
QString CardDatabase::getPreferredPrintingProviderIdForCard(const QString &cardName)
|
QString CardDatabase::getPreferredPrintingProviderIdForCard(const QString &cardName)
|
||||||
{
|
{
|
||||||
CardInfoPerSet preferredSetCardInfo = getPreferredSetForCard(cardName);
|
CardInfoPerSet preferredSetCardInfo = getPreferredSetForCard(cardName);
|
||||||
|
|
|
||||||
|
|
@ -465,6 +465,8 @@ public:
|
||||||
[[nodiscard]] CardInfoPtr getCardByNameAndProviderId(const QString &cardName, const QString &providerId) const;
|
[[nodiscard]] CardInfoPtr getCardByNameAndProviderId(const QString &cardName, const QString &providerId) const;
|
||||||
[[nodiscard]] CardInfoPerSet getPreferredSetForCard(const QString &cardName) const;
|
[[nodiscard]] CardInfoPerSet getPreferredSetForCard(const QString &cardName) const;
|
||||||
[[nodiscard]] CardInfoPerSet getSpecificSetForCard(const QString &cardName, const QString &providerId) const;
|
[[nodiscard]] CardInfoPerSet getSpecificSetForCard(const QString &cardName, const QString &providerId) const;
|
||||||
|
CardInfoPerSet
|
||||||
|
getSpecificSetForCard(const QString &cardName, const QString &setShortName, const QString &collectorNumber) const;
|
||||||
QString getPreferredPrintingProviderIdForCard(const QString &cardName);
|
QString getPreferredPrintingProviderIdForCard(const QString &cardName);
|
||||||
[[nodiscard]] CardInfoPtr guessCard(const QString &cardName, const QString &providerId = QString()) const;
|
[[nodiscard]] CardInfoPtr guessCard(const QString &cardName, const QString &providerId = QString()) const;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -530,13 +530,20 @@ bool DeckList::loadFromStream_Plain(QTextStream &in)
|
||||||
const QRegularExpression reDeckComment("^((main)?deck(list)?|mainboard)\\b",
|
const QRegularExpression reDeckComment("^((main)?deck(list)?|mainboard)\\b",
|
||||||
QRegularExpression::CaseInsensitiveOption);
|
QRegularExpression::CaseInsensitiveOption);
|
||||||
|
|
||||||
// simplified matches
|
// Regex for advanced card parsing
|
||||||
const QRegularExpression reMultiplier(R"(^[xX\(\[]*(\d+)[xX\*\)\]]* ?(.+))");
|
const QRegularExpression reMultiplier(R"(^[xX\(\[]*(\d+)[xX\*\)\]]* ?(.+))");
|
||||||
|
const QRegularExpression reSplitCard(R"( ?\/\/ ?)");
|
||||||
const QRegularExpression reBrace(R"( ?[\[\{][^\]\}]*[\]\}] ?)"); // not nested
|
const QRegularExpression reBrace(R"( ?[\[\{][^\]\}]*[\]\}] ?)"); // not nested
|
||||||
const QRegularExpression reRoundBrace(R"(^\([^\)]*\) ?)"); // () are only matched at start of string
|
const QRegularExpression reRoundBrace(R"(^\([^\)]*\) ?)"); // () are only matched at start of string
|
||||||
const QRegularExpression reDigitBrace(R"( ?\(\d*\) ?)"); // () are matched if containing digits
|
const QRegularExpression reDigitBrace(R"( ?\(\d*\) ?)"); // () are matched if containing digits
|
||||||
// () are matched if containing setcode then a number
|
const QRegularExpression reBraceDigit(
|
||||||
const QRegularExpression reBraceDigit(R"( ?\([\dA-Z]+\) *\d+$)");
|
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<QRegularExpression, QString> differences{{QRegularExpression("’"), QString("'")},
|
const QHash<QRegularExpression, QString> differences{{QRegularExpression("’"), QString("'")},
|
||||||
{QRegularExpression("Æ"), QString("Ae")},
|
{QRegularExpression("Æ"), QString("Ae")},
|
||||||
{QRegularExpression("æ"), QString("ae")},
|
{QRegularExpression("æ"), QString("ae")},
|
||||||
|
|
@ -547,11 +554,11 @@ bool DeckList::loadFromStream_Plain(QTextStream &in)
|
||||||
auto inputs = in.readAll().trimmed().split('\n');
|
auto inputs = in.readAll().trimmed().split('\n');
|
||||||
auto max_line = inputs.size();
|
auto max_line = inputs.size();
|
||||||
|
|
||||||
// start at the first empty line before the first cardline
|
// Start at the first empty line before the first card line
|
||||||
auto deckStart = inputs.indexOf(reCardLine);
|
auto deckStart = inputs.indexOf(reCardLine);
|
||||||
if (deckStart == -1) { // there are no cards?
|
if (deckStart == -1) {
|
||||||
if (inputs.indexOf(reComment) == -1) {
|
if (inputs.indexOf(reComment) == -1) {
|
||||||
return false; // input is empty
|
return false; // Input is empty
|
||||||
}
|
}
|
||||||
deckStart = max_line;
|
deckStart = max_line;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -572,7 +579,7 @@ bool DeckList::loadFromStream_Plain(QTextStream &in)
|
||||||
}
|
}
|
||||||
auto nextCard = inputs.indexOf(reCardLine, sBStart + 1);
|
auto nextCard = inputs.indexOf(reCardLine, sBStart + 1);
|
||||||
if (inputs.indexOf(reEmpty, nextCard + 1) != -1) {
|
if (inputs.indexOf(reEmpty, nextCard + 1) != -1) {
|
||||||
sBStart = max_line; // if there is another empty line all cards are mainboard
|
sBStart = max_line;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -580,7 +587,7 @@ bool DeckList::loadFromStream_Plain(QTextStream &in)
|
||||||
int index = 0;
|
int index = 0;
|
||||||
QRegularExpressionMatch match;
|
QRegularExpressionMatch match;
|
||||||
|
|
||||||
// parse name and comments
|
// Parse name and comments
|
||||||
while (index < deckStart) {
|
while (index < deckStart) {
|
||||||
const auto ¤t = inputs.at(index++);
|
const auto ¤t = inputs.at(index++);
|
||||||
if (!current.contains(reEmpty)) {
|
if (!current.contains(reEmpty)) {
|
||||||
|
|
@ -596,29 +603,29 @@ bool DeckList::loadFromStream_Plain(QTextStream &in)
|
||||||
comments += match.captured() + '\n';
|
comments += match.captured() + '\n';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
comments.chop(1); // remove last newline
|
comments.chop(1);
|
||||||
|
|
||||||
// discard empty lines
|
// Discard empty lines
|
||||||
while (index < max_line && inputs.at(index).contains(reEmpty)) {
|
while (index < max_line && inputs.at(index).contains(reEmpty)) {
|
||||||
++index;
|
++index;
|
||||||
}
|
}
|
||||||
|
|
||||||
// discard line if it starts with deck or mainboard, all cards until the sideboard starts are in the mainboard
|
// 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)) {
|
if (inputs.at(index).contains(reDeckComment)) {
|
||||||
++index;
|
++index;
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse decklist
|
// Parse decklist
|
||||||
for (; index < max_line; ++index) {
|
for (; index < max_line; ++index) {
|
||||||
|
|
||||||
// check if line is a card
|
// check if line is a card
|
||||||
match = reCardLine.match(inputs.at(index));
|
match = reCardLine.match(inputs.at(index));
|
||||||
if (!match.hasMatch())
|
if (!match.hasMatch())
|
||||||
continue;
|
continue;
|
||||||
QString cardName = match.captured().simplified();
|
|
||||||
|
|
||||||
// check if card should be sideboard
|
QString cardName = match.captured().simplified();
|
||||||
bool sideboard = false;
|
bool sideboard = false;
|
||||||
|
|
||||||
|
// Sideboard detection
|
||||||
if (sBStart < 0) {
|
if (sBStart < 0) {
|
||||||
match = reSBMark.match(cardName);
|
match = reSBMark.match(cardName);
|
||||||
if (match.hasMatch()) {
|
if (match.hasMatch()) {
|
||||||
|
|
@ -626,11 +633,39 @@ bool DeckList::loadFromStream_Plain(QTextStream &in)
|
||||||
cardName = match.captured(1);
|
cardName = match.captured(1);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (index == sBStart) // skip sideboard line itself
|
if (index == sBStart)
|
||||||
continue;
|
continue;
|
||||||
sideboard = index > sBStart;
|
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
|
// check if a specific amount is mentioned
|
||||||
int amount = 1;
|
int amount = 1;
|
||||||
match = reMultiplier.match(cardName);
|
match = reMultiplier.match(cardName);
|
||||||
|
|
@ -639,25 +674,35 @@ bool DeckList::loadFromStream_Plain(QTextStream &in)
|
||||||
cardName = match.captured(2);
|
cardName = match.captured(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove stuff inbetween braces
|
// 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(reBrace);
|
||||||
cardName.remove(reRoundBrace); // I'll be entirely honest here, these are split to accommodate just three cards
|
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(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
|
cardName.remove(reBraceDigit); // very specific format with the set code in () and collectors number after
|
||||||
|
|
||||||
// replace common differences in cardnames
|
// Normalize names
|
||||||
for (auto diff = differences.constBegin(); diff != differences.constEnd(); ++diff) {
|
for (auto diff = differences.constBegin(); diff != differences.constEnd(); ++diff) {
|
||||||
cardName.replace(diff.key(), diff.value());
|
cardName.replace(diff.key(), diff.value());
|
||||||
}
|
}
|
||||||
|
|
||||||
// get cardname, this function does nothing if the name is not found
|
// Resolve complete card name, this function does nothing if the name is not found
|
||||||
cardName = getCompleteCardName(cardName);
|
cardName = getCompleteCardName(cardName);
|
||||||
|
|
||||||
// get zone name based on if it's in sideboard
|
// Determine the zone (mainboard/sideboard)
|
||||||
QString zoneName = getCardZoneFromName(cardName, sideboard ? DECK_ZONE_SIDE : DECK_ZONE_MAIN);
|
QString zoneName = getCardZoneFromName(cardName, sideboard ? DECK_ZONE_SIDE : DECK_ZONE_MAIN);
|
||||||
|
|
||||||
// make new entry in decklist
|
// make new entry in decklist
|
||||||
new DecklistCardNode(cardName, amount, getZoneObjFromName(zoneName));
|
new DecklistCardNode(cardName, amount, getZoneObjFromName(zoneName), setCode, collectorNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDeckHash();
|
updateDeckHash();
|
||||||
|
|
|
||||||
|
|
@ -352,14 +352,14 @@ public:
|
||||||
* take a InnerDecklistNode* as its first argument and a
|
* take a InnerDecklistNode* as its first argument and a
|
||||||
* DecklistCardNode* as its second.
|
* DecklistCardNode* as its second.
|
||||||
*/
|
*/
|
||||||
template <typename Callback> void forEachCard(Callback &callback) const
|
template <typename Callback> void forEachCard(Callback &callback)
|
||||||
{
|
{
|
||||||
// Support for this is only possible if the internal structure
|
// Support for this is only possible if the internal structure
|
||||||
// doesn't get more complicated.
|
// doesn't get more complicated.
|
||||||
for (int i = 0; i < root->size(); i++) {
|
for (int i = 0; i < root->size(); i++) {
|
||||||
const InnerDecklistNode *node = dynamic_cast<InnerDecklistNode *>(root->at(i));
|
InnerDecklistNode *node = dynamic_cast<InnerDecklistNode *>(root->at(i));
|
||||||
for (int j = 0; j < node->size(); j++) {
|
for (int j = 0; j < node->size(); j++) {
|
||||||
const DecklistCardNode *card = dynamic_cast<DecklistCardNode *>(node->at(j));
|
DecklistCardNode *card = dynamic_cast<DecklistCardNode *>(node->at(j));
|
||||||
callback(node, card);
|
callback(node, card);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue