mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 09:33:25 -08:00
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
* [CardDB] Avoid copying printing maps and property strings Reading a card's sets was a measured hotspot: `SetToPrintingsMap setMap = card->getSets();` copies the whole QMap, and then iterating it with a non-const iterator detaches and deep-copies every PrintingInfo, including its property hash, on first use. A micro-benchmark of that exact pattern (copy plus non-const iteration) came out about 19x slower than a const reference over the same data. The querier, the cache writer, the XML writer, the picture loader and the printing/set selection widgets now take `getSets()` by const reference and only use const iterators. The tests that iterate generated data are covered by the existing card database and picture loader suites. String accessors were also returning by value where the value lives in a hash that has already been materialized: - CardInfo::getProperty() and the legacy getCardType()/getColors()/ getManaCost()/getCmc()/getPowTough()/getLoyalty() accessors, - PrintingInfo::getProperty(), getUuid(), getFlavorName(), getArtist(), and getSet(), - ExactCard::getPrinting() and getCardPtr(), - CardSet::getShortName(), getLongName() and getSetType(). getPrinting() in particular copied a PrintingInfo on every call, and a copy of an unmaterialized PrintingInfo has to take the properties mutex. Callers that need an owned copy still get one; the rest just read. Other allocation removals in the same path: - LazyPropertiesHash::ensureMaterialized() checks an atomic flag before taking the mutex, so property reads after the first one do not lock. The flag is release-stored after materialization and acquire-loaded by readers/copies, preserving the existing locking for the first read. - CardSet::getCorrectedShortName() no longer rebuilds its QSet of reserved Windows file names on every call. - CardSet::getIsKnownIgnored() no longer formats the release date into a throwaway string just to check its length. - The querier builds its card-type/format count caches without `cards.values()` or a properties key list, reserves its result lists, and picks a random card without materializing `cards.keys()`. - The querier's pinned-printing path reuses the CardInfoPtr it already has instead of looking the card up again by name. - CardDatabaseCache/XML serialization copy the alt-name QSet by reference. - The picture loader compiles its !prop!/!set! regular expressions once instead of per URL template, and walks the transform map with an iterator instead of keys() plus operator[]. Full build and ctest (628 tests) pass. * [CardDB] Address review comments PrintingInfo is a value type: it is stored in QLists and returned by value from the querier, so a const-ref getter can hand out a reference into a temporary. Return the QStrings and the CardSetPtr by value again. The constFind lookup stays, so a property read still never materializes the lazy property hash - only the last shared-data refcount is paid. * [CardDB] Undo the const-ref QString return changes QString is implicitly shared, so returning by value costs only a refcount and avoids handing out references into a lazily materialized hash. The constFind lookups stay, so a property read still never materializes the hash. * [CardDB] Use QMap::asKeyValueRange() in transformUrl The range is a view over the map's own iterators and yields references to the stored key and value, so it keeps the no-allocation property of the manual const iteration. --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
400 lines
12 KiB
C++
400 lines
12 KiB
C++
#include "card_database_querier.h"
|
|
|
|
#include "../card_info.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();
|
|
}
|
|
|
|
/**
|
|
* 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::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;
|
|
}
|