From a0e76607b5a0fb8693c12d79135b8a46efe93929 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Mon, 10 Aug 2026 22:43:21 -0700 Subject: [PATCH] [CardInfo] Encapsulate lazy properties loading into new class (#7088) * [CardInfo] Encapsulate lazy properties loading into new class * check if new value was inserted --- libcockatrice_card/CMakeLists.txt | 2 + .../libcockatrice/card/card_info.cpp | 64 +++---------- .../libcockatrice/card/card_info.h | 22 ++--- .../card/database/card_database_cache.cpp | 2 +- .../card/database/parser/cockatrice_xml_3.cpp | 2 +- .../card/database/parser/cockatrice_xml_4.cpp | 2 +- .../card/lazy_properties_hash.cpp | 95 +++++++++++++++++++ .../libcockatrice/card/lazy_properties_hash.h | 74 +++++++++++++++ .../card/printing/printing_info.cpp | 29 +----- .../card/printing/printing_info.h | 32 ++----- oracle/src/oracleimporter.cpp | 2 +- 11 files changed, 204 insertions(+), 122 deletions(-) create mode 100644 libcockatrice_card/libcockatrice/card/lazy_properties_hash.cpp create mode 100644 libcockatrice_card/libcockatrice/card/lazy_properties_hash.h diff --git a/libcockatrice_card/CMakeLists.txt b/libcockatrice_card/CMakeLists.txt index 7d3d47eea..081e6fd05 100644 --- a/libcockatrice_card/CMakeLists.txt +++ b/libcockatrice_card/CMakeLists.txt @@ -5,6 +5,7 @@ set(CMAKE_AUTORCC ON) set(HEADERS libcockatrice/card/card_info.h libcockatrice/card/card_info_comparator.h + libcockatrice/card/lazy_properties_hash.h libcockatrice/card/database/card_database.h libcockatrice/card/database/card_database_loader.h libcockatrice/card/database/card_database_manager.h @@ -26,6 +27,7 @@ add_library( ${MOC_SOURCES} libcockatrice/card/card_info.cpp libcockatrice/card/card_info_comparator.cpp + libcockatrice/card/lazy_properties_hash.cpp libcockatrice/card/database/card_database.cpp libcockatrice/card/database/card_database_cache.cpp libcockatrice/card/database/card_database_loader.cpp diff --git a/libcockatrice_card/libcockatrice/card/card_info.cpp b/libcockatrice_card/libcockatrice/card/card_info.cpp index f03503550..786e17950 100644 --- a/libcockatrice_card/libcockatrice/card/card_info.cpp +++ b/libcockatrice_card/libcockatrice/card/card_info.cpp @@ -5,10 +5,7 @@ #include "relation/card_relation.h" #include "set/card_set.h" -#include #include -#include -#include #include #include #include @@ -21,64 +18,33 @@ class CardInfo; using CardInfoPtr = QSharedPointer; -namespace -{ -QByteArray serializeProperties(const QHash &props) -{ - QByteArray blob; - QDataStream out(&blob, QIODevice::WriteOnly); - out.setVersion(QDataStream::Qt_6_4); - out << props; - return blob; -} -} // namespace - -void CardInfo::ensurePropertiesLoaded() const -{ - QMutexLocker lock(&propertiesMutex); - if (propertiesLoaded) { - return; - } - if (!propertiesBlob.isEmpty()) { - QDataStream in(propertiesBlob); - in.setVersion(QDataStream::Qt_6_4); - in >> propertiesCache; - } - propertiesLoaded = true; -} - const QHash &CardInfo::getPropertiesHash() const { - ensurePropertiesLoaded(); - return propertiesCache; + return properties.getProperties(); } void CardInfo::setProperty(const QString &_name, const QString &_value) { - ensurePropertiesLoaded(); - if (propertiesCache.value(_name) == _value) { + bool changed = properties.insert(_name, _value); + if (!changed) { return; } - propertiesCache.insert(_name, _value); - propertiesBlob = serializeProperties(propertiesCache); + emit cardInfoChanged(smartThis); } CardInfo::CardInfo(const QString &_name, const QString &_text, bool _isToken, - QHash _properties, + const QHash &_properties, const QList &_relatedCards, const QList &_reverseRelatedCards, SetToPrintingsMap _sets, const UiAttributes _uiAttributes) - : name(_name), text(_text), isToken(_isToken), relatedCards(_relatedCards), - reverseRelatedCards(_reverseRelatedCards), setsToPrintings(std::move(_sets)), uiAttributes(_uiAttributes) + : name(_name), text(_text), isToken(_isToken), properties(LazyPropertiesHash(_properties)), + relatedCards(_relatedCards), reverseRelatedCards(_reverseRelatedCards), setsToPrintings(std::move(_sets)), + uiAttributes(_uiAttributes) { - propertiesCache = std::move(_properties); - propertiesBlob = serializeProperties(propertiesCache); - propertiesLoaded = true; - simpleName = CardInfo::simplifyName(name); refreshCachedSets(); @@ -87,7 +53,7 @@ CardInfo::CardInfo(const QString &_name, CardInfo::CardInfo(const QString &_name, const QString &_text, bool _isToken, - QByteArray _propertiesBlob, + const QByteArray &_propertiesBlob, const QList &_relatedCards, const QList &_reverseRelatedCards, SetToPrintingsMap _sets, @@ -95,7 +61,7 @@ CardInfo::CardInfo(const QString &_name, QString _simpleName, QSet _altNames) : name(_name), simpleName(std::move(_simpleName)), text(_text), isToken(_isToken), - propertiesBlob(std::move(_propertiesBlob)), relatedCards(_relatedCards), + properties(LazyPropertiesHash(_propertiesBlob)), relatedCards(_relatedCards), reverseRelatedCards(_reverseRelatedCards), setsToPrintings(std::move(_sets)), uiAttributes(_uiAttributes), altNames(std::move(_altNames)) { @@ -113,14 +79,14 @@ CardInfoPtr CardInfo::newInstance(const QString &_name) CardInfoPtr CardInfo::newInstance(const QString &_name, const QString &_text, bool _isToken, - QHash _properties, + const QHash &_properties, const QList &_relatedCards, const QList &_reverseRelatedCards, SetToPrintingsMap _sets, const UiAttributes _uiAttributes) { - CardInfoPtr ptr(new CardInfo(_name, _text, _isToken, std::move(_properties), _relatedCards, _reverseRelatedCards, - _sets, _uiAttributes)); + CardInfoPtr ptr( + new CardInfo(_name, _text, _isToken, _properties, _relatedCards, _reverseRelatedCards, _sets, _uiAttributes)); ptr->setSmartPointer(ptr); for (const auto &printings : _sets) { @@ -203,15 +169,13 @@ void CardInfo::addToSet(const CardSetPtr &_set, const PrintingInfo &_info) void CardInfo::combineLegalities(const QHash &props) { - ensurePropertiesLoaded(); QHashIterator it(props); while (it.hasNext()) { it.next(); if (it.key().startsWith("format-")) { - propertiesCache.insert(it.key(), it.value()); + properties.insert(it.key(), it.value()); } } - propertiesBlob = serializeProperties(propertiesCache); emit cardInfoChanged(smartThis); } diff --git a/libcockatrice_card/libcockatrice/card/card_info.h b/libcockatrice_card/libcockatrice/card/card_info.h index a5c208893..392dc3849 100644 --- a/libcockatrice_card/libcockatrice/card/card_info.h +++ b/libcockatrice_card/libcockatrice/card/card_info.h @@ -2,6 +2,7 @@ #define CARD_INFO_H #include "format/format_legality_rules.h" +#include "lazy_properties_hash.h" #include "printing/printing_info.h" #include @@ -75,19 +76,8 @@ private: QString simpleName; ///< Simplified name for fuzzy matching. QString text; ///< Text description or rules text of the card. bool isToken; ///< Whether this card is a token or not. - // Properties are stored as a pre-serialized blob (cheap to load) and the - // QHash is materialized on first query, so database load avoids - // constructing thousands of QStrings per card. - mutable QByteArray propertiesBlob; ///< Serialized properties (load form). - mutable QHash propertiesCache; ///< Materialized properties (query form). - mutable bool propertiesLoaded = false; ///< Whether propertiesCache is valid. - mutable QMutex propertiesMutex; ///< Guards lazy materialization. - /** - * @brief Materializes propertiesCache from propertiesBlob if not already done. - * Safe to call from const getters (members are mutable). - */ - void ensurePropertiesLoaded() const; + LazyPropertiesHash properties; ///< Key-value store of dynamic card properties. QList relatedCards; ///< Forward references to related cards. QList reverseRelatedCards; ///< Cards that refer back to this card. @@ -114,7 +104,7 @@ public: explicit CardInfo(const QString &_name, const QString &_text, bool _isToken, - QHash _properties, + const QHash &_properties, const QList &_relatedCards, const QList &_reverseRelatedCards, SetToPrintingsMap _sets, @@ -144,7 +134,7 @@ public: explicit CardInfo(const QString &_name, const QString &_text, bool _isToken, - QByteArray _propertiesBlob, + const QByteArray &_propertiesBlob, const QList &_relatedCards, const QList &_reverseRelatedCards, SetToPrintingsMap _sets, @@ -161,7 +151,7 @@ public: */ CardInfo(const CardInfo &other) : QObject(other.parent()), name(other.name), simpleName(other.simpleName), text(other.text), - isToken(other.isToken), propertiesBlob(other.propertiesBlob), relatedCards(other.relatedCards), + isToken(other.isToken), properties(other.properties), relatedCards(other.relatedCards), reverseRelatedCards(other.reverseRelatedCards), reverseRelatedCardsToMe(other.reverseRelatedCardsToMe), setsToPrintings(other.setsToPrintings), uiAttributes(other.uiAttributes), setsNames(other.setsNames), altNames(other.altNames) @@ -194,7 +184,7 @@ public: static CardInfoPtr newInstance(const QString &_name, const QString &_text, bool _isToken, - QHash _properties, + const QHash &_properties, const QList &_relatedCards, const QList &_reverseRelatedCards, SetToPrintingsMap _sets, diff --git a/libcockatrice_card/libcockatrice/card/database/card_database_cache.cpp b/libcockatrice_card/libcockatrice/card/database/card_database_cache.cpp index 3985889b7..2b27f50f8 100644 --- a/libcockatrice_card/libcockatrice/card/database/card_database_cache.cpp +++ b/libcockatrice_card/libcockatrice/card/database/card_database_cache.cpp @@ -119,7 +119,7 @@ PrintingInfo readPrinting(QDataStream &in, const SetNameMap &sets) QByteArray propsBlob = readHashBlob(in); auto set = sets.value(setName); - return PrintingInfo(set, propsBlob); + return PrintingInfo(set, LazyPropertiesHash(propsBlob)); } // ---- CardSet --------------------------------------------------------------- diff --git a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp index f3aac7809..64202ab21 100644 --- a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp @@ -248,7 +248,7 @@ void CockatriceXml3Parser::loadCardsFromXml(QXmlStreamReader &xml) if (attrs.hasAttribute("rarity")) { printingProps.insert("rarity", attrs.value("rarity").toString()); } - PrintingInfo setInfo(set, printingProps); + PrintingInfo setInfo(set, LazyPropertiesHash(printingProps)); _sets[setName].append(setInfo); } // related cards diff --git a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp index 8649bbfaf..ec460d685 100644 --- a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp @@ -322,7 +322,7 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml) } printingProps.insert(attrName, attr.value().toString()); } - PrintingInfo printingInfo(set, printingProps); + PrintingInfo printingInfo(set, LazyPropertiesHash(printingProps)); // This is very much a hack and not the right place to // put this check, as it requires a reload of Cockatrice diff --git a/libcockatrice_card/libcockatrice/card/lazy_properties_hash.cpp b/libcockatrice_card/libcockatrice/card/lazy_properties_hash.cpp new file mode 100644 index 000000000..4d903a3bd --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/lazy_properties_hash.cpp @@ -0,0 +1,95 @@ +#include "lazy_properties_hash.h" + +#include + +LazyPropertiesHash::LazyPropertiesHash() : isMaterialized(true) +{ +} + +LazyPropertiesHash::LazyPropertiesHash(const QByteArray &blob) : blob(blob) +{ +} + +LazyPropertiesHash::LazyPropertiesHash(const QHash &properties) + : properties(properties), isMaterialized(true) +{ +} + +LazyPropertiesHash::LazyPropertiesHash(const LazyPropertiesHash &other) +{ + // since we do not allow dematerialization, we only need to lock if not materialized yet + if (other.isMaterialized) { + blob = other.blob; + properties = other.properties; + isMaterialized = true; + } else { + QMutexLocker lock(&other.propertiesMutex); + blob = other.blob; + properties = other.properties; + isMaterialized = false; + } +} + +LazyPropertiesHash &LazyPropertiesHash::operator=(const LazyPropertiesHash &other) +{ + if (this == &other) { + return *this; + } + + // since we do not allow dematerialization, we only need to lock if not materialized yet + if (other.isMaterialized) { + blob = other.blob; + properties = other.properties; + isMaterialized = true; + } else { + QMutexLocker lock(&other.propertiesMutex); + blob = other.blob; + properties = other.properties; + isMaterialized = false; + } + + return *this; +} + +void LazyPropertiesHash::ensureMaterialized() const +{ + QMutexLocker lock(&propertiesMutex); + + if (isMaterialized) { + return; + } + + if (!blob.isEmpty()) { + QDataStream in(blob); + in.setVersion(QDataStream::Qt_6_4); + in >> properties; + } + + blob.clear(); + + isMaterialized = true; +} + +QString LazyPropertiesHash::value(const QString &key) const +{ + ensureMaterialized(); + return properties.value(key); +} + +bool LazyPropertiesHash::insert(const QString &key, const QString &value) +{ + ensureMaterialized(); + + if (value == properties.value(key)) { + return false; + } + + properties.insert(key, value); + return true; +} + +const QHash &LazyPropertiesHash::getProperties() const +{ + ensureMaterialized(); + return properties; +} diff --git a/libcockatrice_card/libcockatrice/card/lazy_properties_hash.h b/libcockatrice_card/libcockatrice/card/lazy_properties_hash.h new file mode 100644 index 000000000..0ed8c215b --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/lazy_properties_hash.h @@ -0,0 +1,74 @@ +#ifndef COCKATRICE_LAZY_PROPERTIES_HASH_H +#define COCKATRICE_LAZY_PROPERTIES_HASH_H + +#include +#include + +/** + * @brief A property map that can lazily deserialize blobs to avoid loading overhead. + * + * Properties are stored as a pre-serialized blob (cheap to load), and the QString is materialized on the first query, + * so the database load avoids constructing thousands of QString per card. + * + * Once the properties are materialized, it cannot be unmaterialized. + * If you want to reset the properties to an unmaterialized state, you should create a new LazyPropertiesHash. + */ +class LazyPropertiesHash +{ + + mutable QByteArray blob; ///< Serialized properties (load form). + mutable QHash properties; ///< Materialized properties (query form). + mutable QMutex propertiesMutex; ///< Guards lazy materialization. + mutable bool isMaterialized = false; ///< Whether propertiesCache is valid. + + /** + * @brief Materializes properties from blob if not already done. Clears blob afterward. + * Safe to call from const getters (members are mutable). + */ + void ensureMaterialized() const; + +public: + /** + * @brief Default constructor. + */ + LazyPropertiesHash(); + + /** + * @brief Creates an unmaterialized LazyPropertiesHash + * @param blob The pre-serialized blob + */ + explicit LazyPropertiesHash(const QByteArray &blob); + + /** + * @brief Creates an already-materialized LazyPropertiesHash + * @param properties The properties + */ + explicit LazyPropertiesHash(const QHash &properties); + + // Override copy constructor and copy-assignment because mutex isn't copiable + LazyPropertiesHash(const LazyPropertiesHash &other); + LazyPropertiesHash &operator=(const LazyPropertiesHash &other); + + /** + * @brief Gets the value from the materialized properties hash + * @param key The key + * @return The value, or an empty string if the key is not present + */ + QString value(const QString &key) const; + + /** + * @brief Inserts a value into the materialized properties hash + * @param key The key + * @param value The value to insert + * @return True if a new value was inserted; false if the new value is the same as the existing value + */ + bool insert(const QString &key, const QString &value); + + /** + * @brief Gets a view of the materialized properties hash. + * @return The properties hash + */ + const QHash &getProperties() const; +}; + +#endif // COCKATRICE_LAZY_PROPERTIES_HASH_H diff --git a/libcockatrice_card/libcockatrice/card/printing/printing_info.cpp b/libcockatrice_card/libcockatrice/card/printing/printing_info.cpp index 49086f8b7..3d185583a 100644 --- a/libcockatrice_card/libcockatrice/card/printing/printing_info.cpp +++ b/libcockatrice_card/libcockatrice/card/printing/printing_info.cpp @@ -5,37 +5,14 @@ #include #include -PrintingInfo::PrintingInfo(const CardSetPtr &_set, const QHash &_properties) - : set(_set), propertiesCache(_properties), propertiesLoaded(true) +PrintingInfo::PrintingInfo(const CardSetPtr &_set, const LazyPropertiesHash &_properties) + : set(_set), properties(_properties) { } -PrintingInfo::PrintingInfo(const CardSetPtr &_set, const QByteArray &_blob) : set(_set), propertiesBlob(_blob) -{ -} - -void PrintingInfo::ensurePropertiesLoaded() const -{ - QMutexLocker lock(propertiesMutex.data()); - if (propertiesLoaded) { - return; - } - propertiesCache.clear(); - if (!propertiesBlob.isEmpty()) { - QDataStream in(propertiesBlob); - in.setVersion(QDataStream::Qt_6_4); - in >> propertiesCache; - } - propertiesLoaded = true; -} - void PrintingInfo::setProperty(const QString &_name, const QString &_value) { - ensurePropertiesLoaded(); - if (propertiesCache.value(_name) == _value) { - return; - } - propertiesCache.insert(_name, _value); + properties.insert(_name, _value); } /** diff --git a/libcockatrice_card/libcockatrice/card/printing/printing_info.h b/libcockatrice_card/libcockatrice/card/printing/printing_info.h index 70093b686..4d174dc41 100644 --- a/libcockatrice_card/libcockatrice/card/printing/printing_info.h +++ b/libcockatrice_card/libcockatrice/card/printing/printing_info.h @@ -2,12 +2,11 @@ #define COCKATRICE_PRINTING_INFO_H #include "../set/card_set.h" +#include "libcockatrice/card/lazy_properties_hash.h" #include #include -#include #include -#include class PrintingInfo; @@ -34,15 +33,7 @@ public: * @param _set The set this printing belongs to (defaults to null). * @param _properties The printing properties (defaults to empty) */ - explicit PrintingInfo(const CardSetPtr &_set = nullptr, const QHash &_properties = {}); - - /** - * @brief Constructs a PrintingInfo associated with a specific set. - * - * @param _set The set this printing belongs to (defaults to null). - * @param _blob The serialized properties (as written by the cache writer). - */ - explicit PrintingInfo(const CardSetPtr &_set, const QByteArray &_blob); + explicit PrintingInfo(const CardSetPtr &_set = nullptr, const LazyPropertiesHash &_properties = {}); /** * @brief Destroys the PrintingInfo. @@ -76,18 +67,8 @@ public: } private: - CardSetPtr set; ///< The set this variation belongs to. - - // Properties are stored as a pre-serialized blob (cheap to load) and the - // QHash is materialized on first query. This avoids constructing - // thousands of QStrings per card at database-load time. - mutable QByteArray propertiesBlob; ///< Serialized properties (load form). - mutable QHash propertiesCache; ///< Materialized properties (query form). - mutable bool propertiesLoaded = false; ///< Whether propertiesCache is valid. - mutable QSharedPointer propertiesMutex = - QSharedPointer::create(); ///< Guards lazy materialization. - - void ensurePropertiesLoaded() const; + CardSetPtr set; ///< The set this variation belongs to. + LazyPropertiesHash properties; ///< Key-value store for variation-specific attributes. public: /** @@ -112,8 +93,7 @@ public: [[nodiscard]] const QHash &getPropertiesHash() const { - ensurePropertiesLoaded(); - return propertiesCache; + return properties.getProperties(); } /** @@ -124,7 +104,7 @@ public: */ [[nodiscard]] QString getProperty(const QString &propertyName) const { - return getPropertiesHash().value(propertyName); + return properties.value(propertyName); } /** diff --git a/oracle/src/oracleimporter.cpp b/oracle/src/oracleimporter.cpp index a9008c4da..85859e7a2 100644 --- a/oracle/src/oracleimporter.cpp +++ b/oracle/src/oracleimporter.cpp @@ -316,7 +316,7 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList } } - PrintingInfo printingInfo(currentSet, printingProps); + PrintingInfo printingInfo(currentSet, LazyPropertiesHash(printingProps)); QString numComponent; const QString numProperty = printingInfo.getProperty("num");