mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-07-28 11:50:25 -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
|
||||
.idea/
|
||||
*.aps
|
||||
*.cache
|
||||
cmake-build*
|
||||
preferences
|
||||
compile_commands.json
|
||||
|
|
|
|||
|
|
@ -541,6 +541,12 @@ MainWindow::MainWindow(QWidget *parent)
|
|||
|
||||
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()) {
|
||||
LocalGameOptions options;
|
||||
options.numberPlayers = SettingsCache::instance().debug().getLocalGamePlayerCount();
|
||||
|
|
@ -571,8 +577,6 @@ void MainWindow::startupConfigCheck()
|
|||
<< "differs, assuming first start after update";
|
||||
if (SettingsCache::instance().updates().getNotifyAboutNewVersion()) {
|
||||
alertForcedOracleRun(VERSION_STRING, true);
|
||||
} else {
|
||||
const auto reloadOk0 = QtConcurrent::run([] { CardDatabaseManager::getInstance()->loadCardDatabases(); });
|
||||
}
|
||||
|
||||
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.
|
||||
// On the first run after an install/update the startup is already crowded enough
|
||||
if (tip->successfulInit && SettingsCache::instance().personal().getShowTipsOnStartup() &&
|
||||
|
|
|
|||
|
|
@ -258,6 +258,19 @@ int main(int argc, char *argv[])
|
|||
|
||||
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;
|
||||
if (parser.isSet("connect")) {
|
||||
ui.setConnectTo(parser.value("connect"));
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ add_library(
|
|||
libcockatrice/card/card_info.cpp
|
||||
libcockatrice/card/card_info_comparator.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_manager.cpp
|
||||
libcockatrice/card/database/card_database_querier.cpp
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@
|
|||
#include "relation/card_relation.h"
|
||||
#include "set/card_set.h"
|
||||
|
||||
#include <QDataStream>
|
||||
#include <QDir>
|
||||
#include <QMutex>
|
||||
#include <QMutexLocker>
|
||||
#include <QRegularExpression>
|
||||
#include <QSharedPointer>
|
||||
#include <QString>
|
||||
|
|
@ -19,6 +22,57 @@ class 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,
|
||||
const QString &_text,
|
||||
bool _isToken,
|
||||
|
|
@ -27,14 +81,39 @@ CardInfo::CardInfo(const QString &_name,
|
|||
const QList<CardRelation *> &_reverseRelatedCards,
|
||||
SetToPrintingsMap _sets,
|
||||
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)
|
||||
{
|
||||
propertiesCache = std::move(_properties);
|
||||
propertiesBlob = serializeProperties(propertiesCache);
|
||||
propertiesLoaded = true;
|
||||
|
||||
simpleName = CardInfo::simplifyName(name);
|
||||
|
||||
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)
|
||||
{
|
||||
return newInstance(_name, "", false, {}, {}, {}, {}, {});
|
||||
|
|
@ -63,6 +142,35 @@ CardInfoPtr CardInfo::newInstance(const QString &_name,
|
|||
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
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
ensurePropertiesLoaded();
|
||||
QHashIterator<QString, QVariant> it(props);
|
||||
while (it.hasNext()) {
|
||||
it.next();
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
#include <QLoggingCategory>
|
||||
#include <QMap>
|
||||
#include <QMetaType>
|
||||
#include <QMutex>
|
||||
#include <QSharedPointer>
|
||||
#include <QVariant>
|
||||
#include <utility>
|
||||
|
|
@ -69,12 +70,25 @@ private:
|
|||
* @anchor PrivateCardProperties
|
||||
*/
|
||||
///@{
|
||||
CardInfoPtr smartThis; ///< Smart pointer to self for safe cross-references.
|
||||
QString name; ///< Full name of the card.
|
||||
QString simpleName; ///< Simplified name for fuzzy matching.
|
||||
QString text; ///< Text description or rules text of the card.
|
||||
bool isToken; ///< Whether this card is a token or not.
|
||||
QVariantHash properties; ///< Key-value store of dynamic card properties.
|
||||
CardInfoPtr smartThis; ///< Smart pointer to self for safe cross-references.
|
||||
QString name; ///< Full name of the card.
|
||||
QString simpleName; ///< Simplified name for fuzzy matching.
|
||||
QString text; ///< Text description or rules text of the card.
|
||||
bool isToken; ///< Whether this card is a token or not.
|
||||
// Properties are stored as a pre-serialized blob (cheap to load) and the
|
||||
// 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 *> reverseRelatedCards; ///< Cards that refer back to this card.
|
||||
QList<CardRelation *> reverseRelatedCardsToMe; ///< Cards that consider this card as related.
|
||||
|
|
@ -106,6 +120,38 @@ public:
|
|||
SetToPrintingsMap _sets,
|
||||
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.
|
||||
*
|
||||
|
|
@ -115,7 +161,7 @@ public:
|
|||
*/
|
||||
CardInfo(const CardInfo &other)
|
||||
: 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),
|
||||
setsToPrintings(other.setsToPrintings), uiAttributes(other.uiAttributes), setsNames(other.setsNames),
|
||||
altNames(other.altNames)
|
||||
|
|
@ -154,6 +200,38 @@ public:
|
|||
SetToPrintingsMap _sets,
|
||||
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.
|
||||
*
|
||||
|
|
@ -208,20 +286,32 @@ public:
|
|||
}
|
||||
[[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
|
||||
{
|
||||
return properties.value(propertyName).toString();
|
||||
}
|
||||
void setProperty(const QString &_name, const QString &_value)
|
||||
{
|
||||
properties.insert(_name, _value);
|
||||
emit cardInfoChanged(smartThis);
|
||||
return getPropertiesHash().value(propertyName).toString();
|
||||
}
|
||||
void setProperty(const QString &_name, const QString &_value);
|
||||
void setProperties(const QVariantHash &_props);
|
||||
[[nodiscard]] bool hasProperty(const QString &propertyName) const
|
||||
{
|
||||
return properties.contains(propertyName);
|
||||
return getPropertiesHash().contains(propertyName);
|
||||
}
|
||||
[[nodiscard]] const SetToPrintingsMap &getSets() const
|
||||
{
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ CardDatabase::CardDatabase(QObject *parent,
|
|||
{
|
||||
qRegisterMetaType<CardInfoPtr>("CardInfoPtr");
|
||||
qRegisterMetaType<CardInfoPtr>("CardSetPtr");
|
||||
qRegisterMetaType<CardDatabaseData>("CardDatabaseData");
|
||||
|
||||
// create loader and wire it up
|
||||
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::newSetsFound, this, &CardDatabase::cardDatabaseNewSetsFound);
|
||||
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);
|
||||
}
|
||||
|
|
@ -66,7 +71,12 @@ void CardDatabase::reloadCardDatabasesAndNotify()
|
|||
loadCardDatabases();
|
||||
|
||||
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()
|
||||
{
|
||||
for (const auto &card : cards) {
|
||||
refreshCachedReverseRelatedCards(cards);
|
||||
}
|
||||
|
||||
void CardDatabase::refreshCachedReverseRelatedCards(CardNameMap &cardMap)
|
||||
{
|
||||
for (const auto &card : cardMap) {
|
||||
card->resetReverseRelatedCards2Me();
|
||||
}
|
||||
|
||||
for (const auto &card : cards) {
|
||||
for (const auto &card : cardMap) {
|
||||
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(),
|
||||
rel->getIsVariable(), rel->getDefaultCount(), rel->getIsPersistent(),
|
||||
rel->getIsFaceDown());
|
||||
|
|
@ -205,11 +220,15 @@ void CardDatabase::markAllSetsAsKnown()
|
|||
_sets.markAllAsKnown();
|
||||
}
|
||||
|
||||
void CardDatabase::notifyEnabledSetsChanged()
|
||||
void CardDatabase::notifyEnabledSetsChanged(bool recomputeCachedSets)
|
||||
{
|
||||
// refresh the list of cached set names
|
||||
for (const CardInfoPtr &card : cards) {
|
||||
card->refreshCachedSets();
|
||||
if (recomputeCachedSets) {
|
||||
// refresh the list of cached set names / alt names for every card; this is
|
||||
// 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
|
||||
|
|
@ -220,3 +239,30 @@ void CardDatabase::addFormat(const FormatRulesPtr &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
|
||||
|
||||
#include "../set/card_set_list.h"
|
||||
#include "card_database_data.h"
|
||||
#include "card_database_loader.h"
|
||||
#include "card_database_querier.h"
|
||||
|
||||
|
|
@ -54,16 +55,17 @@ protected:
|
|||
CardDatabaseQuerier *querier;
|
||||
|
||||
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.
|
||||
*/
|
||||
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. */
|
||||
QBasicMutex *clearDatabaseMutex = new QBasicMutex(), *addCardMutex = new QBasicMutex(),
|
||||
*removeCardMutex = new QBasicMutex();
|
||||
|
|
@ -127,8 +129,25 @@ public:
|
|||
/** @brief Marks all sets as known. */
|
||||
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()
|
||||
{
|
||||
|
|
@ -159,10 +178,26 @@ public slots:
|
|||
*/
|
||||
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:
|
||||
/** @brief Emitted when the card database has finished loading successfully. */
|
||||
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. */
|
||||
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.h"
|
||||
#include "card_database_cache.h"
|
||||
#include "parser/card_database_parser.h"
|
||||
#include "parser/cockatrice_xml_3.h"
|
||||
#include "parser/cockatrice_xml_4.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QCoreApplication>
|
||||
#include <QCryptographicHash>
|
||||
#include <QDebug>
|
||||
#include <QDirIterator>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QTime>
|
||||
|
||||
CardDatabaseLoader::CardDatabaseLoader(QObject *parent,
|
||||
|
|
@ -14,18 +20,14 @@ CardDatabaseLoader::CardDatabaseLoader(QObject *parent,
|
|||
ICardDatabasePathProvider *_pathProvider,
|
||||
ICardPreferenceProvider *_preferenceProvider,
|
||||
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 CockatriceXml3Parser(_priorityController);
|
||||
|
||||
for (auto *p : availableParsers) {
|
||||
// connect parser outputs to the database adders
|
||||
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);
|
||||
}
|
||||
// The load path parses into a snapshot and never emits per-card signals;
|
||||
// the finished snapshot is swapped into the live database on the GUI thread.
|
||||
|
||||
// when SettingsCache's path changes, trigger reloads
|
||||
connect(pathProvider, &ICardDatabasePathProvider::cardDatabasePathChanged, this,
|
||||
|
|
@ -38,7 +40,7 @@ CardDatabaseLoader::~CardDatabaseLoader()
|
|||
availableParsers.clear();
|
||||
}
|
||||
|
||||
LoadStatus CardDatabaseLoader::loadFromFile(const QString &fileName)
|
||||
LoadStatus CardDatabaseLoader::loadFromFile(const QString &fileName, CardDatabaseData &data)
|
||||
{
|
||||
QFile file(fileName);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
|
|
@ -49,7 +51,7 @@ LoadStatus CardDatabaseLoader::loadFromFile(const QString &fileName)
|
|||
file.reset();
|
||||
if (parser->getCanParseFile(fileName, file)) {
|
||||
file.reset();
|
||||
parser->parseFile(file);
|
||||
parser->parseFileInto(file, data);
|
||||
return Ok;
|
||||
}
|
||||
}
|
||||
|
|
@ -57,24 +59,29 @@ LoadStatus CardDatabaseLoader::loadFromFile(const QString &fileName)
|
|||
return Invalid;
|
||||
}
|
||||
|
||||
LoadStatus CardDatabaseLoader::loadCardDatabase(const QString &path)
|
||||
LoadStatus CardDatabaseLoader::loadCardDatabase(const QString &path, CardDatabaseData &data)
|
||||
{
|
||||
auto startTime = QTime::currentTime();
|
||||
LoadStatus tempLoadStatus = NotLoaded;
|
||||
if (!path.isEmpty()) {
|
||||
QMutexLocker locker(loadFromFileMutex);
|
||||
tempLoadStatus = loadFromFile(path);
|
||||
tempLoadStatus = loadFromFile(path, data);
|
||||
}
|
||||
|
||||
int msecs = startTime.msecsTo(QTime::currentTime());
|
||||
qCInfo(CardDatabaseLoadingLog) << "Loaded card database: Path =" << path << "Status =" << tempLoadStatus
|
||||
<< "Cards =" << (database ? database->cards.size() : 0)
|
||||
<< "Sets =" << (database ? database->sets.size() : 0) << QString("%1ms").arg(msecs);
|
||||
<< "Cards =" << data.cards.size() << "Sets =" << data.sets.size()
|
||||
<< QString("%1ms").arg(msecs);
|
||||
|
||||
return tempLoadStatus;
|
||||
}
|
||||
|
||||
LoadStatus CardDatabaseLoader::loadCardDatabases()
|
||||
{
|
||||
return doLoadCardDatabases();
|
||||
}
|
||||
|
||||
LoadStatus CardDatabaseLoader::doLoadCardDatabases()
|
||||
{
|
||||
QMutexLocker locker(reloadDatabaseMutex);
|
||||
|
||||
|
|
@ -86,31 +93,55 @@ LoadStatus CardDatabaseLoader::loadCardDatabases()
|
|||
emit loadingStarted();
|
||||
qCInfo(CardDatabaseLoadingLog) << "Card Database Loading Started";
|
||||
|
||||
database->clear(); // remove old db
|
||||
ICardDatabaseParser::clearSetlist();
|
||||
|
||||
LoadStatus loadStatus = loadCardDatabase(pathProvider->getCardDatabasePath()); // load main card database
|
||||
loadCardDatabase(pathProvider->getTokenDatabasePath()); // load tokens database
|
||||
loadCardDatabase(pathProvider->getSpoilerCardDatabasePath()); // load spoilers database
|
||||
CardDatabaseData data;
|
||||
LoadStatus loadStatus = NotLoaded;
|
||||
|
||||
// find all custom card databases, recursively & following symlinks
|
||||
// then load them alphabetically
|
||||
// Try the binary cache first: a cache hit avoids re-parsing the (large) XML.
|
||||
const QStringList customPaths = collectCustomDatabasePaths();
|
||||
for (int i = 0; i < customPaths.size(); ++i) {
|
||||
const auto &p = customPaths.at(i);
|
||||
qCInfo(CardDatabaseLoadingLog) << "Loading Custom Set" << i << "(" << p << ")";
|
||||
loadCardDatabase(p);
|
||||
const QByteArray sourceHash = computeSourceHash(customPaths);
|
||||
if (loadFromCache(data, sourceHash)) {
|
||||
qCInfo(CardDatabaseLoadingLog) << "Loaded card database from binary cache";
|
||||
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
|
||||
|
||||
// resolve the reverse-related tags
|
||||
|
||||
database->refreshCachedReverseRelatedCards();
|
||||
|
||||
// AFTER all the cards have been loaded: resolve the reverse-related tags
|
||||
// against the fully-built snapshot.
|
||||
if (loadStatus == Ok) {
|
||||
database->checkUnknownSets(); // update deck editors, etc
|
||||
database->refreshCachedReverseRelatedCards(data.cards);
|
||||
|
||||
qCInfo(CardDatabaseLoadingSuccessOrFailureLog) << "Card Database Loading Success";
|
||||
emit databaseDataReady(std::move(data));
|
||||
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 {
|
||||
qCInfo(CardDatabaseLoadingSuccessOrFailureLog) << "Card Database Loading Failed";
|
||||
emit loadingFailed(); // bring up the settings dialog
|
||||
|
|
@ -119,6 +150,55 @@ LoadStatus CardDatabaseLoader::loadCardDatabases()
|
|||
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
|
||||
{
|
||||
QDirIterator it(pathProvider->getCustomCardDatabasePath(), {"*.xml"}, QDir::Files,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include <QBasicMutex>
|
||||
#include <QList>
|
||||
#include <QLoggingCategory>
|
||||
#include <libcockatrice/card/database/card_database_data.h>
|
||||
#include <libcockatrice/interfaces/interface_card_database_path_provider.h>
|
||||
#include <libcockatrice/interfaces/interface_card_preference_provider.h>
|
||||
#include <libcockatrice/interfaces/interface_card_set_priority_controller.h>
|
||||
|
|
@ -62,16 +63,20 @@ public:
|
|||
public slots:
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
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 data Snapshot to populate.
|
||||
* @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.
|
||||
|
|
@ -89,6 +94,12 @@ signals:
|
|||
/** @brief Emitted when loading fails. */
|
||||
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.
|
||||
* @param numSets Number of new sets.
|
||||
|
|
@ -101,11 +112,18 @@ signals:
|
|||
|
||||
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 data Snapshot to populate.
|
||||
* @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.
|
||||
|
|
@ -113,13 +131,43 @@ private:
|
|||
*/
|
||||
[[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:
|
||||
CardDatabase *database; /**< Non-owning pointer to the target CardDatabase. */
|
||||
ICardDatabasePathProvider *pathProvider; /**< Pointer to the path provider. */
|
||||
QList<ICardDatabaseParser *> availableParsers; /**< List of available parsers for different formats. */
|
||||
CardDatabase *database; /**< Non-owning pointer to the target CardDatabase. */
|
||||
ICardDatabasePathProvider *pathProvider; /**< Pointer to the path provider. */
|
||||
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 *reloadDatabaseMutex = new QBasicMutex(); /**< Mutex for reloading entire database. */
|
||||
bool initialLoadComplete = false; /**< Set after the first successful load. */
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_CARD_DATABASE_LOADER_H
|
||||
|
|
|
|||
|
|
@ -30,6 +30,10 @@ CardSetPtr ICardDatabaseParser::internalAddSet(const QString &setName,
|
|||
newSet->setPriority(priority);
|
||||
|
||||
sets.insert(setName, newSet);
|
||||
emit addSet(newSet);
|
||||
if (targetData) {
|
||||
targetData->sets.insert(setName, newSet);
|
||||
} else {
|
||||
emit addSet(newSet);
|
||||
}
|
||||
return newSet;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
#define CARDDATABASE_PARSER_H
|
||||
|
||||
#include "../../card_info.h"
|
||||
#include "../card_database_data.h"
|
||||
|
||||
#include <QIODevice>
|
||||
#include <QString>
|
||||
|
|
@ -37,6 +38,15 @@ public:
|
|||
*/
|
||||
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.
|
||||
* @param _formats
|
||||
|
|
@ -62,6 +72,21 @@ protected:
|
|||
static SetNameMap sets;
|
||||
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.
|
||||
* @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)
|
||||
{
|
||||
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.
|
||||
if (set->getEnabled()) {
|
||||
PrintingInfo setInfo(set);
|
||||
QVariantHash printingProps;
|
||||
if (attrs.hasAttribute("muId")) {
|
||||
setInfo.setProperty("muid", attrs.value("muId").toString());
|
||||
printingProps.insert("muid", attrs.value("muId").toString());
|
||||
}
|
||||
|
||||
if (attrs.hasAttribute("uuId")) {
|
||||
setInfo.setProperty("uuid", attrs.value("uuId").toString());
|
||||
printingProps.insert("uuid", attrs.value("uuId").toString());
|
||||
}
|
||||
|
||||
if (attrs.hasAttribute("picURL")) {
|
||||
setInfo.setProperty("picurl", attrs.value("picURL").toString());
|
||||
printingProps.insert("picurl", attrs.value("picURL").toString());
|
||||
}
|
||||
|
||||
if (attrs.hasAttribute("num")) {
|
||||
setInfo.setProperty("num", attrs.value("num").toString());
|
||||
printingProps.insert("num", attrs.value("num").toString());
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
// related cards
|
||||
|
|
@ -299,7 +308,20 @@ void CockatriceXml3Parser::loadCardsFromXml(QXmlStreamReader &xml)
|
|||
.upsideDownArt = upsideDown};
|
||||
CardInfoPtr newCard = CardInfo::newInstance(name, text, isToken, properties, relatedCards,
|
||||
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;
|
||||
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
auto rulesPtr = FormatRulesPtr(new FormatRules());
|
||||
|
|
@ -187,7 +194,11 @@ void CockatriceXml4Parser::loadFormats(QXmlStreamReader &xml)
|
|||
|
||||
if (xml.name().toString() == "format") {
|
||||
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);
|
||||
if (set->getEnabled()) {
|
||||
PrintingInfo printingInfo(set);
|
||||
QVariantHash printingProps;
|
||||
for (QXmlStreamAttribute attr : attrs) {
|
||||
QString attrName = attr.name().toString();
|
||||
if (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
|
||||
// put this check, as it requires a reload of Cockatrice
|
||||
|
|
@ -389,7 +402,22 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml)
|
|||
.upsideDownArt = upsideDown};
|
||||
CardInfoPtr newCard = CardInfo::newInstance(name, text, isToken, properties, relatedCards,
|
||||
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;
|
||||
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -2,19 +2,56 @@
|
|||
|
||||
#include "../set/card_set.h"
|
||||
|
||||
#include <QDataStream>
|
||||
#include <QIODevice>
|
||||
|
||||
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
|
||||
*/
|
||||
QString PrintingInfo::getUuid() const
|
||||
{
|
||||
return properties.value("uuid").toString();
|
||||
return getPropertiesHash().value("uuid").toString();
|
||||
}
|
||||
|
||||
QString PrintingInfo::getFlavorName() const
|
||||
{
|
||||
return properties.value("flavorName").toString();
|
||||
return getPropertiesHash().value("flavorName").toString();
|
||||
}
|
||||
|
|
@ -5,6 +5,8 @@
|
|||
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QMutex>
|
||||
#include <QSharedPointer>
|
||||
#include <QVariant>
|
||||
|
||||
class PrintingInfo;
|
||||
|
|
@ -51,7 +53,7 @@ public:
|
|||
*/
|
||||
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
|
||||
{
|
||||
return set == nullptr && properties.isEmpty();
|
||||
return set == nullptr && getPropertiesHash().isEmpty();
|
||||
}
|
||||
|
||||
private:
|
||||
CardSetPtr set; ///< The set this variation belongs to.
|
||||
QVariantHash properties; ///< Key-value store for variation-specific attributes.
|
||||
CardSetPtr set; ///< The set this variation belongs to.
|
||||
|
||||
// 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:
|
||||
/**
|
||||
|
|
@ -86,7 +98,13 @@ public:
|
|||
*/
|
||||
[[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
|
||||
{
|
||||
return properties.value(propertyName).toString();
|
||||
return getPropertiesHash().value(propertyName).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -108,9 +126,21 @@ public:
|
|||
* @param _name The name of the property.
|
||||
* @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()
|
||||
{
|
||||
sortKey = priorityController->getSortKey(shortName);
|
||||
enabled = priorityController->isEnabled(shortName);
|
||||
isknown = priorityController->isKnown(shortName);
|
||||
const ICardSetPriorityController::SetOptions options = priorityController->getSetOptions(shortName);
|
||||
sortKey = options.sortKey;
|
||||
enabled = options.enabled;
|
||||
isknown = options.isKnown;
|
||||
}
|
||||
|
||||
void CardSet::setSortKey(unsigned int _sortKey)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,22 @@ public:
|
|||
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 void setSortKey(QString shortName, unsigned int sortKey) = 0;
|
||||
|
|
@ -23,6 +39,13 @@ public:
|
|||
virtual bool isEnabled(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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,11 @@ public:
|
|||
return true;
|
||||
}
|
||||
|
||||
SetOptions getSetOptions(QString /* shortName */) const override
|
||||
{
|
||||
return {0, true, true};
|
||||
}
|
||||
|
||||
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::cardDatabaseEnabledSetsChanged, this,
|
||||
&CardDatabaseModel::cardDatabaseEnabledSetsChanged);
|
||||
connect(db, &CardDatabase::cardDatabaseReset, this, &CardDatabaseModel::resetCardList);
|
||||
|
||||
cardDatabaseEnabledSetsChanged();
|
||||
}
|
||||
|
|
@ -151,3 +152,18 @@ void CardDatabaseModel::cardRemoved(CardInfoPtr card)
|
|||
cardList.removeAt(row);
|
||||
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 cardInfoChanged(const CardInfoPtr &card);
|
||||
void cardDatabaseEnabledSetsChanged();
|
||||
void resetCardList();
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
#include "card_database_settings.h"
|
||||
|
||||
#include <QMutexLocker>
|
||||
|
||||
CardDatabaseSettings::CardDatabaseSettings(const QString &settingPath, QObject *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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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
|
||||
{
|
||||
return getValue("sortkey", "sets", std::move(shortName)).toUInt();
|
||||
QMutexLocker lock(&setOptionsMutex);
|
||||
ensureSetOptionsLoaded();
|
||||
return setOptionsCache.value(shortName).sortKey;
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
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)
|
||||
|
|
@ -47,4 +90,12 @@ void CardDatabaseSettings::saveSets(const QVector<ICardSetPriorityController::Se
|
|||
}
|
||||
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 <QHash>
|
||||
#include <QMutex>
|
||||
#include <libcockatrice/interfaces/interface_card_set_priority_controller.h>
|
||||
|
||||
class CardDatabaseSettings : public SettingsManager, public ICardSetPriorityController
|
||||
|
|
@ -26,10 +28,23 @@ public:
|
|||
bool isEnabled(QString shortName) const override;
|
||||
bool isKnown(QString shortName) const override;
|
||||
|
||||
SetOptions getSetOptions(QString shortName) const override;
|
||||
|
||||
void saveSets(const QVector<SetSaveData> &data) override;
|
||||
|
||||
private:
|
||||
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
|
||||
|
|
|
|||
|
|
@ -291,12 +291,13 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList
|
|||
|
||||
// per-set properties
|
||||
PrintingInfo printingInfo = PrintingInfo(currentSet);
|
||||
QVariantHash printingProps;
|
||||
for (auto i = setInfoProperties.cbegin(), end = setInfoProperties.cend(); i != end; ++i) {
|
||||
QString mtgjsonProperty = i.key();
|
||||
QString xmlPropertyName = i.value();
|
||||
QString propertyValue = getStringPropertyFromMap(card, mtgjsonProperty);
|
||||
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 flavorName = !faceFlavorName.isEmpty() ? faceFlavorName : getStringPropertyFromMap(card, "flavorName");
|
||||
if (!flavorName.isEmpty()) {
|
||||
printingInfo.setProperty("flavorName", flavorName);
|
||||
printingProps.insert("flavorName", flavorName);
|
||||
}
|
||||
|
||||
// Identifiers
|
||||
|
|
@ -313,10 +314,12 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList
|
|||
QString xmlPropertyName = i.value();
|
||||
QString propertyValue = getStringPropertyFromMap(card.value("identifiers").toMap(), mtgjsonProperty);
|
||||
if (!propertyValue.isEmpty()) {
|
||||
printingInfo.setProperty(xmlPropertyName, propertyValue);
|
||||
printingProps.insert(xmlPropertyName, propertyValue);
|
||||
}
|
||||
}
|
||||
|
||||
printingInfo.setProperties(printingProps);
|
||||
|
||||
QString numComponent;
|
||||
const QString numProperty = printingInfo.getProperty("num");
|
||||
const QChar lastChar = numProperty.isEmpty() ? QChar() : numProperty.back();
|
||||
|
|
|
|||
|
|
@ -21,6 +21,24 @@ target_link_libraries(
|
|||
|
||||
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
|
||||
# (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