mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-08 19:13:25 -08:00
* [Card] Import Scryfall Tagger tags from MTGJson * [Card] Search cards by Scryfall Tagger tag * [Card] Add a Tag attribute to the card filter tree * [Card] Add a Tags filter to the visual card database * [Oracle] Add a script to inject Scryfall tags into AllPrintings * [Card] Elide long tag lists in the card info and drop the Scryfall label from the tag filter tooltip * [Oracle] Load card tags from a tag database during import Add a TagDatabase loader that understands Scryfall's Oracle Tags bulk data and user-supplied tag files (tag objects, flat oracle-id or name maps, optional manifest, JSON or JSONL, gzip/xz/zip). The importer joins the slugs on identifiers.scryfallOracleId while the identifiers are still available, falls back to the card name, and unions them with any tags the source already provides. Split-card faces merge through the existing tag union path. This replaces oracle/scripts/inject_scryfall_tags.py, which patched AllPrintings.json before import. Loading during import keeps tags on by default for every source and lets users pair Oracle with any tag database. * [Oracle] Add a tag data step to the import wizard Insert a Tags page between set selection and the import. It downloads Scryfall's Oracle Tags by default (or a user-supplied file/URL), can include parent tags, and persists tagsenabled, tagssource and tagsparents in oracle.ini. The page loads asynchronously and soft-fails in background mode, so a tag download or parse error never aborts the card database import. * [Help] Describe tag search without naming Scryfall Tags can come from Scryfall's Oracle Tags or any user-supplied tag database, so the search help should not claim a single source. * [Oracle] Fix Scryfall tag requests and the retry loop api.scryfall.com rejects requests that lack an Accept header with HTTP 400, and QNetworkAccessManager does not add one. Send application/json for the bulk metadata request and */* for the tag file, and surface the URL plus Scryfall's JSON error body when a request fails. A failed tag load used to call next(), which re-ran validatePage() and started the same failing download again, trapping the user in an error loop. Stay on the page instead, drop the unusable database, and tell the user they can retry, change the source, or untick the option to import without tags. Also explain parent tags inline with a real Scryfall hierarchy example. * [Oracle] Give the parent-tag explainer room to breathe The wrapped explainer sat flush under the checkbox with no separation from the status label, which made the paragraph look cramped. Indent it under the checkbox and add a little vertical margin. * [Oracle] Stop the parent-tag explainer from being clipped A word-wrapped QLabel reports a one-line sizeHint, so the wizard page reserved too little height and the explanation lost its top and bottom lines. Pin the vertical size policy to Minimum on the explainer and on the status label, so the wrapped height becomes part of the page minimum. Verified with a layout probe: the label went from 28 px tall (needed 56) to its full height. --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
417 lines
13 KiB
C++
417 lines
13 KiB
C++
#include "card_database_querier.h"
|
|
|
|
#include "../card_info.h"
|
|
#include "../game_specific_terms.h"
|
|
#include "../printing/exact_card.h"
|
|
#include "../set/card_set_comparator.h"
|
|
#include "card_database.h"
|
|
#include "libcockatrice/card/set/card_set.h"
|
|
#include "libcockatrice/interfaces/interface_card_preference_provider.h"
|
|
#include "libcockatrice/utility/card_ref.h"
|
|
|
|
#include <QLatin1String>
|
|
#include <QRandomGenerator>
|
|
#include <QSharedPointer>
|
|
#include <QStringLiteral>
|
|
#include <iterator>
|
|
#include <qnamespace.h>
|
|
|
|
CardDatabaseQuerier::CardDatabaseQuerier(QObject *_parent,
|
|
const CardDatabase *_db,
|
|
const ICardPreferenceProvider *prefs)
|
|
: QObject(_parent), db(_db), prefs(prefs)
|
|
{
|
|
// Invalidate the cached count maps whenever the database contents change.
|
|
connect(db, &CardDatabase::cardAdded, this, &CardDatabaseQuerier::invalidateCaches);
|
|
connect(db, &CardDatabase::cardRemoved, this, &CardDatabaseQuerier::invalidateCaches);
|
|
connect(db, &CardDatabase::cardDatabaseReset, this, &CardDatabaseQuerier::invalidateCaches);
|
|
}
|
|
|
|
void CardDatabaseQuerier::invalidateCaches()
|
|
{
|
|
mainCardTypeCountsCache.clear();
|
|
subCardTypeCountsCache.clear();
|
|
formatsCountCache.clear();
|
|
tagCountsCache.clear();
|
|
}
|
|
|
|
/**
|
|
* Looks up the cardInfo corresponding to the cardName.
|
|
*
|
|
* @param cardName The card name to look up
|
|
* @return A CardInfoPtr, or null if not corresponding CardInfo is found.
|
|
*/
|
|
CardInfoPtr CardDatabaseQuerier::getCardInfo(const QString &cardName) const
|
|
{
|
|
return db->cards.value(cardName);
|
|
}
|
|
|
|
/**
|
|
* Looks up the cardInfos for a list of card names.
|
|
*
|
|
* @param cardNames The card names to look up
|
|
* @return A List of CardInfoPtr. Any failed lookups will be ignored and dropped from the resulting list
|
|
*/
|
|
QList<CardInfoPtr> CardDatabaseQuerier::getCardInfos(const QStringList &cardNames) const
|
|
{
|
|
QList<CardInfoPtr> cardInfos;
|
|
cardInfos.reserve(cardNames.size());
|
|
for (const QString &cardName : cardNames) {
|
|
CardInfoPtr ptr = db->cards.value(cardName);
|
|
if (ptr) {
|
|
cardInfos.append(ptr);
|
|
}
|
|
}
|
|
|
|
return cardInfos;
|
|
}
|
|
|
|
CardInfoPtr CardDatabaseQuerier::getCardBySimpleName(const QString &cardName) const
|
|
{
|
|
return db->simpleNameCards.value(CardInfo::simplifyName(cardName));
|
|
}
|
|
|
|
CardInfoPtr CardDatabaseQuerier::lookupCardByName(const QString &name) const
|
|
{
|
|
if (auto info = getCardInfo(name)) {
|
|
return info;
|
|
}
|
|
if (auto info = getCardBySimpleName(name)) {
|
|
return info;
|
|
}
|
|
return getCardBySimpleName(CardInfo::simplifyName(name));
|
|
}
|
|
|
|
/**
|
|
* Looks up the cards corresponding to the CardRefs.
|
|
* If the providerId is empty, will default to the preferred printing.
|
|
* If providerId is given but not found, the PrintingInfo will be empty.
|
|
*
|
|
* @param cardRefs The cards to look up. If providerId is empty for an entry, will default to the preferred printing for
|
|
* that entry. If providerId is given but not found, the PrintingInfo will be empty for that entry.
|
|
* @return A list of cards. Any failed lookups will be ignored and dropped from the resulting list.
|
|
*/
|
|
QList<ExactCard> CardDatabaseQuerier::getCards(const QList<CardRef> &cardRefs) const
|
|
{
|
|
QList<ExactCard> cards;
|
|
cards.reserve(cardRefs.size());
|
|
for (const auto &cardRef : cardRefs) {
|
|
ExactCard card = getCard(cardRef);
|
|
if (card) {
|
|
cards.append(card);
|
|
}
|
|
}
|
|
|
|
return cards;
|
|
}
|
|
|
|
/**
|
|
* Looks up the card corresponding to the CardRef.
|
|
* If the providerId is empty, will default to the preferred printing.
|
|
* If providerId is given but not found, the PrintingInfo will be empty.
|
|
*
|
|
* @param cardRef The card to look up.
|
|
* @return A specific printing of a card, or empty if not found.
|
|
*/
|
|
ExactCard CardDatabaseQuerier::getCard(const CardRef &cardRef) const
|
|
{
|
|
auto info = getCardInfo(cardRef.name);
|
|
if (info.isNull()) {
|
|
return {};
|
|
}
|
|
|
|
if (cardRef.providerId.isEmpty() || cardRef.providerId.isNull()) {
|
|
return ExactCard(info, getPreferredPrinting(info));
|
|
}
|
|
|
|
return ExactCard(info, findPrintingWithId(info, cardRef.providerId));
|
|
}
|
|
|
|
/**
|
|
* Looks up the card by CardRef, simplifying the name if required.
|
|
* If the providerId is empty, will default to the preferred printing.
|
|
* If providerId is given but not found, the PrintingInfo will be empty.
|
|
*
|
|
* @param cardRef The card to look up.
|
|
* @return A specific printing of a card, or empty if not found.
|
|
*/
|
|
ExactCard CardDatabaseQuerier::guessCard(const CardRef &cardRef) const
|
|
{
|
|
auto card = lookupCardByName(cardRef.name);
|
|
if (!card) {
|
|
return {};
|
|
}
|
|
|
|
auto printing =
|
|
cardRef.providerId.isEmpty() ? getPreferredPrinting(card) : findPrintingWithId(card, cardRef.providerId);
|
|
|
|
return ExactCard(card, printing);
|
|
}
|
|
|
|
ExactCard CardDatabaseQuerier::getRandomCard() const
|
|
{
|
|
if (db->cards.isEmpty()) {
|
|
return {};
|
|
}
|
|
|
|
const auto randomIndex = QRandomGenerator::global()->bounded(db->cards.size());
|
|
auto it = db->cards.constBegin();
|
|
std::advance(it, randomIndex);
|
|
CardInfoPtr randomCard = it.value();
|
|
|
|
return ExactCard{randomCard, getPreferredPrinting(randomCard)};
|
|
}
|
|
|
|
ExactCard CardDatabaseQuerier::getCardFromSameSet(const QString &cardName, const PrintingInfo &otherPrinting) const
|
|
{
|
|
// The source card does not have a printing defined, which means we can't get a card from the same set.
|
|
if (otherPrinting.isEmpty()) {
|
|
return getCard({cardName});
|
|
}
|
|
|
|
// The source card does have a printing defined, which means we can attempt to get a card from the same set.
|
|
PrintingInfo relatedPrinting = getSpecificPrinting(cardName, otherPrinting.getSet()->getCorrectedShortName(), "");
|
|
ExactCard relatedCard(guessCard({cardName}).getCardPtr(), relatedPrinting);
|
|
|
|
// If we didn't find a card from the same set, just try to find any card with the same name.
|
|
return relatedCard ? relatedCard : getCard({cardName});
|
|
}
|
|
|
|
/**
|
|
* Finds the PrintingInfo in the cardInfo that has the given uuid field.
|
|
*
|
|
* @param cardInfo The CardInfo to search
|
|
* @param providerId The uuid to look for
|
|
* @return The PrintingInfo, or a default-constructed PrintingInfo if not found or if cardInfo is null.
|
|
*/
|
|
PrintingInfo CardDatabaseQuerier::findPrintingWithId(const CardInfoPtr &cardInfo, const QString &providerId) const
|
|
{
|
|
if (!cardInfo) {
|
|
return PrintingInfo();
|
|
}
|
|
|
|
for (const auto &printings : cardInfo->getSets()) {
|
|
for (const auto &printing : printings) {
|
|
if (printing.getUuid() == providerId) {
|
|
return printing;
|
|
}
|
|
}
|
|
}
|
|
|
|
return PrintingInfo();
|
|
}
|
|
|
|
PrintingInfo CardDatabaseQuerier::getSpecificPrinting(const CardRef &cardRef) const
|
|
{
|
|
CardInfoPtr cardInfo = getCardInfo(cardRef.name);
|
|
if (!cardInfo) {
|
|
return PrintingInfo(nullptr);
|
|
}
|
|
|
|
return findPrintingWithId(cardInfo, cardRef.providerId);
|
|
}
|
|
|
|
PrintingInfo CardDatabaseQuerier::getSpecificPrinting(const QString &cardName,
|
|
const QString &setShortName,
|
|
const QString &collectorNumber) const
|
|
{
|
|
CardInfoPtr cardInfo = getCardInfo(cardName);
|
|
if (!cardInfo) {
|
|
return PrintingInfo(nullptr);
|
|
}
|
|
|
|
const auto &setMap = cardInfo->getSets();
|
|
if (setMap.empty()) {
|
|
return PrintingInfo(nullptr);
|
|
}
|
|
|
|
for (const auto &printings : setMap) {
|
|
for (const auto &cardInfoForSet : printings) {
|
|
const CardSetPtr &set = cardInfoForSet.getSet();
|
|
if (!set) {
|
|
continue;
|
|
}
|
|
if (!collectorNumber.isEmpty()) {
|
|
if (set->getShortName() == setShortName && cardInfoForSet.getProperty("num") == collectorNumber) {
|
|
return cardInfoForSet;
|
|
}
|
|
} else {
|
|
if (set->getShortName() == setShortName) {
|
|
return cardInfoForSet;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return PrintingInfo(nullptr);
|
|
}
|
|
|
|
/**
|
|
* Gets the card representing the preferred printing of the cardInfo
|
|
*
|
|
* @param cardName The cardName to find the preferred card and printing for
|
|
* @return A specific printing of a card
|
|
*/
|
|
ExactCard CardDatabaseQuerier::getPreferredCard(const QString &cardName) const
|
|
{
|
|
return getPreferredCard(getCardInfo(cardName));
|
|
}
|
|
|
|
/**
|
|
* Gets the card representing the preferred printing of the cardInfo
|
|
*
|
|
* @param cardInfo The cardInfo to find the preferred printing for
|
|
* @return A specific printing of a card
|
|
*/
|
|
ExactCard CardDatabaseQuerier::getPreferredCard(const CardInfoPtr &cardInfo) const
|
|
{
|
|
return ExactCard(cardInfo, getPreferredPrinting(cardInfo));
|
|
}
|
|
|
|
bool CardDatabaseQuerier::isPreferredPrinting(const CardRef &cardRef) const
|
|
{
|
|
if (cardRef.providerId.startsWith("card_")) {
|
|
return cardRef.providerId ==
|
|
QLatin1String("card_") + cardRef.name + QString("_") + getPreferredPrintingProviderId(cardRef.name);
|
|
}
|
|
return cardRef.providerId == getPreferredPrintingProviderId(cardRef.name);
|
|
}
|
|
|
|
PrintingInfo CardDatabaseQuerier::getPreferredPrinting(const QString &cardName) const
|
|
{
|
|
CardInfoPtr cardInfo = getCardInfo(cardName);
|
|
return getPreferredPrinting(cardInfo);
|
|
}
|
|
|
|
PrintingInfo CardDatabaseQuerier::getPreferredPrinting(const CardInfoPtr &cardInfo) const
|
|
{
|
|
if (!cardInfo) {
|
|
return PrintingInfo(nullptr);
|
|
}
|
|
|
|
const auto &pinnedPrintingProviderId = prefs->getCardPreferenceOverride(cardInfo->getName());
|
|
|
|
if (!pinnedPrintingProviderId.isEmpty()) {
|
|
return findPrintingWithId(cardInfo, pinnedPrintingProviderId);
|
|
}
|
|
|
|
const auto &setMap = cardInfo->getSets();
|
|
if (setMap.empty()) {
|
|
return PrintingInfo(nullptr);
|
|
}
|
|
|
|
CardSetPtr preferredSet = nullptr;
|
|
PrintingInfo preferredPrinting;
|
|
SetPriorityComparator comparator;
|
|
|
|
for (const auto &printings : setMap) {
|
|
for (const auto &printing : printings) {
|
|
const CardSetPtr ¤tSet = printing.getSet();
|
|
if (!preferredSet || comparator(currentSet, preferredSet)) {
|
|
preferredSet = currentSet;
|
|
preferredPrinting = printing;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (preferredSet) {
|
|
return preferredPrinting;
|
|
}
|
|
|
|
return PrintingInfo(nullptr);
|
|
}
|
|
|
|
QString CardDatabaseQuerier::getPreferredPrintingProviderId(const QString &cardName) const
|
|
{
|
|
CardInfoPtr cardInfo = getCardInfo(cardName);
|
|
if (!cardInfo) {
|
|
return cardName;
|
|
}
|
|
|
|
PrintingInfo preferredPrinting = getPreferredPrinting(cardInfo);
|
|
const QString &uuid = preferredPrinting.getUuid();
|
|
if (!uuid.isEmpty()) {
|
|
return uuid;
|
|
}
|
|
|
|
return cardInfo->getName();
|
|
}
|
|
|
|
QStringList CardDatabaseQuerier::getAllMainCardTypes() const
|
|
{
|
|
return getAllMainCardTypesWithCount().keys();
|
|
}
|
|
|
|
QMap<QString, int> CardDatabaseQuerier::getAllMainCardTypesWithCount() const
|
|
{
|
|
// An empty cache is always recomputed correctly: a database with no cards
|
|
// produces an empty map, so the cache is only ever empty when it needs a
|
|
// (trivially cheap) rebuild.
|
|
if (mainCardTypeCountsCache.isEmpty()) {
|
|
for (const auto &card : db->cards) {
|
|
const QString type = card->getMainCardType();
|
|
mainCardTypeCountsCache[type]++;
|
|
}
|
|
}
|
|
|
|
return mainCardTypeCountsCache;
|
|
}
|
|
|
|
QMap<QString, int> CardDatabaseQuerier::getAllSubCardTypesWithCount() const
|
|
{
|
|
if (subCardTypeCountsCache.isEmpty()) {
|
|
for (const auto &card : db->cards) {
|
|
const QString type = card->getCardType();
|
|
|
|
const QStringList parts = type.split(" — ");
|
|
|
|
if (parts.size() > 1) { // Ensure there are subtypes
|
|
const QStringList subtypes = parts[1].split(" ", Qt::SkipEmptyParts);
|
|
|
|
for (const QString &subtype : subtypes) {
|
|
subCardTypeCountsCache[subtype]++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return subCardTypeCountsCache;
|
|
}
|
|
|
|
FormatRulesPtr CardDatabaseQuerier::getFormat(const QString &formatName) const
|
|
{
|
|
return db->formats.value(formatName.toLower());
|
|
}
|
|
|
|
QMap<QString, int> CardDatabaseQuerier::getAllTagsWithCount() const
|
|
{
|
|
if (tagCountsCache.isEmpty()) {
|
|
for (const auto &card : db->cards.values()) {
|
|
const QStringList tags = card->getProperty(Mtg::Tags).split(" ", Qt::SkipEmptyParts);
|
|
|
|
for (const QString &tag : tags) {
|
|
tagCountsCache[tag]++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return tagCountsCache;
|
|
}
|
|
|
|
QMap<QString, int> CardDatabaseQuerier::getAllFormatsWithCount() const
|
|
{
|
|
if (formatsCountCache.isEmpty()) {
|
|
static const QString formatPrefix = QStringLiteral("format-");
|
|
|
|
for (const auto &card : db->cards) {
|
|
const auto &properties = card->getPropertiesHash();
|
|
for (auto it = properties.constBegin(); it != properties.constEnd(); ++it) {
|
|
if (it.key().startsWith(formatPrefix)) {
|
|
formatsCountCache[it.key().mid(formatPrefix.size())]++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return formatsCountCache;
|
|
}
|