diff --git a/cockatrice/src/client/tapped_out_interface.cpp b/cockatrice/src/client/tapped_out_interface.cpp index 796b9ddc7..46ea4bf74 100644 --- a/cockatrice/src/client/tapped_out_interface.cpp +++ b/cockatrice/src/client/tapped_out_interface.cpp @@ -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); source.forEachCard(copyMainOrSide); diff --git a/cockatrice/src/client/tapped_out_interface.h b/cockatrice/src/client/tapped_out_interface.h index 239a3ee18..3fb5f4778 100644 --- a/cockatrice/src/client/tapped_out_interface.h +++ b/cockatrice/src/client/tapped_out_interface.h @@ -24,7 +24,7 @@ private: QNetworkAccessManager *manager; CardDatabase &cardDatabase; - void copyDeckSplitMainAndSide(const DeckList &source, DeckList &mainboard, DeckList &sideboard); + void copyDeckSplitMainAndSide(DeckList &source, DeckList &mainboard, DeckList &sideboard); private slots: void queryFinished(QNetworkReply *reply); void getAnalyzeRequestData(DeckList *deck, QByteArray *data); diff --git a/cockatrice/src/deck/deck_loader.cpp b/cockatrice/src/deck/deck_loader.cpp index 685c033a3..e64ef7446 100644 --- a/cockatrice/src/deck/deck_loader.cpp +++ b/cockatrice/src/deck/deck_loader.cpp @@ -176,6 +176,42 @@ QString DeckLoader::exportDeckToDecklist() 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) { if (fileName.endsWith(".cod", Qt::CaseInsensitive)) { diff --git a/cockatrice/src/deck/deck_loader.h b/cockatrice/src/deck/deck_loader.h index 61b695335..3f1ff1d28 100644 --- a/cockatrice/src/deck/deck_loader.h +++ b/cockatrice/src/deck/deck_loader.h @@ -47,6 +47,8 @@ public: bool saveToFile(const QString &fileName, FileFormat fmt); QString exportDeckToDecklist(); + void resolveSetNameAndNumberToProviderID(); + // overload bool saveToStream_Plain(QTextStream &out, bool addComments = true); diff --git a/cockatrice/src/deck/deck_stats_interface.cpp b/cockatrice/src/deck/deck_stats_interface.cpp index 038565370..4051bfe74 100644 --- a/cockatrice/src/deck/deck_stats_interface.cpp +++ b/cockatrice/src/deck/deck_stats_interface.cpp @@ -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); source.forEachCard(copyIfNotAToken); diff --git a/cockatrice/src/deck/deck_stats_interface.h b/cockatrice/src/deck/deck_stats_interface.h index 4798ee67b..a6d77d1dc 100644 --- a/cockatrice/src/deck/deck_stats_interface.h +++ b/cockatrice/src/deck/deck_stats_interface.h @@ -24,7 +24,7 @@ private: * closest non-token card instead. So we construct a new deck which has no * tokens. */ - void copyDeckWithoutTokens(const DeckList &source, DeckList &destination); + void copyDeckWithoutTokens(DeckList &source, DeckList &destination); private slots: void queryFinished(QNetworkReply *reply); diff --git a/cockatrice/src/dialogs/dlg_load_deck_from_clipboard.cpp b/cockatrice/src/dialogs/dlg_load_deck_from_clipboard.cpp index dd06b45c8..4077a8456 100644 --- a/cockatrice/src/dialogs/dlg_load_deck_from_clipboard.cpp +++ b/cockatrice/src/dialogs/dlg_load_deck_from_clipboard.cpp @@ -65,6 +65,7 @@ void DlgLoadDeckFromClipboard::actOK() } } else if (deckLoader->loadFromStream_Plain(stream)) { deckList = deckLoader; + deckList->resolveSetNameAndNumberToProviderID(); accept(); } else { QMessageBox::critical(this, tr("Error"), tr("Invalid deck list.")); diff --git a/cockatrice/src/game/cards/card_database.cpp b/cockatrice/src/game/cards/card_database.cpp index fb39f278e..00fa4824d 100644 --- a/cockatrice/src/game/cards/card_database.cpp +++ b/cockatrice/src/game/cards/card_database.cpp @@ -704,6 +704,32 @@ CardInfoPerSet CardDatabase::getSpecificSetForCard(const QString &cardName, cons 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) { CardInfoPerSet preferredSetCardInfo = getPreferredSetForCard(cardName); diff --git a/cockatrice/src/game/cards/card_database.h b/cockatrice/src/game/cards/card_database.h index 83ea647d3..a2164bad9 100644 --- a/cockatrice/src/game/cards/card_database.h +++ b/cockatrice/src/game/cards/card_database.h @@ -465,6 +465,8 @@ public: [[nodiscard]] CardInfoPtr getCardByNameAndProviderId(const QString &cardName, const QString &providerId) const; [[nodiscard]] CardInfoPerSet getPreferredSetForCard(const QString &cardName) 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); [[nodiscard]] CardInfoPtr guessCard(const QString &cardName, const QString &providerId = QString()) const; diff --git a/common/decklist.cpp b/common/decklist.cpp index 6ffaa7725..5c34fdb06 100644 --- a/common/decklist.cpp +++ b/common/decklist.cpp @@ -530,13 +530,20 @@ bool DeckList::loadFromStream_Plain(QTextStream &in) const QRegularExpression reDeckComment("^((main)?deck(list)?|mainboard)\\b", QRegularExpression::CaseInsensitiveOption); - // simplified matches + // Regex for advanced card parsing const QRegularExpression reMultiplier(R"(^[xX\(\[]*(\d+)[xX\*\)\]]* ?(.+))"); + const QRegularExpression reSplitCard(R"( ?\/\/ ?)"); const QRegularExpression reBrace(R"( ?[\[\{][^\]\}]*[\]\}] ?)"); // not nested const QRegularExpression reRoundBrace(R"(^\([^\)]*\) ?)"); // () are only matched at start of string const QRegularExpression reDigitBrace(R"( ?\(\d*\) ?)"); // () are matched if containing digits - // () are matched if containing setcode then a number - const QRegularExpression reBraceDigit(R"( ?\([\dA-Z]+\) *\d+$)"); + const QRegularExpression reBraceDigit( + 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 differences{{QRegularExpression("’"), QString("'")}, {QRegularExpression("Æ"), QString("Ae")}, {QRegularExpression("æ"), QString("ae")}, @@ -547,11 +554,11 @@ bool DeckList::loadFromStream_Plain(QTextStream &in) auto inputs = in.readAll().trimmed().split('\n'); 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); - if (deckStart == -1) { // there are no cards? + if (deckStart == -1) { if (inputs.indexOf(reComment) == -1) { - return false; // input is empty + return false; // Input is empty } deckStart = max_line; } else { @@ -572,7 +579,7 @@ bool DeckList::loadFromStream_Plain(QTextStream &in) } auto nextCard = inputs.indexOf(reCardLine, sBStart + 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; QRegularExpressionMatch match; - // parse name and comments + // Parse name and comments while (index < deckStart) { const auto ¤t = inputs.at(index++); if (!current.contains(reEmpty)) { @@ -596,29 +603,29 @@ bool DeckList::loadFromStream_Plain(QTextStream &in) 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)) { ++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)) { ++index; } - // parse decklist + // Parse decklist for (; index < max_line; ++index) { - // check if line is a card match = reCardLine.match(inputs.at(index)); if (!match.hasMatch()) continue; - QString cardName = match.captured().simplified(); - // check if card should be sideboard + QString cardName = match.captured().simplified(); bool sideboard = false; + + // Sideboard detection if (sBStart < 0) { match = reSBMark.match(cardName); if (match.hasMatch()) { @@ -626,11 +633,39 @@ bool DeckList::loadFromStream_Plain(QTextStream &in) cardName = match.captured(1); } } else { - if (index == sBStart) // skip sideboard line itself + if (index == sBStart) continue; 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 int amount = 1; match = reMultiplier.match(cardName); @@ -639,25 +674,35 @@ bool DeckList::loadFromStream_Plain(QTextStream &in) 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(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(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) { 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); - // 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); // make new entry in decklist - new DecklistCardNode(cardName, amount, getZoneObjFromName(zoneName)); + new DecklistCardNode(cardName, amount, getZoneObjFromName(zoneName), setCode, collectorNumber); } updateDeckHash(); diff --git a/common/decklist.h b/common/decklist.h index e6e5dad15..3c879e376 100644 --- a/common/decklist.h +++ b/common/decklist.h @@ -352,14 +352,14 @@ public: * take a InnerDecklistNode* as its first argument and a * DecklistCardNode* as its second. */ - template void forEachCard(Callback &callback) const + template void forEachCard(Callback &callback) { // Support for this is only possible if the internal structure // doesn't get more complicated. for (int i = 0; i < root->size(); i++) { - const InnerDecklistNode *node = dynamic_cast(root->at(i)); + InnerDecklistNode *node = dynamic_cast(root->at(i)); for (int j = 0; j < node->size(); j++) { - const DecklistCardNode *card = dynamic_cast(node->at(j)); + DecklistCardNode *card = dynamic_cast(node->at(j)); callback(node, card); } }