Cockatrice/libcockatrice_card/libcockatrice/card/database/card_database.h
BruebachL 00151380e3
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
[CardDB] Break the CardInfo <-> CardSet cycle on reload (#7645)
* [CardDB] Drop the CardInfo self pointer

The weak self pointer had to be assigned right after every construction,
and a missed assignment silently stopped change notifications. CardInfo
is always owned by a QSharedPointer, so it now derives from
QEnableSharedFromThis<CardInfo> and asks for sharedFromThis() instead:
nothing to initialize, and the CardSet cycle stays broken.

CardSet::newInstance() also loses the commented-out self-pointer
assignment left over from the same design.

* [CardDB] Pin the cardInfoChanged contract without the self pointer

Dropping CardInfo's self pointer moved change notifications onto
QEnableSharedFromThis, so the signal now rests on the card being owned by
a QSharedPointer. Two tests pin that down:

- A property change on a card owned by a CardDatabase still emits
  cardInfoChanged() carrying that card, which is what the card database
  views and models listen to.
- clone() starts out unowned, so a change to the clone is reported as the
  clone's and must not reach the original's observers.

* [CardDB] Treat an unowned CardInfo as a bug, not a no-op

Every CardInfo is built inside a QSharedPointer (both factory overloads
and clone()), and no constructor notifies anyone, so sharedFromThis() is
always available by the time addToSet() or a setter runs. Assert that and
warn in a released build, instead of silently skipping the notification
or the set append.

The copy constructor keeps its explicit QEnableSharedFromThis
initializer: GCC's -Wextra warns about the uninitialized base in every
translation unit that copies a CardInfo otherwise. A comment there
records that the clone deliberately starts unowned.

* [CardDB] Break the CardInfo <-> CardSet cycle on reload

CardSet is a QList<CardInfoPtr> and CardInfo::addToSet() appends the card
to its sets, while a card keeps a CardSetPtr per printing in
setsToPrintings. The sets therefore hold strong references back to the
cards, and the previous database snapshot stayed alive after a reload, so
RSS grew by roughly a second database on every reload.

Empty the sets before the cards are dropped: CardDatabase::clear() and
swapInDatabaseData() clear the sets before replacing the containers, and
removeCard() detaches the card from its sets, so nothing keeps a card
alive on behalf of a set once the database lets go of it.

Two tests pin the behaviour: a reload and a clear() must drop the last
strong reference to a card from the previous snapshot.

* [CardDB] Keep the set detach in one place

clear() and swapInDatabaseData() both emptied every set before dropping the
cards, so share that as detachSetMembership() instead of two copies that have
to stay in sync.

removeCard() now detaches through the first printing of each set directly, the
same way CardInfo::newInstance() appends, instead of an inner loop that always
breaks after the first item.

Because clear() empties the sets up front, its per-card detach was walking
every card's printings to remove it from sets that were already empty. Split
removeCard() into the per-card detach and unindexCard(), and let clear() call
only the latter.

* [CardDB] Test that removeCard detaches the card from its set

removeCard() is the one teardown path a caller can trigger for a single card
(the token editor uses it), and it now detaches the card from its sets as
well. Without that detach the set's strong CardInfoPtr keeps the removed card
alive forever, which is exactly what the reload and clear tests pin for the
whole-snapshot paths.

The test loads the fixture, removes Cat, and requires the card to be gone from
the CAT set and to be destroyed once the test drops its own reference.

* [Servatrice] Drop the settings cache from the logger

#7602 removed #include "settingscache.h" from server_logger.cpp but left
isLoggingEnabled() reading settingsCache, so master no longer compiles: the
servatrice target fails on the undeclared global and server_logger_test, which
compiles the same file without main.cpp, fails to link it.

The same commit made reloadConfig() the single place that stores writelog and
the log filters, precisely so logging threads stop reading the global QSettings.
isLoggingEnabled() now mirrors logMessage() and reads those members instead: the
console is a sink of its own, and the logfile needs an open file plus writelog.
The mutex is mutable so the const query can take it.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-31 19:37:15 +01:00

256 lines
8.3 KiB
C++

#ifndef CARDDATABASE_H
#define CARDDATABASE_H
#include "../set/card_set_list.h"
#include "card_database_loader.h"
// IWYU pragma: keep
// CardDatabaseData is passed by value to the swapInDatabaseData() slot, and
// CardInfoPtr is passed by value to the cardAdded()/cardRemoved() signals, so
// the moc-generated code needs the complete types.
#include "card_database_data.h"
#include "libcockatrice/card/card_info.h"
#include <QBasicMutex>
#include <QLoggingCategory>
#include <QObject>
#include <QString>
#include <QStringList>
#include <qtmetamacros.h>
class CardDatabaseQuerier;
class ICardDatabasePathProvider;
class ICardPreferenceProvider;
class ICardSetPriorityController;
inline Q_LOGGING_CATEGORY(CardDatabaseLog, "card_database");
/**
* @class CardDatabase
* @ingroup CardDatabase
* @brief Core in-memory container for card and set data.
*
* Responsible for maintaining CardInfo objects, CardSet objects, and
* providing access to CardDatabaseQuerier for query operations.
* Handles addition, removal, and clearing of cards and sets.
*/
class CardDatabase : public QObject
{
Q_OBJECT
protected:
/** @brief Controller to determine set priority when choosing preferred printings. */
ICardSetPriorityController *setPriorityController;
/** @brief Cards indexed by exact name. */
CardNameMap cards;
/** @brief Cards indexed by simplified name (normalized). */
CardNameMap simpleNameCards;
/** @brief Sets indexed by short name. */
SetNameMap sets;
FormatRulesNameMap formats;
/** @brief Loader responsible for file discovery and parsing. */
CardDatabaseLoader *loader;
/** @brief Current load status of the database. */
LoadStatus loadStatus;
/** @brief Querier for higher-level card lookups. */
CardDatabaseQuerier *querier;
private:
/**
* @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 Empties every set, dropping the strong card references they hold.
*
* A card keeps a CardSetPtr through its printings while its set keeps a strong CardInfoPtr, so the
* two form a reference cycle. Dropping a whole snapshot has to break that cycle once, up front,
* instead of relying on the per-card detach in removeCard().
*/
void detachSetMembership();
/**
* @brief Drops a card from the database indexes and notifies observers.
*
* Callers are responsible for detaching the card from its sets first.
* @param card Pointer to the card to unindex.
*/
void unindexCard(const CardInfoPtr &card);
/** @brief Mutexes for thread safety. */
QBasicMutex *clearDatabaseMutex = new QBasicMutex(), *addCardMutex = new QBasicMutex(),
*removeCardMutex = new QBasicMutex();
public:
/**
* @brief Constructs a new CardDatabase instance.
* @param parent QObject parent.
* @param prefs Optional card preference provider.
* @param pathProvider Optional database path provider.
* @param setPriorityController Optional controller for set priority.
*/
explicit CardDatabase(QObject *parent = nullptr,
ICardPreferenceProvider *prefs = nullptr,
ICardDatabasePathProvider *pathProvider = nullptr,
ICardSetPriorityController *setPriorityController = nullptr);
/** @brief Destructor clears all internal data. */
~CardDatabase() override;
/**
* @brief Removes a card from the database.
* @param card Pointer to the card to remove.
*/
void removeCard(const CardInfoPtr &card);
/** @brief Clears all cards, sets, and internal state. */
void clear();
/** @brief Returns the map of cards by name. */
[[nodiscard]] const CardNameMap &getCardList() const
{
return cards;
}
/**
* @brief Retrieves a set by short name, creating a new one if missing.
* @param setName Short name of the set.
* @return Pointer to the CardSet.
*/
CardSetPtr getSet(const QString &setName);
/** @brief Returns a list of all sets in the database. */
[[nodiscard]] CardSetList getSetList() const;
/** @brief Returns the current load status. */
[[nodiscard]] LoadStatus getLoadStatus() const
{
return loadStatus;
}
/** @brief Returns the querier for performing card lookups. */
[[nodiscard]] CardDatabaseQuerier *query() const
{
return querier;
}
/** @brief Enables all unknown sets in the database. */
void enableAllUnknownSets();
/** @brief Marks all sets as known. */
void markAllSetsAsKnown();
/**
* @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()
{
return setPriorityController;
}
public slots:
/**
* @brief Adds a card to the database.
* @param card CardInfoPtr to add.
*/
void addCard(const CardInfoPtr &card);
/**
* @brief Adds a set to the database.
* @param set Pointer to CardSet to add.
*/
void addSet(const CardSetPtr &set);
void addFormat(const FormatRulesPtr &format);
/** @brief Loads card databases from configured paths, using the binary cache when valid. */
void loadCardDatabases();
/**
* @brief Reloads card databases from disk, bypassing the binary cache,
* and notifies observers of the fresh state.
*/
void reloadCardDatabasesAndNotify();
/** @brief Saves custom tokens to file.
* @return True if successful.
*/
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();
/**
* @brief Emitted when new sets are found.
* @param numUnknownSets Number of unknown sets.
* @param unknownSetsNames Names of unknown sets.
*/
void cardDatabaseNewSetsFound(int numUnknownSets, QStringList unknownSetsNames);
/** @brief Emitted when all new sets have been enabled. */
void cardDatabaseAllNewSetsEnabled();
/** @brief Emitted when enabled sets have changed. */
void cardDatabaseEnabledSetsChanged();
/** @brief Emitted when a new card is added. */
void cardAdded(CardInfoPtr card);
/** @brief Emitted when a card is removed. */
void cardRemoved(CardInfoPtr card);
friend class CardDatabaseLoader;
friend class CardDatabaseQuerier;
};
#endif // CARDDATABASE_H