mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-07-28 20:00:24 -07:00
[Card Database] Improve loading times through binary cache (#7051)
* [Card Database] Improve loading times through binary cache Took 10 minutes Took 9 minutes Took 16 seconds * [Card Database] Remove lib qt include Took 18 minutes Took 14 seconds * Downgrade to 6.3 datastream Took 5 minutes * go up to 6.4 datastream Took 1 minute * Address comments * Small bug fixes Took 20 minutes Took 10 seconds * More fixes. Took 4 minutes Took 4 seconds * Even more fixes. Took 11 minutes Took 4 seconds * More fixes. Took 6 minutes Took 26 seconds Took 8 minutes * Namespace instead of class Took 6 minutes --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
parent
4bb8831531
commit
749223c2dc
31 changed files with 1604 additions and 106 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -6,6 +6,7 @@ mysql.cnf
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.idea/
|
.idea/
|
||||||
*.aps
|
*.aps
|
||||||
|
*.cache
|
||||||
cmake-build*
|
cmake-build*
|
||||||
preferences
|
preferences
|
||||||
compile_commands.json
|
compile_commands.json
|
||||||
|
|
|
||||||
|
|
@ -541,6 +541,12 @@ MainWindow::MainWindow(QWidget *parent)
|
||||||
|
|
||||||
void MainWindow::startupConfigCheck()
|
void MainWindow::startupConfigCheck()
|
||||||
{
|
{
|
||||||
|
// checkUnknownSets() is intentionally deferred from the card database load
|
||||||
|
// (which runs in main() before MainWindow exists) so that
|
||||||
|
// cardDatabaseNewSetsFound / cardDatabaseAllNewSetsEnabled have live
|
||||||
|
// receivers when emitted.
|
||||||
|
CardDatabaseManager::getInstance()->checkUnknownSets();
|
||||||
|
|
||||||
if (SettingsCache::instance().debug().getLocalGameOnStartup()) {
|
if (SettingsCache::instance().debug().getLocalGameOnStartup()) {
|
||||||
LocalGameOptions options;
|
LocalGameOptions options;
|
||||||
options.numberPlayers = SettingsCache::instance().debug().getLocalGamePlayerCount();
|
options.numberPlayers = SettingsCache::instance().debug().getLocalGamePlayerCount();
|
||||||
|
|
@ -571,8 +577,6 @@ void MainWindow::startupConfigCheck()
|
||||||
<< "differs, assuming first start after update";
|
<< "differs, assuming first start after update";
|
||||||
if (SettingsCache::instance().updates().getNotifyAboutNewVersion()) {
|
if (SettingsCache::instance().updates().getNotifyAboutNewVersion()) {
|
||||||
alertForcedOracleRun(VERSION_STRING, true);
|
alertForcedOracleRun(VERSION_STRING, true);
|
||||||
} else {
|
|
||||||
const auto reloadOk0 = QtConcurrent::run([] { CardDatabaseManager::getInstance()->loadCardDatabases(); });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
qCInfo(WindowMainStartupShortcutsLog) << "Migrating shortcuts after update detected.";
|
qCInfo(WindowMainStartupShortcutsLog) << "Migrating shortcuts after update detected.";
|
||||||
|
|
@ -629,8 +633,6 @@ void MainWindow::startupConfigCheck()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto reloadOk1 = QtConcurrent::run([] { CardDatabaseManager::getInstance()->loadCardDatabases(); });
|
|
||||||
|
|
||||||
// Run the tips dialog only on subsequent startups.
|
// Run the tips dialog only on subsequent startups.
|
||||||
// On the first run after an install/update the startup is already crowded enough
|
// On the first run after an install/update the startup is already crowded enough
|
||||||
if (tip->successfulInit && SettingsCache::instance().personal().getShowTipsOnStartup() &&
|
if (tip->successfulInit && SettingsCache::instance().personal().getShowTipsOnStartup() &&
|
||||||
|
|
|
||||||
|
|
@ -258,6 +258,19 @@ int main(int argc, char *argv[])
|
||||||
|
|
||||||
qCInfo(MainLog) << "Starting main program";
|
qCInfo(MainLog) << "Starting main program";
|
||||||
|
|
||||||
|
// Front-load the card database before constructing the main window. The
|
||||||
|
// binary-cache read is cheap when uncontended (~1s); doing it here -- while no
|
||||||
|
// GUI work yet competes for CPU -- avoids the multi-second, GUI-freezing
|
||||||
|
// contention that happens when the load runs alongside window construction.
|
||||||
|
// The CardDatabaseModel populates from the already-loaded data in its
|
||||||
|
// constructor, so the window appears fully populated with no startup lag.
|
||||||
|
// Note: checkUnknownSets() is deferred from the initial load to
|
||||||
|
// MainWindow::startupConfigCheck() so that
|
||||||
|
// cardDatabaseNewSetsFound / cardDatabaseAllNewSetsEnabled have live
|
||||||
|
// receivers when emitted. Subsequent reloads (e.g. path changes) call
|
||||||
|
// checkUnknownSets() directly from the loader after the first load.
|
||||||
|
CardDatabaseManager::getInstance()->loadCardDatabases();
|
||||||
|
|
||||||
MainWindow ui;
|
MainWindow ui;
|
||||||
if (parser.isSet("connect")) {
|
if (parser.isSet("connect")) {
|
||||||
ui.setConnectTo(parser.value("connect"));
|
ui.setConnectTo(parser.value("connect"));
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ add_library(
|
||||||
libcockatrice/card/card_info.cpp
|
libcockatrice/card/card_info.cpp
|
||||||
libcockatrice/card/card_info_comparator.cpp
|
libcockatrice/card/card_info_comparator.cpp
|
||||||
libcockatrice/card/database/card_database.cpp
|
libcockatrice/card/database/card_database.cpp
|
||||||
|
libcockatrice/card/database/card_database_cache.cpp
|
||||||
libcockatrice/card/database/card_database_loader.cpp
|
libcockatrice/card/database/card_database_loader.cpp
|
||||||
libcockatrice/card/database/card_database_manager.cpp
|
libcockatrice/card/database/card_database_manager.cpp
|
||||||
libcockatrice/card/database/card_database_querier.cpp
|
libcockatrice/card/database/card_database_querier.cpp
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,10 @@
|
||||||
#include "relation/card_relation.h"
|
#include "relation/card_relation.h"
|
||||||
#include "set/card_set.h"
|
#include "set/card_set.h"
|
||||||
|
|
||||||
|
#include <QDataStream>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
|
#include <QMutex>
|
||||||
|
#include <QMutexLocker>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
#include <QSharedPointer>
|
#include <QSharedPointer>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
@ -19,6 +22,57 @@ class CardInfo;
|
||||||
|
|
||||||
using CardInfoPtr = QSharedPointer<CardInfo>;
|
using CardInfoPtr = QSharedPointer<CardInfo>;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
QByteArray serializeProperties(const QVariantHash &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 QVariantHash &CardInfo::getPropertiesHash() const
|
||||||
|
{
|
||||||
|
ensurePropertiesLoaded();
|
||||||
|
return propertiesCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CardInfo::setProperty(const QString &_name, const QString &_value)
|
||||||
|
{
|
||||||
|
ensurePropertiesLoaded();
|
||||||
|
if (propertiesCache.value(_name).toString() == _value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
propertiesCache.insert(_name, _value);
|
||||||
|
propertiesBlob = serializeProperties(propertiesCache);
|
||||||
|
emit cardInfoChanged(smartThis);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CardInfo::setProperties(const QVariantHash &_props)
|
||||||
|
{
|
||||||
|
ensurePropertiesLoaded();
|
||||||
|
propertiesCache = _props;
|
||||||
|
propertiesBlob = serializeProperties(propertiesCache);
|
||||||
|
emit cardInfoChanged(smartThis);
|
||||||
|
}
|
||||||
|
|
||||||
CardInfo::CardInfo(const QString &_name,
|
CardInfo::CardInfo(const QString &_name,
|
||||||
const QString &_text,
|
const QString &_text,
|
||||||
bool _isToken,
|
bool _isToken,
|
||||||
|
|
@ -27,14 +81,39 @@ CardInfo::CardInfo(const QString &_name,
|
||||||
const QList<CardRelation *> &_reverseRelatedCards,
|
const QList<CardRelation *> &_reverseRelatedCards,
|
||||||
SetToPrintingsMap _sets,
|
SetToPrintingsMap _sets,
|
||||||
const UiAttributes _uiAttributes)
|
const UiAttributes _uiAttributes)
|
||||||
: name(_name), text(_text), isToken(_isToken), properties(std::move(_properties)), relatedCards(_relatedCards),
|
: name(_name), text(_text), isToken(_isToken), relatedCards(_relatedCards),
|
||||||
reverseRelatedCards(_reverseRelatedCards), setsToPrintings(std::move(_sets)), uiAttributes(_uiAttributes)
|
reverseRelatedCards(_reverseRelatedCards), setsToPrintings(std::move(_sets)), uiAttributes(_uiAttributes)
|
||||||
{
|
{
|
||||||
|
propertiesCache = std::move(_properties);
|
||||||
|
propertiesBlob = serializeProperties(propertiesCache);
|
||||||
|
propertiesLoaded = true;
|
||||||
|
|
||||||
simpleName = CardInfo::simplifyName(name);
|
simpleName = CardInfo::simplifyName(name);
|
||||||
|
|
||||||
refreshCachedSets();
|
refreshCachedSets();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CardInfo::CardInfo(const QString &_name,
|
||||||
|
const QString &_text,
|
||||||
|
bool _isToken,
|
||||||
|
QByteArray _propertiesBlob,
|
||||||
|
const QList<CardRelation *> &_relatedCards,
|
||||||
|
const QList<CardRelation *> &_reverseRelatedCards,
|
||||||
|
SetToPrintingsMap _sets,
|
||||||
|
const UiAttributes _uiAttributes,
|
||||||
|
QString _simpleName,
|
||||||
|
QSet<QString> _altNames)
|
||||||
|
: name(_name), simpleName(std::move(_simpleName)), text(_text), isToken(_isToken),
|
||||||
|
propertiesBlob(std::move(_propertiesBlob)), relatedCards(_relatedCards),
|
||||||
|
reverseRelatedCards(_reverseRelatedCards), setsToPrintings(std::move(_sets)), uiAttributes(_uiAttributes),
|
||||||
|
altNames(std::move(_altNames))
|
||||||
|
{
|
||||||
|
// propertiesBlob is materialized lazily on first query; simpleName and
|
||||||
|
// altNames are supplied by the caller (binary cache). Only the set-name
|
||||||
|
// display list still depends on the current enabled-set state.
|
||||||
|
refreshCachedSetNames();
|
||||||
|
}
|
||||||
|
|
||||||
CardInfoPtr CardInfo::newInstance(const QString &_name)
|
CardInfoPtr CardInfo::newInstance(const QString &_name)
|
||||||
{
|
{
|
||||||
return newInstance(_name, "", false, {}, {}, {}, {}, {});
|
return newInstance(_name, "", false, {}, {}, {}, {}, {});
|
||||||
|
|
@ -63,6 +142,35 @@ CardInfoPtr CardInfo::newInstance(const QString &_name,
|
||||||
return ptr;
|
return ptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CardInfoPtr CardInfo::newInstance(const QString &_name,
|
||||||
|
const QString &_text,
|
||||||
|
bool _isToken,
|
||||||
|
QByteArray _propertiesBlob,
|
||||||
|
const QList<CardRelation *> &_relatedCards,
|
||||||
|
const QList<CardRelation *> &_reverseRelatedCards,
|
||||||
|
SetToPrintingsMap _sets,
|
||||||
|
const UiAttributes _uiAttributes,
|
||||||
|
QString _simpleName,
|
||||||
|
QSet<QString> _altNames,
|
||||||
|
bool _appendToSets)
|
||||||
|
{
|
||||||
|
CardInfoPtr ptr(new CardInfo(_name, _text, _isToken, std::move(_propertiesBlob), _relatedCards,
|
||||||
|
_reverseRelatedCards, _sets, _uiAttributes, std::move(_simpleName),
|
||||||
|
std::move(_altNames)));
|
||||||
|
ptr->setSmartPointer(ptr);
|
||||||
|
|
||||||
|
if (_appendToSets) {
|
||||||
|
for (const auto &printings : _sets) {
|
||||||
|
for (const PrintingInfo &printing : printings) {
|
||||||
|
printing.getSet()->append(ptr);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
QString CardInfo::getCorrectedName() const
|
QString CardInfo::getCorrectedName() const
|
||||||
{
|
{
|
||||||
// remove all the characters reserved in windows file paths,
|
// remove all the characters reserved in windows file paths,
|
||||||
|
|
@ -104,13 +212,16 @@ void CardInfo::addToSet(const CardSetPtr &_set, const PrintingInfo &_info)
|
||||||
|
|
||||||
void CardInfo::combineLegalities(const QVariantHash &props)
|
void CardInfo::combineLegalities(const QVariantHash &props)
|
||||||
{
|
{
|
||||||
|
ensurePropertiesLoaded();
|
||||||
QHashIterator<QString, QVariant> it(props);
|
QHashIterator<QString, QVariant> it(props);
|
||||||
while (it.hasNext()) {
|
while (it.hasNext()) {
|
||||||
it.next();
|
it.next();
|
||||||
if (it.key().startsWith("format-")) {
|
if (it.key().startsWith("format-")) {
|
||||||
smartThis->setProperty(it.key(), it.value().toString());
|
propertiesCache.insert(it.key(), it.value());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
propertiesBlob = serializeProperties(propertiesCache);
|
||||||
|
emit cardInfoChanged(smartThis);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CardInfo::refreshCachedSets()
|
void CardInfo::refreshCachedSets()
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
#include <QLoggingCategory>
|
#include <QLoggingCategory>
|
||||||
#include <QMap>
|
#include <QMap>
|
||||||
#include <QMetaType>
|
#include <QMetaType>
|
||||||
|
#include <QMutex>
|
||||||
#include <QSharedPointer>
|
#include <QSharedPointer>
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
@ -69,12 +70,25 @@ private:
|
||||||
* @anchor PrivateCardProperties
|
* @anchor PrivateCardProperties
|
||||||
*/
|
*/
|
||||||
///@{
|
///@{
|
||||||
CardInfoPtr smartThis; ///< Smart pointer to self for safe cross-references.
|
CardInfoPtr smartThis; ///< Smart pointer to self for safe cross-references.
|
||||||
QString name; ///< Full name of the card.
|
QString name; ///< Full name of the card.
|
||||||
QString simpleName; ///< Simplified name for fuzzy matching.
|
QString simpleName; ///< Simplified name for fuzzy matching.
|
||||||
QString text; ///< Text description or rules text of the card.
|
QString text; ///< Text description or rules text of the card.
|
||||||
bool isToken; ///< Whether this card is a token or not.
|
bool isToken; ///< Whether this card is a token or not.
|
||||||
QVariantHash properties; ///< Key-value store of dynamic card properties.
|
// Properties are stored as a pre-serialized blob (cheap to load) and the
|
||||||
|
// QVariantHash is materialized on first query, so database load avoids
|
||||||
|
// constructing thousands of QVariants per card.
|
||||||
|
mutable QByteArray propertiesBlob; ///< Serialized properties (load form).
|
||||||
|
mutable QVariantHash 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;
|
||||||
|
|
||||||
QList<CardRelation *> relatedCards; ///< Forward references to related cards.
|
QList<CardRelation *> relatedCards; ///< Forward references to related cards.
|
||||||
QList<CardRelation *> reverseRelatedCards; ///< Cards that refer back to this card.
|
QList<CardRelation *> reverseRelatedCards; ///< Cards that refer back to this card.
|
||||||
QList<CardRelation *> reverseRelatedCardsToMe; ///< Cards that consider this card as related.
|
QList<CardRelation *> reverseRelatedCardsToMe; ///< Cards that consider this card as related.
|
||||||
|
|
@ -106,6 +120,38 @@ public:
|
||||||
SetToPrintingsMap _sets,
|
SetToPrintingsMap _sets,
|
||||||
UiAttributes _uiAttributes);
|
UiAttributes _uiAttributes);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Constructs a CardInfo from a cache snapshot with precomputed derived
|
||||||
|
* state.
|
||||||
|
*
|
||||||
|
* Used by the binary cache reader to skip recomputing @p _simpleName and
|
||||||
|
* @p _altNames (which otherwise require a Unicode normalization and a full
|
||||||
|
* printing scan). Properties are supplied as a pre-serialized @p _propertiesBlob
|
||||||
|
* so the QVariantHash is not built at load time (it is materialized on first
|
||||||
|
* query).
|
||||||
|
*
|
||||||
|
* @param _name The card name.
|
||||||
|
* @param _text Rules text or description of the card.
|
||||||
|
* @param _isToken Token flag.
|
||||||
|
* @param _propertiesBlob Pre-serialized properties blob (as written by the cache).
|
||||||
|
* @param _relatedCards Forward relationships.
|
||||||
|
* @param _reverseRelatedCards Reverse relationships.
|
||||||
|
* @param _sets Printing information per set.
|
||||||
|
* @param _uiAttributes Attributes that affect display and game logic.
|
||||||
|
* @param _simpleName Precomputed simplified name.
|
||||||
|
* @param _altNames Precomputed alternate names.
|
||||||
|
*/
|
||||||
|
explicit CardInfo(const QString &_name,
|
||||||
|
const QString &_text,
|
||||||
|
bool _isToken,
|
||||||
|
QByteArray _propertiesBlob,
|
||||||
|
const QList<CardRelation *> &_relatedCards,
|
||||||
|
const QList<CardRelation *> &_reverseRelatedCards,
|
||||||
|
SetToPrintingsMap _sets,
|
||||||
|
UiAttributes _uiAttributes,
|
||||||
|
QString _simpleName,
|
||||||
|
QSet<QString> _altNames);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Copy constructor for CardInfo.
|
* @brief Copy constructor for CardInfo.
|
||||||
*
|
*
|
||||||
|
|
@ -115,7 +161,7 @@ public:
|
||||||
*/
|
*/
|
||||||
CardInfo(const CardInfo &other)
|
CardInfo(const CardInfo &other)
|
||||||
: QObject(other.parent()), name(other.name), simpleName(other.simpleName), text(other.text),
|
: QObject(other.parent()), name(other.name), simpleName(other.simpleName), text(other.text),
|
||||||
isToken(other.isToken), properties(other.properties), relatedCards(other.relatedCards),
|
isToken(other.isToken), propertiesBlob(other.propertiesBlob), relatedCards(other.relatedCards),
|
||||||
reverseRelatedCards(other.reverseRelatedCards), reverseRelatedCardsToMe(other.reverseRelatedCardsToMe),
|
reverseRelatedCards(other.reverseRelatedCards), reverseRelatedCardsToMe(other.reverseRelatedCardsToMe),
|
||||||
setsToPrintings(other.setsToPrintings), uiAttributes(other.uiAttributes), setsNames(other.setsNames),
|
setsToPrintings(other.setsToPrintings), uiAttributes(other.uiAttributes), setsNames(other.setsNames),
|
||||||
altNames(other.altNames)
|
altNames(other.altNames)
|
||||||
|
|
@ -154,6 +200,38 @@ public:
|
||||||
SetToPrintingsMap _sets,
|
SetToPrintingsMap _sets,
|
||||||
UiAttributes _uiAttributes);
|
UiAttributes _uiAttributes);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Creates a new instance from a cache snapshot with precomputed
|
||||||
|
* derived state.
|
||||||
|
*
|
||||||
|
* @param _name Name of the card.
|
||||||
|
* @param _text Rules text or description.
|
||||||
|
* @param _isToken Token flag.
|
||||||
|
* @param _propertiesBlob Pre-serialized properties blob (as written by the cache).
|
||||||
|
* @param _relatedCards Forward relationships.
|
||||||
|
* @param _reverseRelatedCards Reverse relationships.
|
||||||
|
* @param _sets Printing information per set.
|
||||||
|
* @param _uiAttributes Attributes that affect display and game logic.
|
||||||
|
* @param _simpleName Precomputed simplified name.
|
||||||
|
* @param _altNames Precomputed alternate names.
|
||||||
|
* @param _appendToSets When true (default), the card is appended to each of
|
||||||
|
* its CardSets. Pass false when building cards in parallel so the
|
||||||
|
* (non-thread-safe) set membership is populated in a later
|
||||||
|
* single-threaded pass.
|
||||||
|
* @return Shared pointer to the new CardInfo instance.
|
||||||
|
*/
|
||||||
|
static CardInfoPtr newInstance(const QString &_name,
|
||||||
|
const QString &_text,
|
||||||
|
bool _isToken,
|
||||||
|
QByteArray _propertiesBlob,
|
||||||
|
const QList<CardRelation *> &_relatedCards,
|
||||||
|
const QList<CardRelation *> &_reverseRelatedCards,
|
||||||
|
SetToPrintingsMap _sets,
|
||||||
|
UiAttributes _uiAttributes,
|
||||||
|
QString _simpleName,
|
||||||
|
QSet<QString> _altNames,
|
||||||
|
bool _appendToSets = true);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Clones the current CardInfo instance.
|
* @brief Clones the current CardInfo instance.
|
||||||
*
|
*
|
||||||
|
|
@ -208,20 +286,32 @@ public:
|
||||||
}
|
}
|
||||||
[[nodiscard]] QStringList getProperties() const
|
[[nodiscard]] QStringList getProperties() const
|
||||||
{
|
{
|
||||||
return properties.keys();
|
return getPropertiesHash().keys();
|
||||||
|
}
|
||||||
|
[[nodiscard]] const QVariantHash &getPropertiesHash() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Stores the pre-serialized properties blob and invalidates the
|
||||||
|
* materialized cache. Used by the binary cache reader so the
|
||||||
|
* QVariantHash is not built at load time.
|
||||||
|
* @param _blob The serialized properties (as written by the cache writer).
|
||||||
|
*/
|
||||||
|
void setPropertiesBlob(QByteArray _blob) const
|
||||||
|
{
|
||||||
|
QMutexLocker lock(&propertiesMutex);
|
||||||
|
propertiesBlob = std::move(_blob);
|
||||||
|
propertiesLoaded = false;
|
||||||
|
propertiesCache.clear();
|
||||||
}
|
}
|
||||||
[[nodiscard]] QString getProperty(const QString &propertyName) const
|
[[nodiscard]] QString getProperty(const QString &propertyName) const
|
||||||
{
|
{
|
||||||
return properties.value(propertyName).toString();
|
return getPropertiesHash().value(propertyName).toString();
|
||||||
}
|
|
||||||
void setProperty(const QString &_name, const QString &_value)
|
|
||||||
{
|
|
||||||
properties.insert(_name, _value);
|
|
||||||
emit cardInfoChanged(smartThis);
|
|
||||||
}
|
}
|
||||||
|
void setProperty(const QString &_name, const QString &_value);
|
||||||
|
void setProperties(const QVariantHash &_props);
|
||||||
[[nodiscard]] bool hasProperty(const QString &propertyName) const
|
[[nodiscard]] bool hasProperty(const QString &propertyName) const
|
||||||
{
|
{
|
||||||
return properties.contains(propertyName);
|
return getPropertiesHash().contains(propertyName);
|
||||||
}
|
}
|
||||||
[[nodiscard]] const SetToPrintingsMap &getSets() const
|
[[nodiscard]] const SetToPrintingsMap &getSets() const
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ CardDatabase::CardDatabase(QObject *parent,
|
||||||
{
|
{
|
||||||
qRegisterMetaType<CardInfoPtr>("CardInfoPtr");
|
qRegisterMetaType<CardInfoPtr>("CardInfoPtr");
|
||||||
qRegisterMetaType<CardInfoPtr>("CardSetPtr");
|
qRegisterMetaType<CardInfoPtr>("CardSetPtr");
|
||||||
|
qRegisterMetaType<CardDatabaseData>("CardDatabaseData");
|
||||||
|
|
||||||
// create loader and wire it up
|
// create loader and wire it up
|
||||||
loader = new CardDatabaseLoader(this, this, pathProvider, prefs, setPriorityController);
|
loader = new CardDatabaseLoader(this, this, pathProvider, prefs, setPriorityController);
|
||||||
|
|
@ -28,6 +29,10 @@ CardDatabase::CardDatabase(QObject *parent,
|
||||||
connect(loader, &CardDatabaseLoader::loadingFailed, this, &CardDatabase::cardDatabaseLoadingFailed);
|
connect(loader, &CardDatabaseLoader::loadingFailed, this, &CardDatabase::cardDatabaseLoadingFailed);
|
||||||
connect(loader, &CardDatabaseLoader::newSetsFound, this, &CardDatabase::cardDatabaseNewSetsFound);
|
connect(loader, &CardDatabaseLoader::newSetsFound, this, &CardDatabase::cardDatabaseNewSetsFound);
|
||||||
connect(loader, &CardDatabaseLoader::allNewSetsEnabled, this, &CardDatabase::cardDatabaseAllNewSetsEnabled);
|
connect(loader, &CardDatabaseLoader::allNewSetsEnabled, this, &CardDatabase::cardDatabaseAllNewSetsEnabled);
|
||||||
|
// swap the finished snapshot into the live database. Uses AutoConnection so
|
||||||
|
// that cross-thread loads are delivered via the event loop while same-thread/
|
||||||
|
// synchronous callers get the swap immediately.
|
||||||
|
connect(loader, &CardDatabaseLoader::databaseDataReady, this, &CardDatabase::swapInDatabaseData);
|
||||||
|
|
||||||
querier = new CardDatabaseQuerier(this, this, prefs);
|
querier = new CardDatabaseQuerier(this, this, prefs);
|
||||||
}
|
}
|
||||||
|
|
@ -66,7 +71,12 @@ void CardDatabase::reloadCardDatabasesAndNotify()
|
||||||
loadCardDatabases();
|
loadCardDatabases();
|
||||||
|
|
||||||
if (loadStatus == Ok) {
|
if (loadStatus == Ok) {
|
||||||
notifyEnabledSetsChanged();
|
checkUnknownSets();
|
||||||
|
// A reload reconstructs the exact same enabled-set state, so the cached
|
||||||
|
// set-name / alt-name data computed during construction is still valid.
|
||||||
|
// Skipping the per-card refresh avoids a full 36k-card recompute that
|
||||||
|
// would otherwise duplicate the work already done while building cards.
|
||||||
|
notifyEnabledSetsChanged(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,13 +87,18 @@ bool CardDatabase::saveCustomTokensToFile()
|
||||||
|
|
||||||
void CardDatabase::refreshCachedReverseRelatedCards()
|
void CardDatabase::refreshCachedReverseRelatedCards()
|
||||||
{
|
{
|
||||||
for (const auto &card : cards) {
|
refreshCachedReverseRelatedCards(cards);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CardDatabase::refreshCachedReverseRelatedCards(CardNameMap &cardMap)
|
||||||
|
{
|
||||||
|
for (const auto &card : cardMap) {
|
||||||
card->resetReverseRelatedCards2Me();
|
card->resetReverseRelatedCards2Me();
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto &card : cards) {
|
for (const auto &card : cardMap) {
|
||||||
for (auto *rel : card->getReverseRelatedCards()) {
|
for (auto *rel : card->getReverseRelatedCards()) {
|
||||||
if (auto target = cards.value(rel->getName())) {
|
if (auto target = cardMap.value(rel->getName())) {
|
||||||
auto *newRel = new CardRelation(card->getName(), rel->getAttachType(), rel->getIsCreateAllExclusion(),
|
auto *newRel = new CardRelation(card->getName(), rel->getAttachType(), rel->getIsCreateAllExclusion(),
|
||||||
rel->getIsVariable(), rel->getDefaultCount(), rel->getIsPersistent(),
|
rel->getIsVariable(), rel->getDefaultCount(), rel->getIsPersistent(),
|
||||||
rel->getIsFaceDown());
|
rel->getIsFaceDown());
|
||||||
|
|
@ -205,11 +220,15 @@ void CardDatabase::markAllSetsAsKnown()
|
||||||
_sets.markAllAsKnown();
|
_sets.markAllAsKnown();
|
||||||
}
|
}
|
||||||
|
|
||||||
void CardDatabase::notifyEnabledSetsChanged()
|
void CardDatabase::notifyEnabledSetsChanged(bool recomputeCachedSets)
|
||||||
{
|
{
|
||||||
// refresh the list of cached set names
|
if (recomputeCachedSets) {
|
||||||
for (const CardInfoPtr &card : cards) {
|
// refresh the list of cached set names / alt names for every card; this is
|
||||||
card->refreshCachedSets();
|
// only needed when the enabled-set state actually changed (e.g. the user
|
||||||
|
// toggled a set in the settings UI).
|
||||||
|
for (const CardInfoPtr &card : cards) {
|
||||||
|
card->refreshCachedSets();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// inform the carddatabasemodels that they need to re-check their list of cards
|
// inform the carddatabasemodels that they need to re-check their list of cards
|
||||||
|
|
@ -220,3 +239,30 @@ void CardDatabase::addFormat(const FormatRulesPtr &format)
|
||||||
{
|
{
|
||||||
formats.insert(format->formatName.toLower(), format);
|
formats.insert(format->formatName.toLower(), format);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CardDatabase::swapInDatabaseData(CardDatabaseData data)
|
||||||
|
{
|
||||||
|
for (const auto &card : cards) {
|
||||||
|
for (auto *rel : card->getRelatedCards()) {
|
||||||
|
rel->deleteLater();
|
||||||
|
}
|
||||||
|
for (auto *rel : card->getReverseRelatedCards()) {
|
||||||
|
rel->deleteLater();
|
||||||
|
}
|
||||||
|
for (auto *rel : card->getReverseRelatedCards2Me()) {
|
||||||
|
rel->deleteLater();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cards = std::move(data.cards);
|
||||||
|
simpleNameCards = std::move(data.simpleNameCards);
|
||||||
|
sets = std::move(data.sets);
|
||||||
|
formats = std::move(data.formats);
|
||||||
|
|
||||||
|
loadStatus = cards.isEmpty() ? NotLoaded : Ok;
|
||||||
|
|
||||||
|
// inform listeners that the whole database was replaced; they should
|
||||||
|
// rebuild from the live containers in a single batch instead of reacting
|
||||||
|
// to individual card additions.
|
||||||
|
emit cardDatabaseReset();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
#define CARDDATABASE_H
|
#define CARDDATABASE_H
|
||||||
|
|
||||||
#include "../set/card_set_list.h"
|
#include "../set/card_set_list.h"
|
||||||
|
#include "card_database_data.h"
|
||||||
#include "card_database_loader.h"
|
#include "card_database_loader.h"
|
||||||
#include "card_database_querier.h"
|
#include "card_database_querier.h"
|
||||||
|
|
||||||
|
|
@ -54,16 +55,17 @@ protected:
|
||||||
CardDatabaseQuerier *querier;
|
CardDatabaseQuerier *querier;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
/**
|
|
||||||
* @brief Check for sets that are unknown and emit signals if needed.
|
|
||||||
*/
|
|
||||||
void checkUnknownSets();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Refreshes the cached reverse-related cards for all cards.
|
* @brief Refreshes the cached reverse-related cards for all cards.
|
||||||
*/
|
*/
|
||||||
void refreshCachedReverseRelatedCards();
|
void refreshCachedReverseRelatedCards();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Refreshes the cached reverse-related cards for the given card map.
|
||||||
|
* @param cardMap Map of cards to process.
|
||||||
|
*/
|
||||||
|
void refreshCachedReverseRelatedCards(CardNameMap &cardMap);
|
||||||
|
|
||||||
/** @brief Mutexes for thread safety. */
|
/** @brief Mutexes for thread safety. */
|
||||||
QBasicMutex *clearDatabaseMutex = new QBasicMutex(), *addCardMutex = new QBasicMutex(),
|
QBasicMutex *clearDatabaseMutex = new QBasicMutex(), *addCardMutex = new QBasicMutex(),
|
||||||
*removeCardMutex = new QBasicMutex();
|
*removeCardMutex = new QBasicMutex();
|
||||||
|
|
@ -127,8 +129,25 @@ public:
|
||||||
/** @brief Marks all sets as known. */
|
/** @brief Marks all sets as known. */
|
||||||
void markAllSetsAsKnown();
|
void markAllSetsAsKnown();
|
||||||
|
|
||||||
/** @brief Notifies listeners that enabled sets changed. */
|
/**
|
||||||
void notifyEnabledSetsChanged();
|
* @brief Notifies listeners that enabled sets changed.
|
||||||
|
* @param recomputeCachedSets When true, every card's cached set-name / alt-name
|
||||||
|
* data is recomputed (needed only when enabled-set state actually
|
||||||
|
* changed). Pass false after a plain reload, where enabled-set state is
|
||||||
|
* unchanged and the cached data is still valid.
|
||||||
|
*/
|
||||||
|
void notifyEnabledSetsChanged(bool recomputeCachedSets = true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check for sets that are unknown and emit signals if needed.
|
||||||
|
*
|
||||||
|
* Called from MainWindow::startupConfigCheck() after signal connections are
|
||||||
|
* live (the front-loaded parse in main() runs before MainWindow exists, so
|
||||||
|
* signals emitted there would have no receivers). May also be called by
|
||||||
|
* the loader via the friend declaration. Has side effects: enables all
|
||||||
|
* sets when none are enabled (first-run), marks all sets as known.
|
||||||
|
*/
|
||||||
|
void checkUnknownSets();
|
||||||
|
|
||||||
ICardSetPriorityController *getPriorityController()
|
ICardSetPriorityController *getPriorityController()
|
||||||
{
|
{
|
||||||
|
|
@ -159,10 +178,26 @@ public slots:
|
||||||
*/
|
*/
|
||||||
bool saveCustomTokensToFile();
|
bool saveCustomTokensToFile();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Replaces the entire contents of the database with the provided
|
||||||
|
* snapshot in a single operation. Intended to be called once a
|
||||||
|
* background load has finished building a CardDatabaseData, so that
|
||||||
|
* observers never see a partially-populated database.
|
||||||
|
* @param data The fully-built snapshot to adopt.
|
||||||
|
*/
|
||||||
|
void swapInDatabaseData(CardDatabaseData data);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
/** @brief Emitted when the card database has finished loading successfully. */
|
/** @brief Emitted when the card database has finished loading successfully. */
|
||||||
void cardDatabaseLoadingFinished();
|
void cardDatabaseLoadingFinished();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Emitted once after the live database contents have been replaced
|
||||||
|
* by a freshly loaded snapshot. Models should rebuild from the
|
||||||
|
* database in a single batch rather than reacting to per-card adds.
|
||||||
|
*/
|
||||||
|
void cardDatabaseReset();
|
||||||
|
|
||||||
/** @brief Emitted when the card database fails to load. */
|
/** @brief Emitted when the card database fails to load. */
|
||||||
void cardDatabaseLoadingFailed();
|
void cardDatabaseLoadingFailed();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,485 @@
|
||||||
|
#include "card_database_cache.h"
|
||||||
|
|
||||||
|
#include "../card_info.h"
|
||||||
|
#include "../format/format_legality_rules.h"
|
||||||
|
#include "../printing/printing_info.h"
|
||||||
|
#include "../relation/card_relation.h"
|
||||||
|
#include "../relation/card_relation_type.h"
|
||||||
|
#include "../set/card_set.h"
|
||||||
|
#include "card_database_loader.h"
|
||||||
|
|
||||||
|
#include <QBuffer>
|
||||||
|
#include <QDataStream>
|
||||||
|
#include <QElapsedTimer>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QSaveFile>
|
||||||
|
#include <QVariantHash>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr quint32 CACHE_MAGIC = 0x43445243; // "CDRC"
|
||||||
|
constexpr quint32 CACHE_VERSION = 1;
|
||||||
|
|
||||||
|
// ---- Primitives -----------------------------------------------------------
|
||||||
|
|
||||||
|
void writeString(QDataStream &out, const QString &s)
|
||||||
|
{
|
||||||
|
out << s;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString readString(QDataStream &in)
|
||||||
|
{
|
||||||
|
QString s;
|
||||||
|
in >> s;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stores a QVariantHash as a single pre-serialized blob. The reader keeps the
|
||||||
|
// blob as-is and materializes the QVariantHash lazily on first query, which is
|
||||||
|
// what removes the allocation storm from database load (see card_info.cpp /
|
||||||
|
// printing_info.cpp).
|
||||||
|
void writeHashBlob(QDataStream &out, const QVariantHash &h)
|
||||||
|
{
|
||||||
|
QByteArray blob;
|
||||||
|
QDataStream blobOut(&blob, QIODevice::WriteOnly);
|
||||||
|
blobOut.setVersion(QDataStream::Qt_6_4);
|
||||||
|
blobOut << h;
|
||||||
|
out << static_cast<quint32>(blob.size());
|
||||||
|
out.writeRawData(blob.constData(), blob.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray readHashBlob(QDataStream &in)
|
||||||
|
{
|
||||||
|
quint32 len = 0;
|
||||||
|
in >> len;
|
||||||
|
if (in.status() != QDataStream::Ok || static_cast<qint64>(len) > in.device()->bytesAvailable()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
QByteArray blob(len, Qt::Uninitialized);
|
||||||
|
in.readRawData(blob.data(), static_cast<int>(len));
|
||||||
|
return blob;
|
||||||
|
}
|
||||||
|
|
||||||
|
void writeDate(QDataStream &out, const QDate &d)
|
||||||
|
{
|
||||||
|
out << d;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDate readDate(QDataStream &in)
|
||||||
|
{
|
||||||
|
QDate d;
|
||||||
|
in >> d;
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- CardRelation ----------------------------------------------------------
|
||||||
|
|
||||||
|
void writeRelation(QDataStream &out, const CardRelation *rel)
|
||||||
|
{
|
||||||
|
writeString(out, rel->getName());
|
||||||
|
out << static_cast<quint32>(rel->getAttachType());
|
||||||
|
out << rel->getIsCreateAllExclusion();
|
||||||
|
out << rel->getIsVariable();
|
||||||
|
out << rel->getDefaultCount();
|
||||||
|
out << rel->getIsPersistent();
|
||||||
|
out << rel->getIsFaceDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
CardRelation *readRelation(QDataStream &in)
|
||||||
|
{
|
||||||
|
QString name = readString(in);
|
||||||
|
quint32 attachType = 0;
|
||||||
|
bool isExclusion = false;
|
||||||
|
bool isVariable = false;
|
||||||
|
int defaultCount = 1;
|
||||||
|
bool isPersistent = false;
|
||||||
|
bool isFaceDown = false;
|
||||||
|
in >> attachType;
|
||||||
|
in >> isExclusion;
|
||||||
|
in >> isVariable;
|
||||||
|
in >> defaultCount;
|
||||||
|
in >> isPersistent;
|
||||||
|
in >> isFaceDown;
|
||||||
|
return new CardRelation(name, static_cast<CardRelationType>(attachType), isExclusion, isVariable, defaultCount,
|
||||||
|
isPersistent, isFaceDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- PrintingInfo ----------------------------------------------------------
|
||||||
|
|
||||||
|
// A printing references its set by short name; the CardSetPtr is resolved after
|
||||||
|
// all sets have been reconstructed on read.
|
||||||
|
void writePrinting(QDataStream &out, const PrintingInfo &p)
|
||||||
|
{
|
||||||
|
writeString(out, p.getSet() ? p.getSet()->getShortName() : QString());
|
||||||
|
writeHashBlob(out, p.getPropertiesHash());
|
||||||
|
}
|
||||||
|
|
||||||
|
PrintingInfo readPrinting(QDataStream &in, const SetNameMap &sets)
|
||||||
|
{
|
||||||
|
QString setName = readString(in);
|
||||||
|
QByteArray propsBlob = readHashBlob(in);
|
||||||
|
PrintingInfo p;
|
||||||
|
if (auto set = sets.value(setName)) {
|
||||||
|
p = PrintingInfo(set);
|
||||||
|
}
|
||||||
|
p.setPropertiesBlob(propsBlob);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- CardSet ---------------------------------------------------------------
|
||||||
|
|
||||||
|
void writeSet(QDataStream &out, const CardSetPtr &set)
|
||||||
|
{
|
||||||
|
writeString(out, set->getShortName());
|
||||||
|
writeString(out, set->getLongName());
|
||||||
|
writeString(out, set->getSetType());
|
||||||
|
writeDate(out, set->getReleaseDate());
|
||||||
|
out << static_cast<quint32>(set->getPriority());
|
||||||
|
}
|
||||||
|
|
||||||
|
CardSetPtr readSet(QDataStream &in, ICardSetPriorityController *priorityController)
|
||||||
|
{
|
||||||
|
QString shortName = readString(in);
|
||||||
|
QString longName = readString(in);
|
||||||
|
QString setType = readString(in);
|
||||||
|
QDate releaseDate = readDate(in);
|
||||||
|
quint32 priority = 0;
|
||||||
|
in >> priority;
|
||||||
|
return CardSet::newInstance(priorityController, shortName, longName, setType, releaseDate,
|
||||||
|
static_cast<CardSet::Priority>(priority));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- CardInfo --------------------------------------------------------------
|
||||||
|
|
||||||
|
void writeCard(QDataStream &out, const CardInfoPtr &card)
|
||||||
|
{
|
||||||
|
writeString(out, card->getName());
|
||||||
|
writeString(out, card->getText());
|
||||||
|
out << card->getIsToken();
|
||||||
|
writeHashBlob(out, card->getPropertiesHash());
|
||||||
|
|
||||||
|
CardInfo::UiAttributes ui = card->getUiAttributes();
|
||||||
|
out << ui.cipt;
|
||||||
|
out << ui.landscapeOrientation;
|
||||||
|
out << ui.tableRow;
|
||||||
|
out << ui.upsideDownArt;
|
||||||
|
|
||||||
|
// Precomputed derived state, so the reader can skip simplifyName() and the
|
||||||
|
// per-printing alt-name scan entirely.
|
||||||
|
writeString(out, card->getSimpleName());
|
||||||
|
{
|
||||||
|
const QSet<QString> altNames = card->getAltNames();
|
||||||
|
out << static_cast<quint32>(altNames.size());
|
||||||
|
for (const QString &alt : altNames) {
|
||||||
|
writeString(out, alt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setsToPrintings
|
||||||
|
const SetToPrintingsMap sets = card->getSets();
|
||||||
|
out << static_cast<quint32>(sets.size());
|
||||||
|
for (auto it = sets.constBegin(); it != sets.constEnd(); ++it) {
|
||||||
|
writeString(out, it.key());
|
||||||
|
out << static_cast<quint32>(it.value().size());
|
||||||
|
for (const PrintingInfo &p : it.value()) {
|
||||||
|
writePrinting(out, p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// related cards
|
||||||
|
const QList<CardRelation *> related = card->getRelatedCards();
|
||||||
|
out << static_cast<quint32>(related.size());
|
||||||
|
for (const CardRelation *rel : related) {
|
||||||
|
writeRelation(out, rel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// reverse-related cards (the reverseRelatedCards list, not the computed 2Me)
|
||||||
|
const QList<CardRelation *> reverse = card->getReverseRelatedCards();
|
||||||
|
out << static_cast<quint32>(reverse.size());
|
||||||
|
for (const CardRelation *rel : reverse) {
|
||||||
|
writeRelation(out, rel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CardInfoPtr readCard(QDataStream &in, const SetNameMap &sets)
|
||||||
|
{
|
||||||
|
QString name = readString(in);
|
||||||
|
QString text = readString(in);
|
||||||
|
bool isToken = false;
|
||||||
|
in >> isToken;
|
||||||
|
QByteArray propertiesBlob = readHashBlob(in);
|
||||||
|
|
||||||
|
CardInfo::UiAttributes ui;
|
||||||
|
in >> ui.cipt;
|
||||||
|
in >> ui.landscapeOrientation;
|
||||||
|
in >> ui.tableRow;
|
||||||
|
in >> ui.upsideDownArt;
|
||||||
|
|
||||||
|
// Precomputed derived state, restored directly to skip simplifyName() and the
|
||||||
|
// per-printing alt-name scan.
|
||||||
|
QString simpleName = readString(in);
|
||||||
|
QSet<QString> altNames;
|
||||||
|
quint32 altNameCount = 0;
|
||||||
|
in >> altNameCount;
|
||||||
|
if (in.status() != QDataStream::Ok) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
altNames.reserve(altNameCount);
|
||||||
|
for (quint32 i = 0; i < altNameCount; ++i) {
|
||||||
|
altNames.insert(readString(in));
|
||||||
|
}
|
||||||
|
|
||||||
|
SetToPrintingsMap cardSets;
|
||||||
|
quint32 setCount = 0;
|
||||||
|
in >> setCount;
|
||||||
|
if (in.status() != QDataStream::Ok) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
for (quint32 i = 0; i < setCount; ++i) {
|
||||||
|
QString setName = readString(in);
|
||||||
|
quint32 printingCount = 0;
|
||||||
|
in >> printingCount;
|
||||||
|
if (in.status() != QDataStream::Ok) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
QList<PrintingInfo> printings;
|
||||||
|
printings.reserve(printingCount);
|
||||||
|
for (quint32 j = 0; j < printingCount; ++j) {
|
||||||
|
printings.append(readPrinting(in, sets));
|
||||||
|
}
|
||||||
|
cardSets.insert(setName, printings);
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<CardRelation *> related;
|
||||||
|
quint32 relatedCount = 0;
|
||||||
|
in >> relatedCount;
|
||||||
|
if (in.status() != QDataStream::Ok) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
related.reserve(relatedCount);
|
||||||
|
for (quint32 i = 0; i < relatedCount; ++i) {
|
||||||
|
related.append(readRelation(in));
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<CardRelation *> reverse;
|
||||||
|
quint32 reverseCount = 0;
|
||||||
|
in >> reverseCount;
|
||||||
|
if (in.status() != QDataStream::Ok) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
reverse.reserve(reverseCount);
|
||||||
|
for (quint32 i = 0; i < reverseCount; ++i) {
|
||||||
|
reverse.append(readRelation(in));
|
||||||
|
}
|
||||||
|
|
||||||
|
return CardInfo::newInstance(name, text, isToken, propertiesBlob, related, reverse, cardSets, ui, simpleName,
|
||||||
|
altNames, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- FormatRules -----------------------------------------------------------
|
||||||
|
|
||||||
|
void writeFormat(QDataStream &out, const FormatRulesPtr &format)
|
||||||
|
{
|
||||||
|
writeString(out, format->formatName);
|
||||||
|
out << format->minDeckSize;
|
||||||
|
out << format->maxDeckSize;
|
||||||
|
out << format->maxSideboardSize;
|
||||||
|
|
||||||
|
out << static_cast<quint32>(format->allowedCounts.size());
|
||||||
|
for (const AllowedCount &ac : format->allowedCounts) {
|
||||||
|
out << ac.max;
|
||||||
|
writeString(out, ac.label);
|
||||||
|
}
|
||||||
|
|
||||||
|
out << static_cast<quint32>(format->exceptions.size());
|
||||||
|
for (const ExceptionRule &ex : format->exceptions) {
|
||||||
|
out << static_cast<quint32>(ex.conditions.size());
|
||||||
|
for (const CardCondition &cond : ex.conditions) {
|
||||||
|
writeString(out, cond.field);
|
||||||
|
writeString(out, cond.matchType);
|
||||||
|
writeString(out, cond.value);
|
||||||
|
}
|
||||||
|
out << ex.maxCopies;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FormatRulesPtr readFormat(QDataStream &in)
|
||||||
|
{
|
||||||
|
FormatRulesPtr format(new FormatRules());
|
||||||
|
format->formatName = readString(in);
|
||||||
|
in >> format->minDeckSize;
|
||||||
|
in >> format->maxDeckSize;
|
||||||
|
in >> format->maxSideboardSize;
|
||||||
|
|
||||||
|
quint32 allowedCount = 0;
|
||||||
|
in >> allowedCount;
|
||||||
|
if (in.status() != QDataStream::Ok) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
for (quint32 i = 0; i < allowedCount; ++i) {
|
||||||
|
AllowedCount ac;
|
||||||
|
in >> ac.max;
|
||||||
|
ac.label = readString(in);
|
||||||
|
format->allowedCounts.append(ac);
|
||||||
|
}
|
||||||
|
|
||||||
|
quint32 exceptionCount = 0;
|
||||||
|
in >> exceptionCount;
|
||||||
|
if (in.status() != QDataStream::Ok) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
for (quint32 i = 0; i < exceptionCount; ++i) {
|
||||||
|
ExceptionRule ex;
|
||||||
|
quint32 condCount = 0;
|
||||||
|
in >> condCount;
|
||||||
|
if (in.status() != QDataStream::Ok) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
for (quint32 j = 0; j < condCount; ++j) {
|
||||||
|
CardCondition cond;
|
||||||
|
cond.field = readString(in);
|
||||||
|
cond.matchType = readString(in);
|
||||||
|
cond.value = readString(in);
|
||||||
|
ex.conditions.append(cond);
|
||||||
|
}
|
||||||
|
in >> ex.maxCopies;
|
||||||
|
format->exceptions.append(ex);
|
||||||
|
}
|
||||||
|
return format;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool CardDatabaseCache::write(const QString &cachePath, const CardDatabaseData &data, const QByteArray &sourceHash)
|
||||||
|
{
|
||||||
|
QSaveFile file(cachePath);
|
||||||
|
if (!file.open(QIODevice::WriteOnly)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDataStream out(&file);
|
||||||
|
out.setVersion(QDataStream::Qt_6_4);
|
||||||
|
|
||||||
|
// Header
|
||||||
|
out << CACHE_MAGIC;
|
||||||
|
out << CACHE_VERSION;
|
||||||
|
out << sourceHash;
|
||||||
|
|
||||||
|
// Sets
|
||||||
|
out << static_cast<quint32>(data.sets.size());
|
||||||
|
for (const CardSetPtr &set : data.sets) {
|
||||||
|
writeSet(out, set);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cards
|
||||||
|
const quint32 cardCount = static_cast<quint32>(data.cards.size());
|
||||||
|
out << cardCount;
|
||||||
|
for (const CardInfoPtr &card : data.cards) {
|
||||||
|
writeCard(out, card);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formats
|
||||||
|
out << static_cast<quint32>(data.formats.size());
|
||||||
|
for (const FormatRulesPtr &format : data.formats) {
|
||||||
|
writeFormat(out, format);
|
||||||
|
}
|
||||||
|
|
||||||
|
return file.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CardDatabaseCache::read(const QString &cachePath,
|
||||||
|
CardDatabaseData &data,
|
||||||
|
const QByteArray &sourceHash,
|
||||||
|
ICardSetPriorityController *priorityController)
|
||||||
|
{
|
||||||
|
QFile file(cachePath);
|
||||||
|
if (!file.open(QIODevice::ReadOnly)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the whole cache into memory up front; deserialization then works on an
|
||||||
|
// in-memory buffer with no further disk I/O.
|
||||||
|
QByteArray raw = file.readAll();
|
||||||
|
if (raw.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QBuffer buffer(&raw);
|
||||||
|
buffer.open(QIODevice::ReadOnly);
|
||||||
|
QDataStream in(&buffer);
|
||||||
|
in.setVersion(QDataStream::Qt_6_4);
|
||||||
|
|
||||||
|
quint32 magic = 0;
|
||||||
|
quint32 version = 0;
|
||||||
|
QByteArray storedHash;
|
||||||
|
in >> magic >> version >> storedHash;
|
||||||
|
if (magic != CACHE_MAGIC || version != CACHE_VERSION || storedHash != sourceHash) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QElapsedTimer deserializeTimer;
|
||||||
|
deserializeTimer.start();
|
||||||
|
|
||||||
|
// Sets
|
||||||
|
quint32 setCount = 0;
|
||||||
|
in >> setCount;
|
||||||
|
if (in.status() != QDataStream::Ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (quint32 i = 0; i < setCount; ++i) {
|
||||||
|
CardSetPtr set = readSet(in, priorityController);
|
||||||
|
data.sets.insert(set->getShortName(), set);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cards
|
||||||
|
quint32 cardCount = 0;
|
||||||
|
in >> cardCount;
|
||||||
|
if (in.status() != QDataStream::Ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (cardCount > 0) {
|
||||||
|
data.cards.reserve(static_cast<int>(cardCount));
|
||||||
|
for (quint32 i = 0; i < cardCount; ++i) {
|
||||||
|
CardInfoPtr card = readCard(in, data.sets);
|
||||||
|
if (card == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
data.cards.insert(card->getName(), card);
|
||||||
|
data.simpleNameCards.insert(card->getSimpleName(), card);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const CardInfoPtr &card : data.cards) {
|
||||||
|
for (const auto &printings : card->getSets()) {
|
||||||
|
for (const PrintingInfo &printing : printings) {
|
||||||
|
if (auto set = printing.getSet()) {
|
||||||
|
set->append(card);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formats
|
||||||
|
quint32 formatCount = 0;
|
||||||
|
in >> formatCount;
|
||||||
|
if (in.status() != QDataStream::Ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (quint32 i = 0; i < formatCount; ++i) {
|
||||||
|
FormatRulesPtr format = readFormat(in);
|
||||||
|
if (format == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
data.formats.insert(format->formatName.toLower(), format);
|
||||||
|
}
|
||||||
|
|
||||||
|
qCInfo(CardDatabaseLoadingLog) << "[cache] read + deserialize" << deserializeTimer.elapsed() << "ms for"
|
||||||
|
<< cardCount << "cards";
|
||||||
|
|
||||||
|
if (in.status() != QDataStream::Ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return file.error() == QFile::NoError;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
#ifndef CARDDATABASE_CACHE_H
|
||||||
|
#define CARDDATABASE_CACHE_H
|
||||||
|
|
||||||
|
#include "card_database_data.h"
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <libcockatrice/interfaces/interface_card_set_priority_controller.h>
|
||||||
|
|
||||||
|
namespace CardDatabaseCache
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @brief Writes a fully-built database snapshot to a binary cache file.
|
||||||
|
* @param cachePath Path of the cache file to write.
|
||||||
|
* @param data The snapshot to serialize.
|
||||||
|
* @param sourceHash Hash of the source XML used to validate the cache later.
|
||||||
|
* @return True if the cache was written successfully.
|
||||||
|
*/
|
||||||
|
bool write(const QString &cachePath, const CardDatabaseData &data, const QByteArray &sourceHash);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Reads a database snapshot from a binary cache file.
|
||||||
|
* @param cachePath Path of the cache file to read.
|
||||||
|
* @param data Snapshot to populate.
|
||||||
|
* @param sourceHash Expected hash of the source XML; read fails if it differs.
|
||||||
|
* @param priorityController Controller used to reconstruct CardSet state.
|
||||||
|
* @return True if a valid, up-to-date cache was read.
|
||||||
|
*/
|
||||||
|
bool read(const QString &cachePath,
|
||||||
|
CardDatabaseData &data,
|
||||||
|
const QByteArray &sourceHash,
|
||||||
|
ICardSetPriorityController *priorityController);
|
||||||
|
} // namespace CardDatabaseCache
|
||||||
|
|
||||||
|
#endif // CARDDATABASE_CACHE_H
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
#ifndef CARDDATABASE_DATA_H
|
||||||
|
#define CARDDATABASE_DATA_H
|
||||||
|
|
||||||
|
#include <libcockatrice/card/card_info.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @struct CardDatabaseData
|
||||||
|
* @ingroup CardDatabase
|
||||||
|
* @brief Value type holding the full parsed state of a card database.
|
||||||
|
*
|
||||||
|
* Parsers fill a CardDatabaseData off the UI thread; the finished snapshot is
|
||||||
|
* then swapped into the live CardDatabase atomically. Keeping this separate from
|
||||||
|
* the live container avoids emitting per-card change notifications and prevents
|
||||||
|
* readers from observing a half-loaded database.
|
||||||
|
*/
|
||||||
|
struct CardDatabaseData
|
||||||
|
{
|
||||||
|
CardNameMap cards;
|
||||||
|
CardNameMap simpleNameCards;
|
||||||
|
SetNameMap sets;
|
||||||
|
FormatRulesNameMap formats;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // CARDDATABASE_DATA_H
|
||||||
|
|
@ -1,12 +1,18 @@
|
||||||
#include "card_database_loader.h"
|
#include "card_database_loader.h"
|
||||||
|
|
||||||
#include "card_database.h"
|
#include "card_database.h"
|
||||||
|
#include "card_database_cache.h"
|
||||||
|
#include "parser/card_database_parser.h"
|
||||||
#include "parser/cockatrice_xml_3.h"
|
#include "parser/cockatrice_xml_3.h"
|
||||||
#include "parser/cockatrice_xml_4.h"
|
#include "parser/cockatrice_xml_4.h"
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QCryptographicHash>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QDirIterator>
|
#include <QDirIterator>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
#include <QTime>
|
#include <QTime>
|
||||||
|
|
||||||
CardDatabaseLoader::CardDatabaseLoader(QObject *parent,
|
CardDatabaseLoader::CardDatabaseLoader(QObject *parent,
|
||||||
|
|
@ -14,18 +20,14 @@ CardDatabaseLoader::CardDatabaseLoader(QObject *parent,
|
||||||
ICardDatabasePathProvider *_pathProvider,
|
ICardDatabasePathProvider *_pathProvider,
|
||||||
ICardPreferenceProvider *_preferenceProvider,
|
ICardPreferenceProvider *_preferenceProvider,
|
||||||
ICardSetPriorityController *_priorityController)
|
ICardSetPriorityController *_priorityController)
|
||||||
: QObject(parent), database(db), pathProvider(_pathProvider)
|
: QObject(parent), database(db), pathProvider(_pathProvider), priorityController(_priorityController)
|
||||||
{
|
{
|
||||||
// instantiate available parsers here and connect them to the database
|
// instantiate available parsers here
|
||||||
availableParsers << new CockatriceXml4Parser(_preferenceProvider, _priorityController);
|
availableParsers << new CockatriceXml4Parser(_preferenceProvider, _priorityController);
|
||||||
availableParsers << new CockatriceXml3Parser(_priorityController);
|
availableParsers << new CockatriceXml3Parser(_priorityController);
|
||||||
|
|
||||||
for (auto *p : availableParsers) {
|
// The load path parses into a snapshot and never emits per-card signals;
|
||||||
// connect parser outputs to the database adders
|
// the finished snapshot is swapped into the live database on the GUI thread.
|
||||||
connect(p, &ICardDatabaseParser::addCard, database, &CardDatabase::addCard, Qt::DirectConnection);
|
|
||||||
connect(p, &ICardDatabaseParser::addSet, database, &CardDatabase::addSet, Qt::DirectConnection);
|
|
||||||
connect(p, &ICardDatabaseParser::addFormat, database, &CardDatabase::addFormat, Qt::DirectConnection);
|
|
||||||
}
|
|
||||||
|
|
||||||
// when SettingsCache's path changes, trigger reloads
|
// when SettingsCache's path changes, trigger reloads
|
||||||
connect(pathProvider, &ICardDatabasePathProvider::cardDatabasePathChanged, this,
|
connect(pathProvider, &ICardDatabasePathProvider::cardDatabasePathChanged, this,
|
||||||
|
|
@ -38,7 +40,7 @@ CardDatabaseLoader::~CardDatabaseLoader()
|
||||||
availableParsers.clear();
|
availableParsers.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
LoadStatus CardDatabaseLoader::loadFromFile(const QString &fileName)
|
LoadStatus CardDatabaseLoader::loadFromFile(const QString &fileName, CardDatabaseData &data)
|
||||||
{
|
{
|
||||||
QFile file(fileName);
|
QFile file(fileName);
|
||||||
if (!file.open(QIODevice::ReadOnly)) {
|
if (!file.open(QIODevice::ReadOnly)) {
|
||||||
|
|
@ -49,7 +51,7 @@ LoadStatus CardDatabaseLoader::loadFromFile(const QString &fileName)
|
||||||
file.reset();
|
file.reset();
|
||||||
if (parser->getCanParseFile(fileName, file)) {
|
if (parser->getCanParseFile(fileName, file)) {
|
||||||
file.reset();
|
file.reset();
|
||||||
parser->parseFile(file);
|
parser->parseFileInto(file, data);
|
||||||
return Ok;
|
return Ok;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -57,24 +59,29 @@ LoadStatus CardDatabaseLoader::loadFromFile(const QString &fileName)
|
||||||
return Invalid;
|
return Invalid;
|
||||||
}
|
}
|
||||||
|
|
||||||
LoadStatus CardDatabaseLoader::loadCardDatabase(const QString &path)
|
LoadStatus CardDatabaseLoader::loadCardDatabase(const QString &path, CardDatabaseData &data)
|
||||||
{
|
{
|
||||||
auto startTime = QTime::currentTime();
|
auto startTime = QTime::currentTime();
|
||||||
LoadStatus tempLoadStatus = NotLoaded;
|
LoadStatus tempLoadStatus = NotLoaded;
|
||||||
if (!path.isEmpty()) {
|
if (!path.isEmpty()) {
|
||||||
QMutexLocker locker(loadFromFileMutex);
|
QMutexLocker locker(loadFromFileMutex);
|
||||||
tempLoadStatus = loadFromFile(path);
|
tempLoadStatus = loadFromFile(path, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
int msecs = startTime.msecsTo(QTime::currentTime());
|
int msecs = startTime.msecsTo(QTime::currentTime());
|
||||||
qCInfo(CardDatabaseLoadingLog) << "Loaded card database: Path =" << path << "Status =" << tempLoadStatus
|
qCInfo(CardDatabaseLoadingLog) << "Loaded card database: Path =" << path << "Status =" << tempLoadStatus
|
||||||
<< "Cards =" << (database ? database->cards.size() : 0)
|
<< "Cards =" << data.cards.size() << "Sets =" << data.sets.size()
|
||||||
<< "Sets =" << (database ? database->sets.size() : 0) << QString("%1ms").arg(msecs);
|
<< QString("%1ms").arg(msecs);
|
||||||
|
|
||||||
return tempLoadStatus;
|
return tempLoadStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
LoadStatus CardDatabaseLoader::loadCardDatabases()
|
LoadStatus CardDatabaseLoader::loadCardDatabases()
|
||||||
|
{
|
||||||
|
return doLoadCardDatabases();
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadStatus CardDatabaseLoader::doLoadCardDatabases()
|
||||||
{
|
{
|
||||||
QMutexLocker locker(reloadDatabaseMutex);
|
QMutexLocker locker(reloadDatabaseMutex);
|
||||||
|
|
||||||
|
|
@ -86,31 +93,55 @@ LoadStatus CardDatabaseLoader::loadCardDatabases()
|
||||||
emit loadingStarted();
|
emit loadingStarted();
|
||||||
qCInfo(CardDatabaseLoadingLog) << "Card Database Loading Started";
|
qCInfo(CardDatabaseLoadingLog) << "Card Database Loading Started";
|
||||||
|
|
||||||
database->clear(); // remove old db
|
ICardDatabaseParser::clearSetlist();
|
||||||
|
|
||||||
LoadStatus loadStatus = loadCardDatabase(pathProvider->getCardDatabasePath()); // load main card database
|
CardDatabaseData data;
|
||||||
loadCardDatabase(pathProvider->getTokenDatabasePath()); // load tokens database
|
LoadStatus loadStatus = NotLoaded;
|
||||||
loadCardDatabase(pathProvider->getSpoilerCardDatabasePath()); // load spoilers database
|
|
||||||
|
|
||||||
// find all custom card databases, recursively & following symlinks
|
// Try the binary cache first: a cache hit avoids re-parsing the (large) XML.
|
||||||
// then load them alphabetically
|
|
||||||
const QStringList customPaths = collectCustomDatabasePaths();
|
const QStringList customPaths = collectCustomDatabasePaths();
|
||||||
for (int i = 0; i < customPaths.size(); ++i) {
|
const QByteArray sourceHash = computeSourceHash(customPaths);
|
||||||
const auto &p = customPaths.at(i);
|
if (loadFromCache(data, sourceHash)) {
|
||||||
qCInfo(CardDatabaseLoadingLog) << "Loading Custom Set" << i << "(" << p << ")";
|
qCInfo(CardDatabaseLoadingLog) << "Loaded card database from binary cache";
|
||||||
loadCardDatabase(p);
|
loadStatus = Ok;
|
||||||
|
} else {
|
||||||
|
loadStatus = loadCardDatabase(pathProvider->getCardDatabasePath(), data); // main card database
|
||||||
|
if (loadStatus == Ok) {
|
||||||
|
loadCardDatabase(pathProvider->getTokenDatabasePath(), data); // tokens database
|
||||||
|
loadCardDatabase(pathProvider->getSpoilerCardDatabasePath(), data); // spoilers database
|
||||||
|
|
||||||
|
// find all custom card databases, recursively & following symlinks
|
||||||
|
// then load them alphabetically
|
||||||
|
for (int i = 0; i < customPaths.size(); ++i) {
|
||||||
|
const auto &p = customPaths.at(i);
|
||||||
|
qCInfo(CardDatabaseLoadingLog) << "Loading Custom Set" << i << "(" << p << ")";
|
||||||
|
loadCardDatabase(p, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!saveToCache(data, sourceHash)) {
|
||||||
|
qCWarning(CardDatabaseLoadingLog) << "Failed to write binary cache to" << cachePath();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AFTER all the cards have been loaded
|
// AFTER all the cards have been loaded: resolve the reverse-related tags
|
||||||
|
// against the fully-built snapshot.
|
||||||
// resolve the reverse-related tags
|
|
||||||
|
|
||||||
database->refreshCachedReverseRelatedCards();
|
|
||||||
|
|
||||||
if (loadStatus == Ok) {
|
if (loadStatus == Ok) {
|
||||||
database->checkUnknownSets(); // update deck editors, etc
|
database->refreshCachedReverseRelatedCards(data.cards);
|
||||||
|
|
||||||
qCInfo(CardDatabaseLoadingSuccessOrFailureLog) << "Card Database Loading Success";
|
qCInfo(CardDatabaseLoadingSuccessOrFailureLog) << "Card Database Loading Success";
|
||||||
|
emit databaseDataReady(std::move(data));
|
||||||
emit loadingFinished();
|
emit loadingFinished();
|
||||||
|
// During the front-loaded parse in main() this runs before MainWindow
|
||||||
|
// exists, so cardDatabaseNewSetsFound / cardDatabaseAllNewSetsEnabled
|
||||||
|
// would be emitted with no receivers. Skip the check on the first load;
|
||||||
|
// MainWindow::startupConfigCheck() calls checkUnknownSets() once its
|
||||||
|
// signal connections are live. On subsequent reloads (e.g. path change)
|
||||||
|
// MainWindow exists and signals have live receivers.
|
||||||
|
if (initialLoadComplete) {
|
||||||
|
database->checkUnknownSets();
|
||||||
|
}
|
||||||
|
initialLoadComplete = true;
|
||||||
} else {
|
} else {
|
||||||
qCInfo(CardDatabaseLoadingSuccessOrFailureLog) << "Card Database Loading Failed";
|
qCInfo(CardDatabaseLoadingSuccessOrFailureLog) << "Card Database Loading Failed";
|
||||||
emit loadingFailed(); // bring up the settings dialog
|
emit loadingFailed(); // bring up the settings dialog
|
||||||
|
|
@ -119,6 +150,55 @@ LoadStatus CardDatabaseLoader::loadCardDatabases()
|
||||||
return loadStatus;
|
return loadStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString CardDatabaseLoader::cachePath() const
|
||||||
|
{
|
||||||
|
return pathProvider->getCardDatabasePath() + ".cache";
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray CardDatabaseLoader::computeSourceHash(const QStringList &customPaths) const
|
||||||
|
{
|
||||||
|
// Hash over the paths, sizes and modification times of every input file so
|
||||||
|
// the cache invalidates when any source changes. Cheap (no content read).
|
||||||
|
QCryptographicHash hash(QCryptographicHash::Sha256);
|
||||||
|
|
||||||
|
// Include the application version so parser changes automatically invalidate
|
||||||
|
// old caches even when the XML files are byte-identical.
|
||||||
|
hash.addData(QCoreApplication::applicationVersion().toUtf8());
|
||||||
|
hash.addData(QByteArray(1, '\0'));
|
||||||
|
|
||||||
|
const QStringList inputs = QStringList()
|
||||||
|
<< pathProvider->getCardDatabasePath() << pathProvider->getTokenDatabasePath()
|
||||||
|
<< pathProvider->getSpoilerCardDatabasePath() << customPaths;
|
||||||
|
for (const QString &path : inputs) {
|
||||||
|
QFileInfo info(path);
|
||||||
|
if (info.exists()) {
|
||||||
|
hash.addData(path.toUtf8());
|
||||||
|
hash.addData(QByteArray(1, '\0'));
|
||||||
|
hash.addData(QByteArray::number(info.size()));
|
||||||
|
hash.addData(QByteArray(1, '\0'));
|
||||||
|
hash.addData(QByteArray::number(info.lastModified().toSecsSinceEpoch()));
|
||||||
|
hash.addData(QByteArray(1, '\0'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hash.result();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CardDatabaseLoader::loadFromCache(CardDatabaseData &data, const QByteArray &sourceHash)
|
||||||
|
{
|
||||||
|
if (sourceHash.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return CardDatabaseCache::read(cachePath(), data, sourceHash, priorityController);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CardDatabaseLoader::saveToCache(const CardDatabaseData &data, const QByteArray &sourceHash)
|
||||||
|
{
|
||||||
|
if (sourceHash.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return CardDatabaseCache::write(cachePath(), data, sourceHash);
|
||||||
|
}
|
||||||
|
|
||||||
QStringList CardDatabaseLoader::collectCustomDatabasePaths() const
|
QStringList CardDatabaseLoader::collectCustomDatabasePaths() const
|
||||||
{
|
{
|
||||||
QDirIterator it(pathProvider->getCustomCardDatabasePath(), {"*.xml"}, QDir::Files,
|
QDirIterator it(pathProvider->getCustomCardDatabasePath(), {"*.xml"}, QDir::Files,
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
#include <QBasicMutex>
|
#include <QBasicMutex>
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QLoggingCategory>
|
#include <QLoggingCategory>
|
||||||
|
#include <libcockatrice/card/database/card_database_data.h>
|
||||||
#include <libcockatrice/interfaces/interface_card_database_path_provider.h>
|
#include <libcockatrice/interfaces/interface_card_database_path_provider.h>
|
||||||
#include <libcockatrice/interfaces/interface_card_preference_provider.h>
|
#include <libcockatrice/interfaces/interface_card_preference_provider.h>
|
||||||
#include <libcockatrice/interfaces/interface_card_set_priority_controller.h>
|
#include <libcockatrice/interfaces/interface_card_set_priority_controller.h>
|
||||||
|
|
@ -62,16 +63,20 @@ public:
|
||||||
public slots:
|
public slots:
|
||||||
/**
|
/**
|
||||||
* @brief Loads all configured card databases.
|
* @brief Loads all configured card databases.
|
||||||
|
*
|
||||||
|
* Runs synchronously on the calling thread. The caller should ensure
|
||||||
|
* that any signal receivers are already connected before invoking this.
|
||||||
* @return Status of the main database load.
|
* @return Status of the main database load.
|
||||||
*/
|
*/
|
||||||
LoadStatus loadCardDatabases();
|
LoadStatus loadCardDatabases();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Loads a single card database file.
|
* @brief Loads a single card database file into the given snapshot.
|
||||||
* @param path Path to the database file.
|
* @param path Path to the database file.
|
||||||
|
* @param data Snapshot to populate.
|
||||||
* @return LoadStatus indicating success or failure.
|
* @return LoadStatus indicating success or failure.
|
||||||
*/
|
*/
|
||||||
LoadStatus loadCardDatabase(const QString &path);
|
LoadStatus loadCardDatabase(const QString &path, CardDatabaseData &data);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Saves custom tokens to the user-defined custom database path.
|
* @brief Saves custom tokens to the user-defined custom database path.
|
||||||
|
|
@ -89,6 +94,12 @@ signals:
|
||||||
/** @brief Emitted when loading fails. */
|
/** @brief Emitted when loading fails. */
|
||||||
void loadingFailed();
|
void loadingFailed();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Emitted with the fully-built snapshot once background loading
|
||||||
|
* completes. The receiver swaps it into the live database.
|
||||||
|
*/
|
||||||
|
void databaseDataReady(CardDatabaseData data);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Emitted when new sets are discovered during loading.
|
* @brief Emitted when new sets are discovered during loading.
|
||||||
* @param numSets Number of new sets.
|
* @param numSets Number of new sets.
|
||||||
|
|
@ -101,11 +112,18 @@ signals:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
/**
|
/**
|
||||||
* @brief Loads a database from a single file using the available parsers.
|
* @brief Parses a single file into the given snapshot using the available parsers.
|
||||||
* @param fileName Path to the database file.
|
* @param fileName Path to the database file.
|
||||||
|
* @param data Snapshot to populate.
|
||||||
* @return LoadStatus indicating success or failure.
|
* @return LoadStatus indicating success or failure.
|
||||||
*/
|
*/
|
||||||
LoadStatus loadFromFile(const QString &fileName);
|
LoadStatus loadFromFile(const QString &fileName, CardDatabaseData &data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Performs the actual load work synchronously on the calling thread.
|
||||||
|
* @return Status of the main database load.
|
||||||
|
*/
|
||||||
|
LoadStatus doLoadCardDatabases();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Collects custom card database paths recursively.
|
* @brief Collects custom card database paths recursively.
|
||||||
|
|
@ -113,13 +131,43 @@ private:
|
||||||
*/
|
*/
|
||||||
[[nodiscard]] QStringList collectCustomDatabasePaths() const;
|
[[nodiscard]] QStringList collectCustomDatabasePaths() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Computes a hash identifying the current set of input database files.
|
||||||
|
* @return Hash over the paths, sizes and modification times of all inputs.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] QByteArray computeSourceHash(const QStringList &customPaths) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Path of the binary cache file derived from the main card database path.
|
||||||
|
* @return Cache file path.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] QString cachePath() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Attempts to populate the snapshot from the binary cache.
|
||||||
|
* @param data Snapshot to populate.
|
||||||
|
* @param sourceHash Pre-computed hash identifying the current input files.
|
||||||
|
* @return True if a valid, up-to-date cache was read.
|
||||||
|
*/
|
||||||
|
bool loadFromCache(CardDatabaseData &data, const QByteArray &sourceHash);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Writes the snapshot to the binary cache for the current inputs.
|
||||||
|
* @param data Snapshot to serialize.
|
||||||
|
* @param sourceHash Pre-computed hash identifying the current input files.
|
||||||
|
* @return True if the cache was written successfully.
|
||||||
|
*/
|
||||||
|
bool saveToCache(const CardDatabaseData &data, const QByteArray &sourceHash);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
CardDatabase *database; /**< Non-owning pointer to the target CardDatabase. */
|
CardDatabase *database; /**< Non-owning pointer to the target CardDatabase. */
|
||||||
ICardDatabasePathProvider *pathProvider; /**< Pointer to the path provider. */
|
ICardDatabasePathProvider *pathProvider; /**< Pointer to the path provider. */
|
||||||
QList<ICardDatabaseParser *> availableParsers; /**< List of available parsers for different formats. */
|
ICardSetPriorityController *priorityController; /**< Controller for reconstructing set state. */
|
||||||
|
QList<ICardDatabaseParser *> availableParsers; /**< List of available parsers for different formats. */
|
||||||
|
|
||||||
QBasicMutex *loadFromFileMutex = new QBasicMutex(); /**< Mutex for single-file loading. */
|
QBasicMutex *loadFromFileMutex = new QBasicMutex(); /**< Mutex for single-file loading. */
|
||||||
QBasicMutex *reloadDatabaseMutex = new QBasicMutex(); /**< Mutex for reloading entire database. */
|
QBasicMutex *reloadDatabaseMutex = new QBasicMutex(); /**< Mutex for reloading entire database. */
|
||||||
|
bool initialLoadComplete = false; /**< Set after the first successful load. */
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // COCKATRICE_CARD_DATABASE_LOADER_H
|
#endif // COCKATRICE_CARD_DATABASE_LOADER_H
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,10 @@ CardSetPtr ICardDatabaseParser::internalAddSet(const QString &setName,
|
||||||
newSet->setPriority(priority);
|
newSet->setPriority(priority);
|
||||||
|
|
||||||
sets.insert(setName, newSet);
|
sets.insert(setName, newSet);
|
||||||
emit addSet(newSet);
|
if (targetData) {
|
||||||
|
targetData->sets.insert(setName, newSet);
|
||||||
|
} else {
|
||||||
|
emit addSet(newSet);
|
||||||
|
}
|
||||||
return newSet;
|
return newSet;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
#define CARDDATABASE_PARSER_H
|
#define CARDDATABASE_PARSER_H
|
||||||
|
|
||||||
#include "../../card_info.h"
|
#include "../../card_info.h"
|
||||||
|
#include "../card_database_data.h"
|
||||||
|
|
||||||
#include <QIODevice>
|
#include <QIODevice>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
@ -37,6 +38,15 @@ public:
|
||||||
*/
|
*/
|
||||||
virtual void parseFile(QIODevice &device) = 0;
|
virtual void parseFile(QIODevice &device) = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Parses a database file into the given snapshot, without emitting
|
||||||
|
* any signals. Used for background loads that swap the snapshot into
|
||||||
|
* the live database once complete.
|
||||||
|
* @param device QIODevice representing the file content.
|
||||||
|
* @param data Target snapshot to populate.
|
||||||
|
*/
|
||||||
|
virtual void parseFileInto(QIODevice &device, CardDatabaseData &data) = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Saves card and set data to a file.
|
* @brief Saves card and set data to a file.
|
||||||
* @param _formats
|
* @param _formats
|
||||||
|
|
@ -62,6 +72,21 @@ protected:
|
||||||
static SetNameMap sets;
|
static SetNameMap sets;
|
||||||
ICardSetPriorityController *cardSetPriorityController;
|
ICardSetPriorityController *cardSetPriorityController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Snapshot the current parse is filling, or nullptr when emitting signals.
|
||||||
|
*
|
||||||
|
* This is an implicit-inheritance-via-member-variable pattern: parseFileInto()
|
||||||
|
* sets this before delegating to parseFile(), and loadCardsFromXml() /
|
||||||
|
* loadFormats() check it to decide between direct insertion and signal
|
||||||
|
* emission. Safe under the current model because loadFromFileMutex serialises
|
||||||
|
* all parser access and parseFile() is never re-entered. If a future change
|
||||||
|
* adds parallel parsing or a second parseFileInto() call within parseFile(),
|
||||||
|
* this pointer would race -- at that point refactor to pass CardDatabaseData*
|
||||||
|
* through the call chain instead. However, we are mostly RAM, not CPU bound on
|
||||||
|
* DB startup at this point so there's not much point to parallel parsing.
|
||||||
|
*/
|
||||||
|
CardDatabaseData *targetData = nullptr;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Internal helper to add a set to the global set cache.
|
* @brief Internal helper to add a set to the global set cache.
|
||||||
* @param setName Short set name.
|
* @param setName Short set name.
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,13 @@ void CockatriceXml3Parser::parseFile(QIODevice &device)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CockatriceXml3Parser::parseFileInto(QIODevice &device, CardDatabaseData &data)
|
||||||
|
{
|
||||||
|
targetData = &data;
|
||||||
|
parseFile(device);
|
||||||
|
targetData = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
void CockatriceXml3Parser::loadSetsFromXml(QXmlStreamReader &xml)
|
void CockatriceXml3Parser::loadSetsFromXml(QXmlStreamReader &xml)
|
||||||
{
|
{
|
||||||
while (!xml.atEnd()) {
|
while (!xml.atEnd()) {
|
||||||
|
|
@ -222,25 +229,27 @@ void CockatriceXml3Parser::loadCardsFromXml(QXmlStreamReader &xml)
|
||||||
// behaviour. Without this check, disabling a set has no effect on v3 databases.
|
// behaviour. Without this check, disabling a set has no effect on v3 databases.
|
||||||
if (set->getEnabled()) {
|
if (set->getEnabled()) {
|
||||||
PrintingInfo setInfo(set);
|
PrintingInfo setInfo(set);
|
||||||
|
QVariantHash printingProps;
|
||||||
if (attrs.hasAttribute("muId")) {
|
if (attrs.hasAttribute("muId")) {
|
||||||
setInfo.setProperty("muid", attrs.value("muId").toString());
|
printingProps.insert("muid", attrs.value("muId").toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attrs.hasAttribute("uuId")) {
|
if (attrs.hasAttribute("uuId")) {
|
||||||
setInfo.setProperty("uuid", attrs.value("uuId").toString());
|
printingProps.insert("uuid", attrs.value("uuId").toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attrs.hasAttribute("picURL")) {
|
if (attrs.hasAttribute("picURL")) {
|
||||||
setInfo.setProperty("picurl", attrs.value("picURL").toString());
|
printingProps.insert("picurl", attrs.value("picURL").toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attrs.hasAttribute("num")) {
|
if (attrs.hasAttribute("num")) {
|
||||||
setInfo.setProperty("num", attrs.value("num").toString());
|
printingProps.insert("num", attrs.value("num").toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attrs.hasAttribute("rarity")) {
|
if (attrs.hasAttribute("rarity")) {
|
||||||
setInfo.setProperty("rarity", attrs.value("rarity").toString());
|
printingProps.insert("rarity", attrs.value("rarity").toString());
|
||||||
}
|
}
|
||||||
|
setInfo.setProperties(printingProps);
|
||||||
_sets[setName].append(setInfo);
|
_sets[setName].append(setInfo);
|
||||||
}
|
}
|
||||||
// related cards
|
// related cards
|
||||||
|
|
@ -299,7 +308,20 @@ void CockatriceXml3Parser::loadCardsFromXml(QXmlStreamReader &xml)
|
||||||
.upsideDownArt = upsideDown};
|
.upsideDownArt = upsideDown};
|
||||||
CardInfoPtr newCard = CardInfo::newInstance(name, text, isToken, properties, relatedCards,
|
CardInfoPtr newCard = CardInfo::newInstance(name, text, isToken, properties, relatedCards,
|
||||||
reverseRelatedCards, _sets, attributes);
|
reverseRelatedCards, _sets, attributes);
|
||||||
emit addCard(newCard);
|
if (targetData) {
|
||||||
|
if (auto existing = targetData->cards.value(name)) {
|
||||||
|
for (const auto &printings : newCard->getSets()) {
|
||||||
|
for (const auto &printing : printings) {
|
||||||
|
existing->addToSet(printing.getSet(), printing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
targetData->cards.insert(name, newCard);
|
||||||
|
targetData->simpleNameCards.insert(newCard->getSimpleName(), newCard);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emit addCard(newCard);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,13 @@ public:
|
||||||
*/
|
*/
|
||||||
void parseFile(QIODevice &device) override;
|
void parseFile(QIODevice &device) override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Parse the XML database into the given snapshot.
|
||||||
|
* @param device Open QIODevice positioned at start of file.
|
||||||
|
* @param data Target snapshot to populate.
|
||||||
|
*/
|
||||||
|
void parseFileInto(QIODevice &device, CardDatabaseData &data) override;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Save sets and cards back to an XML3 file.
|
* @brief Save sets and cards back to an XML3 file.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,13 @@ void CockatriceXml4Parser::parseFile(QIODevice &device)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CockatriceXml4Parser::parseFileInto(QIODevice &device, CardDatabaseData &data)
|
||||||
|
{
|
||||||
|
targetData = &data;
|
||||||
|
parseFile(device);
|
||||||
|
targetData = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
static QSharedPointer<FormatRules> parseFormat(QXmlStreamReader &xml)
|
static QSharedPointer<FormatRules> parseFormat(QXmlStreamReader &xml)
|
||||||
{
|
{
|
||||||
auto rulesPtr = FormatRulesPtr(new FormatRules());
|
auto rulesPtr = FormatRulesPtr(new FormatRules());
|
||||||
|
|
@ -187,7 +194,11 @@ void CockatriceXml4Parser::loadFormats(QXmlStreamReader &xml)
|
||||||
|
|
||||||
if (xml.name().toString() == "format") {
|
if (xml.name().toString() == "format") {
|
||||||
auto rulesPtr = parseFormat(xml);
|
auto rulesPtr = parseFormat(xml);
|
||||||
emit addFormat(rulesPtr);
|
if (targetData) {
|
||||||
|
targetData->formats.insert(rulesPtr->formatName.toLower(), rulesPtr);
|
||||||
|
} else {
|
||||||
|
emit addFormat(rulesPtr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -304,13 +315,15 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml)
|
||||||
auto set = internalAddSet(setName);
|
auto set = internalAddSet(setName);
|
||||||
if (set->getEnabled()) {
|
if (set->getEnabled()) {
|
||||||
PrintingInfo printingInfo(set);
|
PrintingInfo printingInfo(set);
|
||||||
|
QVariantHash printingProps;
|
||||||
for (QXmlStreamAttribute attr : attrs) {
|
for (QXmlStreamAttribute attr : attrs) {
|
||||||
QString attrName = attr.name().toString();
|
QString attrName = attr.name().toString();
|
||||||
if (attrName == "picURL") {
|
if (attrName == "picURL") {
|
||||||
attrName = "picurl";
|
attrName = "picurl";
|
||||||
}
|
}
|
||||||
printingInfo.setProperty(attrName, attr.value().toString());
|
printingProps.insert(attrName, attr.value().toString());
|
||||||
}
|
}
|
||||||
|
printingInfo.setProperties(printingProps);
|
||||||
|
|
||||||
// This is very much a hack and not the right place to
|
// This is very much a hack and not the right place to
|
||||||
// put this check, as it requires a reload of Cockatrice
|
// put this check, as it requires a reload of Cockatrice
|
||||||
|
|
@ -389,7 +402,22 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml)
|
||||||
.upsideDownArt = upsideDown};
|
.upsideDownArt = upsideDown};
|
||||||
CardInfoPtr newCard = CardInfo::newInstance(name, text, isToken, properties, relatedCards,
|
CardInfoPtr newCard = CardInfo::newInstance(name, text, isToken, properties, relatedCards,
|
||||||
reverseRelatedCards, _sets, attributes);
|
reverseRelatedCards, _sets, attributes);
|
||||||
emit addCard(newCard);
|
if (targetData) {
|
||||||
|
// Mirror CardDatabase::addCard: if a card with this name already
|
||||||
|
// exists, merge the new printings into it instead of replacing.
|
||||||
|
if (auto existing = targetData->cards.value(name)) {
|
||||||
|
for (const auto &printings : newCard->getSets()) {
|
||||||
|
for (const auto &printing : printings) {
|
||||||
|
existing->addToSet(printing.getSet(), printing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
targetData->cards.insert(name, newCard);
|
||||||
|
targetData->simpleNameCards.insert(newCard->getSimpleName(), newCard);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emit addCard(newCard);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,13 @@ public:
|
||||||
*/
|
*/
|
||||||
void parseFile(QIODevice &device) override;
|
void parseFile(QIODevice &device) override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Parse the XML database into the given snapshot.
|
||||||
|
* @param device Open QIODevice positioned at start of file.
|
||||||
|
* @param data Target snapshot to populate.
|
||||||
|
*/
|
||||||
|
void parseFileInto(QIODevice &device, CardDatabaseData &data) override;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Save sets and cards back to an XML4 file.
|
* @brief Save sets and cards back to an XML4 file.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -2,19 +2,56 @@
|
||||||
|
|
||||||
#include "../set/card_set.h"
|
#include "../set/card_set.h"
|
||||||
|
|
||||||
|
#include <QDataStream>
|
||||||
|
#include <QIODevice>
|
||||||
|
|
||||||
PrintingInfo::PrintingInfo(const CardSetPtr &_set) : set(_set)
|
PrintingInfo::PrintingInfo(const CardSetPtr &_set) : set(_set)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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).toString() == _value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
propertiesCache.insert(_name, _value);
|
||||||
|
setProperties(propertiesCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PrintingInfo::setProperties(const QVariantHash &_props)
|
||||||
|
{
|
||||||
|
ensurePropertiesLoaded();
|
||||||
|
propertiesCache = _props;
|
||||||
|
QDataStream out(&propertiesBlob, QIODevice::WriteOnly);
|
||||||
|
out.setVersion(QDataStream::Qt_6_4);
|
||||||
|
out << propertiesCache;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the uuid property of the printing, or an empty string if the property isn't present
|
* Gets the uuid property of the printing, or an empty string if the property isn't present
|
||||||
*/
|
*/
|
||||||
QString PrintingInfo::getUuid() const
|
QString PrintingInfo::getUuid() const
|
||||||
{
|
{
|
||||||
return properties.value("uuid").toString();
|
return getPropertiesHash().value("uuid").toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString PrintingInfo::getFlavorName() const
|
QString PrintingInfo::getFlavorName() const
|
||||||
{
|
{
|
||||||
return properties.value("flavorName").toString();
|
return getPropertiesHash().value("flavorName").toString();
|
||||||
}
|
}
|
||||||
|
|
@ -5,6 +5,8 @@
|
||||||
|
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QMap>
|
#include <QMap>
|
||||||
|
#include <QMutex>
|
||||||
|
#include <QSharedPointer>
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
|
|
||||||
class PrintingInfo;
|
class PrintingInfo;
|
||||||
|
|
@ -51,7 +53,7 @@ public:
|
||||||
*/
|
*/
|
||||||
bool operator==(const PrintingInfo &other) const
|
bool operator==(const PrintingInfo &other) const
|
||||||
{
|
{
|
||||||
return this->set == other.set && this->properties == other.properties;
|
return this->set == other.set && this->getPropertiesHash() == other.getPropertiesHash();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -61,12 +63,22 @@ public:
|
||||||
*/
|
*/
|
||||||
bool isEmpty() const
|
bool isEmpty() const
|
||||||
{
|
{
|
||||||
return set == nullptr && properties.isEmpty();
|
return set == nullptr && getPropertiesHash().isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
CardSetPtr set; ///< The set this variation belongs to.
|
CardSetPtr set; ///< The set this variation belongs to.
|
||||||
QVariantHash properties; ///< Key-value store for variation-specific attributes.
|
|
||||||
|
// Properties are stored as a pre-serialized blob (cheap to load) and the
|
||||||
|
// QVariantHash is materialized on first query. This avoids constructing
|
||||||
|
// thousands of QVariants per card at database-load time.
|
||||||
|
mutable QByteArray propertiesBlob; ///< Serialized properties (load form).
|
||||||
|
mutable QVariantHash propertiesCache; ///< Materialized properties (query form).
|
||||||
|
mutable bool propertiesLoaded = false; ///< Whether propertiesCache is valid.
|
||||||
|
mutable QSharedPointer<QBasicMutex> propertiesMutex =
|
||||||
|
QSharedPointer<QBasicMutex>::create(); ///< Guards lazy materialization.
|
||||||
|
|
||||||
|
void ensurePropertiesLoaded() const;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
/**
|
/**
|
||||||
|
|
@ -86,7 +98,13 @@ public:
|
||||||
*/
|
*/
|
||||||
[[nodiscard]] QStringList getProperties() const
|
[[nodiscard]] QStringList getProperties() const
|
||||||
{
|
{
|
||||||
return properties.keys();
|
return getPropertiesHash().keys();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] const QVariantHash &getPropertiesHash() const
|
||||||
|
{
|
||||||
|
ensurePropertiesLoaded();
|
||||||
|
return propertiesCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -97,7 +115,7 @@ public:
|
||||||
*/
|
*/
|
||||||
[[nodiscard]] QString getProperty(const QString &propertyName) const
|
[[nodiscard]] QString getProperty(const QString &propertyName) const
|
||||||
{
|
{
|
||||||
return properties.value(propertyName).toString();
|
return getPropertiesHash().value(propertyName).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -108,9 +126,21 @@ public:
|
||||||
* @param _name The name of the property.
|
* @param _name The name of the property.
|
||||||
* @param _value The string value to assign.
|
* @param _value The string value to assign.
|
||||||
*/
|
*/
|
||||||
void setProperty(const QString &_name, const QString &_value)
|
void setProperty(const QString &_name, const QString &_value);
|
||||||
|
void setProperties(const QVariantHash &_props);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Stores the pre-serialized properties blob and marks the materialized
|
||||||
|
* cache as invalid. Used by the binary cache reader to avoid building a
|
||||||
|
* QVariantHash at load time.
|
||||||
|
* @param _blob The serialized properties (as written by the cache writer).
|
||||||
|
*/
|
||||||
|
void setPropertiesBlob(QByteArray _blob) const
|
||||||
{
|
{
|
||||||
properties.insert(_name, _value);
|
QMutexLocker lock(propertiesMutex.data());
|
||||||
|
propertiesBlob = std::move(_blob);
|
||||||
|
propertiesLoaded = false;
|
||||||
|
propertiesCache.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,10 @@ QString CardSet::getCorrectedShortName() const
|
||||||
|
|
||||||
void CardSet::loadSetOptions()
|
void CardSet::loadSetOptions()
|
||||||
{
|
{
|
||||||
sortKey = priorityController->getSortKey(shortName);
|
const ICardSetPriorityController::SetOptions options = priorityController->getSetOptions(shortName);
|
||||||
enabled = priorityController->isEnabled(shortName);
|
sortKey = options.sortKey;
|
||||||
isknown = priorityController->isKnown(shortName);
|
enabled = options.enabled;
|
||||||
|
isknown = options.isKnown;
|
||||||
}
|
}
|
||||||
|
|
||||||
void CardSet::setSortKey(unsigned int _sortKey)
|
void CardSet::setSortKey(unsigned int _sortKey)
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,22 @@ public:
|
||||||
bool enabled;
|
bool enabled;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Bundled per-set priority options, returned in a single call.
|
||||||
|
*
|
||||||
|
* Reading these individually (getSortKey/isEnabled/isKnown) is cheap for the
|
||||||
|
* noop controller but extremely expensive when backed by QSettings, which
|
||||||
|
* re-opens and re-parses the whole .ini on every access. Callers that need
|
||||||
|
* more than one option for the same set (notably CardSet construction during
|
||||||
|
* database load) should use getSetOptions() to avoid that cost.
|
||||||
|
*/
|
||||||
|
struct SetOptions
|
||||||
|
{
|
||||||
|
unsigned int sortKey = 0;
|
||||||
|
bool enabled = false;
|
||||||
|
bool isKnown = false;
|
||||||
|
};
|
||||||
|
|
||||||
virtual ~ICardSetPriorityController() = default;
|
virtual ~ICardSetPriorityController() = default;
|
||||||
|
|
||||||
virtual void setSortKey(QString shortName, unsigned int sortKey) = 0;
|
virtual void setSortKey(QString shortName, unsigned int sortKey) = 0;
|
||||||
|
|
@ -23,6 +39,13 @@ public:
|
||||||
virtual bool isEnabled(QString shortName) const = 0;
|
virtual bool isEnabled(QString shortName) const = 0;
|
||||||
virtual bool isKnown(QString shortName) const = 0;
|
virtual bool isKnown(QString shortName) const = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Returns the bundled priority options for a set in a single call.
|
||||||
|
* @param shortName The set's short name.
|
||||||
|
* @return The sort key, enabled and known flags for the set.
|
||||||
|
*/
|
||||||
|
virtual SetOptions getSetOptions(QString shortName) const = 0;
|
||||||
|
|
||||||
virtual void saveSets(const QVector<SetSaveData> &data) = 0;
|
virtual void saveSets(const QVector<SetSaveData> &data) = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,11 @@ public:
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SetOptions getSetOptions(QString /* shortName */) const override
|
||||||
|
{
|
||||||
|
return {0, true, true};
|
||||||
|
}
|
||||||
|
|
||||||
void saveSets(const QVector<SetSaveData> & /* data */) override
|
void saveSets(const QVector<SetSaveData> & /* data */) override
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ CardDatabaseModel::CardDatabaseModel(CardDatabase *_db, bool _showOnlyCardsFromE
|
||||||
connect(db, &CardDatabase::cardRemoved, this, &CardDatabaseModel::cardRemoved);
|
connect(db, &CardDatabase::cardRemoved, this, &CardDatabaseModel::cardRemoved);
|
||||||
connect(db, &CardDatabase::cardDatabaseEnabledSetsChanged, this,
|
connect(db, &CardDatabase::cardDatabaseEnabledSetsChanged, this,
|
||||||
&CardDatabaseModel::cardDatabaseEnabledSetsChanged);
|
&CardDatabaseModel::cardDatabaseEnabledSetsChanged);
|
||||||
|
connect(db, &CardDatabase::cardDatabaseReset, this, &CardDatabaseModel::resetCardList);
|
||||||
|
|
||||||
cardDatabaseEnabledSetsChanged();
|
cardDatabaseEnabledSetsChanged();
|
||||||
}
|
}
|
||||||
|
|
@ -151,3 +152,18 @@ void CardDatabaseModel::cardRemoved(CardInfoPtr card)
|
||||||
cardList.removeAt(row);
|
cardList.removeAt(row);
|
||||||
endRemoveRows();
|
endRemoveRows();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CardDatabaseModel::resetCardList()
|
||||||
|
{
|
||||||
|
beginResetModel();
|
||||||
|
cardList.clear();
|
||||||
|
cardListSet.clear();
|
||||||
|
for (const CardInfoPtr &card : db->getCardList()) {
|
||||||
|
if (checkCardHasAtLeastOneEnabledSet(card)) {
|
||||||
|
cardList.append(card);
|
||||||
|
cardListSet.insert(card);
|
||||||
|
connect(card.data(), &CardInfo::cardInfoChanged, this, &CardDatabaseModel::cardInfoChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
endResetModel();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ private slots:
|
||||||
void cardRemoved(CardInfoPtr card);
|
void cardRemoved(CardInfoPtr card);
|
||||||
void cardInfoChanged(const CardInfoPtr &card);
|
void cardInfoChanged(const CardInfoPtr &card);
|
||||||
void cardDatabaseEnabledSetsChanged();
|
void cardDatabaseEnabledSetsChanged();
|
||||||
|
void resetCardList();
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
#include "card_database_settings.h"
|
#include "card_database_settings.h"
|
||||||
|
|
||||||
|
#include <QMutexLocker>
|
||||||
|
|
||||||
CardDatabaseSettings::CardDatabaseSettings(const QString &settingPath, QObject *parent)
|
CardDatabaseSettings::CardDatabaseSettings(const QString &settingPath, QObject *parent)
|
||||||
: SettingsManager(settingPath + "cardDatabase.ini", QString(), QString(), parent)
|
: SettingsManager(settingPath + "cardDatabase.ini", QString(), QString(), parent)
|
||||||
{
|
{
|
||||||
|
|
@ -7,32 +9,73 @@ CardDatabaseSettings::CardDatabaseSettings(const QString &settingPath, QObject *
|
||||||
|
|
||||||
void CardDatabaseSettings::setSortKey(QString shortName, unsigned int sortKey)
|
void CardDatabaseSettings::setSortKey(QString shortName, unsigned int sortKey)
|
||||||
{
|
{
|
||||||
setValue(sortKey, "sortkey", "sets", std::move(shortName));
|
setValue(sortKey, "sortkey", "sets", shortName);
|
||||||
|
QMutexLocker lock(&setOptionsMutex);
|
||||||
|
ensureSetOptionsLoaded();
|
||||||
|
setOptionsCache[shortName].sortKey = sortKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
void CardDatabaseSettings::setEnabled(QString shortName, bool enabled)
|
void CardDatabaseSettings::setEnabled(QString shortName, bool enabled)
|
||||||
{
|
{
|
||||||
setValue(enabled, "enabled", "sets", std::move(shortName));
|
setValue(enabled, "enabled", "sets", shortName);
|
||||||
|
QMutexLocker lock(&setOptionsMutex);
|
||||||
|
ensureSetOptionsLoaded();
|
||||||
|
setOptionsCache[shortName].enabled = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
void CardDatabaseSettings::setIsKnown(QString shortName, bool isknown)
|
void CardDatabaseSettings::setIsKnown(QString shortName, bool isknown)
|
||||||
{
|
{
|
||||||
setValue(isknown, "isknown", "sets", std::move(shortName));
|
setValue(isknown, "isknown", "sets", shortName);
|
||||||
|
QMutexLocker lock(&setOptionsMutex);
|
||||||
|
ensureSetOptionsLoaded();
|
||||||
|
setOptionsCache[shortName].isKnown = isknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CardDatabaseSettings::ensureSetOptionsLoaded() const
|
||||||
|
{
|
||||||
|
if (setOptionsLoaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QSettings settings(getSettings());
|
||||||
|
settings.beginGroup("sets");
|
||||||
|
const QStringList groups = settings.childGroups();
|
||||||
|
for (const QString &group : groups) {
|
||||||
|
settings.beginGroup(group);
|
||||||
|
SetOptions &o = setOptionsCache[group];
|
||||||
|
o.sortKey = settings.value("sortkey", 0).toUInt();
|
||||||
|
o.enabled = settings.value("enabled", true).toBool();
|
||||||
|
o.isKnown = settings.value("isknown", true).toBool();
|
||||||
|
settings.endGroup();
|
||||||
|
}
|
||||||
|
setOptionsLoaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
unsigned int CardDatabaseSettings::getSortKey(QString shortName) const
|
unsigned int CardDatabaseSettings::getSortKey(QString shortName) const
|
||||||
{
|
{
|
||||||
return getValue("sortkey", "sets", std::move(shortName)).toUInt();
|
QMutexLocker lock(&setOptionsMutex);
|
||||||
|
ensureSetOptionsLoaded();
|
||||||
|
return setOptionsCache.value(shortName).sortKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CardDatabaseSettings::isEnabled(QString shortName) const
|
bool CardDatabaseSettings::isEnabled(QString shortName) const
|
||||||
{
|
{
|
||||||
return getValue("enabled", "sets", std::move(shortName)).toBool();
|
QMutexLocker lock(&setOptionsMutex);
|
||||||
|
ensureSetOptionsLoaded();
|
||||||
|
return setOptionsCache.value(shortName).enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CardDatabaseSettings::isKnown(QString shortName) const
|
bool CardDatabaseSettings::isKnown(QString shortName) const
|
||||||
{
|
{
|
||||||
return getValue("isknown", "sets", std::move(shortName)).toBool();
|
QMutexLocker lock(&setOptionsMutex);
|
||||||
|
ensureSetOptionsLoaded();
|
||||||
|
return setOptionsCache.value(shortName).isKnown;
|
||||||
|
}
|
||||||
|
|
||||||
|
ICardSetPriorityController::SetOptions CardDatabaseSettings::getSetOptions(QString shortName) const
|
||||||
|
{
|
||||||
|
QMutexLocker lock(&setOptionsMutex);
|
||||||
|
ensureSetOptionsLoaded();
|
||||||
|
return setOptionsCache.value(shortName);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CardDatabaseSettings::saveSets(const QVector<ICardSetPriorityController::SetSaveData> &data)
|
void CardDatabaseSettings::saveSets(const QVector<ICardSetPriorityController::SetSaveData> &data)
|
||||||
|
|
@ -47,4 +90,12 @@ void CardDatabaseSettings::saveSets(const QVector<ICardSetPriorityController::Se
|
||||||
}
|
}
|
||||||
s.endGroup();
|
s.endGroup();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
QMutexLocker lock(&setOptionsMutex);
|
||||||
|
ensureSetOptionsLoaded();
|
||||||
|
for (const auto &entry : data) {
|
||||||
|
SetOptions &o = setOptionsCache[entry.shortName];
|
||||||
|
o.sortKey = entry.sortKey;
|
||||||
|
o.enabled = entry.enabled;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -10,6 +10,8 @@
|
||||||
|
|
||||||
#include "settings_manager.h"
|
#include "settings_manager.h"
|
||||||
|
|
||||||
|
#include <QHash>
|
||||||
|
#include <QMutex>
|
||||||
#include <libcockatrice/interfaces/interface_card_set_priority_controller.h>
|
#include <libcockatrice/interfaces/interface_card_set_priority_controller.h>
|
||||||
|
|
||||||
class CardDatabaseSettings : public SettingsManager, public ICardSetPriorityController
|
class CardDatabaseSettings : public SettingsManager, public ICardSetPriorityController
|
||||||
|
|
@ -26,10 +28,23 @@ public:
|
||||||
bool isEnabled(QString shortName) const override;
|
bool isEnabled(QString shortName) const override;
|
||||||
bool isKnown(QString shortName) const override;
|
bool isKnown(QString shortName) const override;
|
||||||
|
|
||||||
|
SetOptions getSetOptions(QString shortName) const override;
|
||||||
|
|
||||||
void saveSets(const QVector<SetSaveData> &data) override;
|
void saveSets(const QVector<SetSaveData> &data) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
explicit CardDatabaseSettings(const QString &settingPath, QObject *parent = nullptr);
|
explicit CardDatabaseSettings(const QString &settingPath, QObject *parent = nullptr);
|
||||||
|
|
||||||
|
// In-memory cache of set priority options. QSettings re-opens and re-parses
|
||||||
|
// the whole .ini on every access, which is catastrophic when CardSet
|
||||||
|
// construction calls getSortKey/isEnabled/isKnown once per set during the
|
||||||
|
// database load (thousands of file parses). The cache is populated from a
|
||||||
|
// single QSettings session and kept consistent with the writes below.
|
||||||
|
mutable QMutex setOptionsMutex;
|
||||||
|
mutable QHash<QString, SetOptions> setOptionsCache;
|
||||||
|
mutable bool setOptionsLoaded = false;
|
||||||
|
|
||||||
|
void ensureSetOptionsLoaded() const;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // CARDDATABASESETTINGS_H
|
#endif // CARDDATABASESETTINGS_H
|
||||||
|
|
|
||||||
|
|
@ -291,12 +291,13 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList
|
||||||
|
|
||||||
// per-set properties
|
// per-set properties
|
||||||
PrintingInfo printingInfo = PrintingInfo(currentSet);
|
PrintingInfo printingInfo = PrintingInfo(currentSet);
|
||||||
|
QVariantHash printingProps;
|
||||||
for (auto i = setInfoProperties.cbegin(), end = setInfoProperties.cend(); i != end; ++i) {
|
for (auto i = setInfoProperties.cbegin(), end = setInfoProperties.cend(); i != end; ++i) {
|
||||||
QString mtgjsonProperty = i.key();
|
QString mtgjsonProperty = i.key();
|
||||||
QString xmlPropertyName = i.value();
|
QString xmlPropertyName = i.value();
|
||||||
QString propertyValue = getStringPropertyFromMap(card, mtgjsonProperty);
|
QString propertyValue = getStringPropertyFromMap(card, mtgjsonProperty);
|
||||||
if (!propertyValue.isEmpty()) {
|
if (!propertyValue.isEmpty()) {
|
||||||
printingInfo.setProperty(xmlPropertyName, propertyValue);
|
printingProps.insert(xmlPropertyName, propertyValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -304,7 +305,7 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList
|
||||||
QString faceFlavorName = getStringPropertyFromMap(card, "faceFlavorName");
|
QString faceFlavorName = getStringPropertyFromMap(card, "faceFlavorName");
|
||||||
QString flavorName = !faceFlavorName.isEmpty() ? faceFlavorName : getStringPropertyFromMap(card, "flavorName");
|
QString flavorName = !faceFlavorName.isEmpty() ? faceFlavorName : getStringPropertyFromMap(card, "flavorName");
|
||||||
if (!flavorName.isEmpty()) {
|
if (!flavorName.isEmpty()) {
|
||||||
printingInfo.setProperty("flavorName", flavorName);
|
printingProps.insert("flavorName", flavorName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Identifiers
|
// Identifiers
|
||||||
|
|
@ -313,10 +314,12 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList
|
||||||
QString xmlPropertyName = i.value();
|
QString xmlPropertyName = i.value();
|
||||||
QString propertyValue = getStringPropertyFromMap(card.value("identifiers").toMap(), mtgjsonProperty);
|
QString propertyValue = getStringPropertyFromMap(card.value("identifiers").toMap(), mtgjsonProperty);
|
||||||
if (!propertyValue.isEmpty()) {
|
if (!propertyValue.isEmpty()) {
|
||||||
printingInfo.setProperty(xmlPropertyName, propertyValue);
|
printingProps.insert(xmlPropertyName, propertyValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
printingInfo.setProperties(printingProps);
|
||||||
|
|
||||||
QString numComponent;
|
QString numComponent;
|
||||||
const QString numProperty = printingInfo.getProperty("num");
|
const QString numProperty = printingInfo.getProperty("num");
|
||||||
const QChar lastChar = numProperty.isEmpty() ? QChar() : numProperty.back();
|
const QChar lastChar = numProperty.isEmpty() ? QChar() : numProperty.back();
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,24 @@ target_link_libraries(
|
||||||
|
|
||||||
add_test(NAME carddatabase_test COMMAND carddatabase_test)
|
add_test(NAME carddatabase_test COMMAND carddatabase_test)
|
||||||
|
|
||||||
|
# ------------------------
|
||||||
|
# Load Benchmark (manual, not run in CI)
|
||||||
|
# ------------------------
|
||||||
|
add_executable(load_benchmark ${MOCKS_SOURCES} ${VERSION_STRING_CPP} load_benchmark.cpp mocks.cpp)
|
||||||
|
|
||||||
|
target_link_libraries(
|
||||||
|
load_benchmark
|
||||||
|
PRIVATE libcockatrice_card
|
||||||
|
PRIVATE libcockatrice_models
|
||||||
|
PRIVATE Threads::Threads
|
||||||
|
PRIVATE ${GTEST_BOTH_LIBRARIES}
|
||||||
|
PRIVATE ${TEST_QT_MODULES}
|
||||||
|
)
|
||||||
|
|
||||||
|
if(NOT GTEST_FOUND)
|
||||||
|
add_dependencies(load_benchmark gtest)
|
||||||
|
endif()
|
||||||
|
|
||||||
# ------------------------
|
# ------------------------
|
||||||
# Filter String Test
|
# Filter String Test
|
||||||
# (guard must match the condition for libcockatrice_filters in the root CMakeLists.txt)
|
# (guard must match the condition for libcockatrice_filters in the root CMakeLists.txt)
|
||||||
|
|
|
||||||
235
tests/carddatabase/load_benchmark.cpp
Normal file
235
tests/carddatabase/load_benchmark.cpp
Normal file
|
|
@ -0,0 +1,235 @@
|
||||||
|
/*
|
||||||
|
* Standalone benchmark for card database loading.
|
||||||
|
*
|
||||||
|
* Measures wall-clock cost and model churn:
|
||||||
|
* - Cold load (XML -> write cache)
|
||||||
|
* - Warm load (read cache)
|
||||||
|
*
|
||||||
|
* Run:
|
||||||
|
* load_benchmark [--carddb PATH] [--tokens PATH] [--spoilers PATH] [--custom DIR]
|
||||||
|
*
|
||||||
|
* All arguments are optional; they default to the XDG Cockatrice data location
|
||||||
|
* (~/.local/share/Cockatrice/Cockatrice/...).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "mocks.h"
|
||||||
|
|
||||||
|
#include "gtest/gtest.h"
|
||||||
|
#include <QAbstractItemModel>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QElapsedTimer>
|
||||||
|
#include <QEventLoop>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QLoggingCategory>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <libcockatrice/card/database/card_database.h>
|
||||||
|
#include <libcockatrice/card/database/card_database_loader.h>
|
||||||
|
#include <libcockatrice/interfaces/interface_card_database_path_provider.h>
|
||||||
|
#include <libcockatrice/interfaces/noop_card_preference_provider.h>
|
||||||
|
#include <libcockatrice/interfaces/noop_card_set_priority_controller.h>
|
||||||
|
#include <libcockatrice/models/database/card_database_model.h>
|
||||||
|
|
||||||
|
static QString defaultXdgPath(const QString &file)
|
||||||
|
{
|
||||||
|
QString base = QDir::homePath() + "/.local/share/Cockatrice/Cockatrice/";
|
||||||
|
return base + file;
|
||||||
|
}
|
||||||
|
|
||||||
|
class CliCardDatabasePathProvider : public ICardDatabasePathProvider
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
QString cardDb;
|
||||||
|
QString tokens;
|
||||||
|
QString spoilers;
|
||||||
|
QString custom;
|
||||||
|
|
||||||
|
QString getCardDatabasePath() const override
|
||||||
|
{
|
||||||
|
return cardDb;
|
||||||
|
}
|
||||||
|
QString getTokenDatabasePath() const override
|
||||||
|
{
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
QString getSpoilerCardDatabasePath() const override
|
||||||
|
{
|
||||||
|
return spoilers;
|
||||||
|
}
|
||||||
|
QString getCustomCardDatabasePath() const override
|
||||||
|
{
|
||||||
|
return custom;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
struct ChurnCounters
|
||||||
|
{
|
||||||
|
qint64 cardAddedEmissions = 0;
|
||||||
|
qint64 cardRemovedEmissions = 0;
|
||||||
|
qint64 modelRowsInserted = 0;
|
||||||
|
qint64 modelRowsRemoved = 0;
|
||||||
|
qint64 loadingFinishedSignals = 0;
|
||||||
|
qint64 databaseResetSignals = 0;
|
||||||
|
qint64 modelResets = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RunResult
|
||||||
|
{
|
||||||
|
qint64 elapsedMs = 0;
|
||||||
|
qint64 cardCount = 0;
|
||||||
|
qint64 setCount = 0;
|
||||||
|
LoadStatus status = NotLoaded;
|
||||||
|
ChurnCounters churn;
|
||||||
|
qint64 propsQueried = 0;
|
||||||
|
qint64 propTotal = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
void deleteCache(const QString &cardDbPath)
|
||||||
|
{
|
||||||
|
const QString cachePath = cardDbPath + ".cache";
|
||||||
|
if (QFile::exists(cachePath)) {
|
||||||
|
QFile::remove(cachePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RunResult runLoad(CardDatabase *db, ChurnCounters &counters)
|
||||||
|
{
|
||||||
|
const ChurnCounters before = counters;
|
||||||
|
|
||||||
|
QElapsedTimer timer;
|
||||||
|
timer.start();
|
||||||
|
|
||||||
|
db->loadCardDatabases();
|
||||||
|
|
||||||
|
RunResult result;
|
||||||
|
result.elapsedMs = timer.elapsed();
|
||||||
|
result.cardCount = db->getCardList().size();
|
||||||
|
result.setCount = db->getSetList().size();
|
||||||
|
result.status = db->getLoadStatus();
|
||||||
|
|
||||||
|
result.churn.cardAddedEmissions = counters.cardAddedEmissions - before.cardAddedEmissions;
|
||||||
|
result.churn.cardRemovedEmissions = counters.cardRemovedEmissions - before.cardRemovedEmissions;
|
||||||
|
result.churn.modelRowsInserted = counters.modelRowsInserted - before.modelRowsInserted;
|
||||||
|
result.churn.modelRowsRemoved = counters.modelRowsRemoved - before.modelRowsRemoved;
|
||||||
|
result.churn.loadingFinishedSignals = counters.loadingFinishedSignals - before.loadingFinishedSignals;
|
||||||
|
result.churn.databaseResetSignals = counters.databaseResetSignals - before.databaseResetSignals;
|
||||||
|
result.churn.modelResets = counters.modelResets - before.modelResets;
|
||||||
|
|
||||||
|
for (const CardInfoPtr &card : db->getCardList()) {
|
||||||
|
result.propTotal += card->getPropertiesHash().size();
|
||||||
|
if (!card->getProperty("colors").isEmpty()) {
|
||||||
|
++result.propsQueried;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void printResult(const QString &label, const RunResult &r)
|
||||||
|
{
|
||||||
|
qInfo() << "----------------------------------------";
|
||||||
|
qInfo() << label;
|
||||||
|
qInfo() << " Load wall-clock time :" << r.elapsedMs << "ms";
|
||||||
|
qInfo() << " Cards loaded :" << r.cardCount;
|
||||||
|
qInfo() << " Sets loaded :" << r.setCount;
|
||||||
|
qInfo() << " Load status :" << r.status;
|
||||||
|
qInfo() << " Properties queried :" << r.propsQueried << "/" << r.cardCount
|
||||||
|
<< " cards, total props:" << r.propTotal;
|
||||||
|
qInfo() << " CHURN during load:";
|
||||||
|
qInfo() << " cardAdded emissions :" << r.churn.cardAddedEmissions;
|
||||||
|
qInfo() << " cardRemoved emissions :" << r.churn.cardRemovedEmissions;
|
||||||
|
qInfo() << " model rowsInserted :" << r.churn.modelRowsInserted;
|
||||||
|
qInfo() << " model rowsRemoved :" << r.churn.modelRowsRemoved;
|
||||||
|
qInfo() << " loadingFinished signals :" << r.churn.loadingFinishedSignals;
|
||||||
|
qInfo() << " databaseReset signals :" << r.churn.databaseResetSignals;
|
||||||
|
qInfo() << " model resets (batch) :" << r.churn.modelResets;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
QCoreApplication app(argc, argv);
|
||||||
|
|
||||||
|
QStringList args = app.arguments();
|
||||||
|
CliCardDatabasePathProvider pathProvider;
|
||||||
|
pathProvider.cardDb = defaultXdgPath("cards.xml");
|
||||||
|
pathProvider.tokens = defaultXdgPath("tokens.xml");
|
||||||
|
pathProvider.spoilers = defaultXdgPath("spoiler.xml");
|
||||||
|
pathProvider.custom = defaultXdgPath("customsets/");
|
||||||
|
|
||||||
|
for (int i = 1; i < args.size(); ++i) {
|
||||||
|
const QString &a = args.at(i);
|
||||||
|
if (a == "--carddb" && i + 1 < args.size()) {
|
||||||
|
pathProvider.cardDb = args.at(++i);
|
||||||
|
} else if (a == "--tokens" && i + 1 < args.size()) {
|
||||||
|
pathProvider.tokens = args.at(++i);
|
||||||
|
} else if (a == "--spoilers" && i + 1 < args.size()) {
|
||||||
|
pathProvider.spoilers = args.at(++i);
|
||||||
|
} else if (a == "--custom" && i + 1 < args.size()) {
|
||||||
|
pathProvider.custom = args.at(++i);
|
||||||
|
} else if (a == "--help" || a == "-h") {
|
||||||
|
qInfo() << "Usage: load_benchmark [--carddb PATH] [--tokens PATH] [--spoilers PATH] [--custom DIR]";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QLoggingCategory::setFilterRules("card_database.loading=true\n"
|
||||||
|
"card_database.loading.success=true\n");
|
||||||
|
|
||||||
|
qInfo() << "=== Card Database Load Benchmark ===";
|
||||||
|
qInfo() << "carddb :" << pathProvider.cardDb;
|
||||||
|
qInfo() << "tokens :" << pathProvider.tokens;
|
||||||
|
qInfo() << "spoilers :" << pathProvider.spoilers;
|
||||||
|
qInfo() << "custom :" << pathProvider.custom;
|
||||||
|
qInfo() << "";
|
||||||
|
|
||||||
|
ChurnCounters counters;
|
||||||
|
|
||||||
|
auto runScenario = [&](bool deleteCacheBefore) -> RunResult {
|
||||||
|
if (deleteCacheBefore) {
|
||||||
|
deleteCache(pathProvider.cardDb);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fresh database each scenario so signals don't accumulate across scenarios
|
||||||
|
// for the churn counters, but we keep the same counters object to get a
|
||||||
|
// running total if desired.
|
||||||
|
CardDatabase db(nullptr, new NoopCardPreferenceProvider(), &pathProvider, new NoopCardSetPriorityController());
|
||||||
|
|
||||||
|
// Re-wire churn counters for this db instance
|
||||||
|
QObject::connect(&db, &CardDatabase::cardAdded, &app,
|
||||||
|
[&counters](const CardInfoPtr &) { ++counters.cardAddedEmissions; });
|
||||||
|
QObject::connect(&db, &CardDatabase::cardRemoved, &app,
|
||||||
|
[&counters](CardInfoPtr) { ++counters.cardRemovedEmissions; });
|
||||||
|
QObject::connect(&db, &CardDatabase::cardDatabaseLoadingFinished, &app,
|
||||||
|
[&counters]() { ++counters.loadingFinishedSignals; });
|
||||||
|
QObject::connect(&db, &CardDatabase::cardDatabaseReset, &app,
|
||||||
|
[&counters]() { ++counters.databaseResetSignals; });
|
||||||
|
|
||||||
|
CardDatabaseModel model(&db, false);
|
||||||
|
QObject::connect(&model, &QAbstractItemModel::rowsInserted, &app,
|
||||||
|
[&counters](const QModelIndex &, int, int) { ++counters.modelRowsInserted; });
|
||||||
|
QObject::connect(&model, &QAbstractItemModel::rowsRemoved, &app,
|
||||||
|
[&counters](const QModelIndex &, int, int) { ++counters.modelRowsRemoved; });
|
||||||
|
QObject::connect(&model, &QAbstractItemModel::modelReset, &app, [&counters]() { ++counters.modelResets; });
|
||||||
|
|
||||||
|
return runLoad(&db, counters);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- Scenario 1: Cold load (XML -> write cache) ----
|
||||||
|
printResult("Scenario 1: Cold load (XML -> cache)", runScenario(true));
|
||||||
|
|
||||||
|
// ---- Scenario 2: Warm load (read cache) ----
|
||||||
|
printResult("Scenario 2: Warm load (read cache)", runScenario(false));
|
||||||
|
|
||||||
|
// Teardown
|
||||||
|
deleteCache(pathProvider.cardDb);
|
||||||
|
|
||||||
|
qInfo() << "----------------------------------------";
|
||||||
|
qInfo() << "Done.";
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue