Cockatrice/libcockatrice_card/libcockatrice/card/database/card_database.cpp
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

328 lines
11 KiB
C++

#include "card_database.h"
#include "../relation/card_relation.h"
#include "libcockatrice/card/database/card_database_data.h"
#include "libcockatrice/card/database/card_database_loader.h"
#include "libcockatrice/card/database/card_database_querier.h"
#include "libcockatrice/card/database/parser/card_database_parser.h"
#include "libcockatrice/card/format/format_legality_rules.h"
#include "libcockatrice/card/printing/printing_info.h"
#include "libcockatrice/card/set/card_set.h"
#include "libcockatrice/card/set/card_set_list.h"
#include <QDebug>
#include <QList>
#include <QMessageLogger>
#include <QMetaObject>
#include <QMetaType>
#include <QMutexLocker>
#include <QSharedPointer>
#include <utility>
class ICardDatabasePathProvider;
class ICardPreferenceProvider;
class ICardSetPriorityController;
CardDatabase::CardDatabase(QObject *parent,
ICardPreferenceProvider *prefs,
ICardDatabasePathProvider *pathProvider,
ICardSetPriorityController *_setPriorityController)
: QObject(parent), setPriorityController(_setPriorityController), loadStatus(NotLoaded)
{
qRegisterMetaType<CardInfoPtr>("CardInfoPtr");
qRegisterMetaType<CardInfoPtr>("CardSetPtr");
qRegisterMetaType<CardDatabaseData>("CardDatabaseData");
// create loader and wire it up
loader = new CardDatabaseLoader(this, this, pathProvider, setPriorityController);
// re-emit loader signals (so other code doesn't need to know about internals)
connect(loader, &CardDatabaseLoader::loadingFinished, this, &CardDatabase::cardDatabaseLoadingFinished);
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);
}
CardDatabase::~CardDatabase()
{
clear();
}
void CardDatabase::detachSetMembership()
{
for (const auto &set : sets) {
if (set) {
set->clear();
}
}
}
void CardDatabase::clear()
{
QMutexLocker locker(clearDatabaseMutex);
// The sets hold strong card references, so empty them before dropping the cards: this breaks the
// CardInfo <-> CardSet cycle and spares the per-card detach over the whole database.
detachSetMembership();
for (const auto &card : cards.values()) {
if (card) {
unindexCard(card);
}
}
cards.clear();
simpleNameCards.clear();
sets.clear();
ICardDatabaseParser::clearSetlist();
loadStatus = NotLoaded;
}
void CardDatabase::loadCardDatabases()
{
loadStatus = loader->loadCardDatabases();
}
void CardDatabase::reloadCardDatabasesAndNotify()
{
// An explicit reload bypasses the binary cache so the files on disk are
// always re-read, even when their size/mtime hash still matches the cache.
// Models refresh from the swap's cardDatabaseReset in a single batch, so
// no cardDatabaseEnabledSetsChanged is emitted here: that rebuilt the
// 36k-card models a second time on the GUI thread, which froze startup
// after enabling new/custom sets. The follow-up unknown-sets check is
// queued to the GUI thread so it sees the swapped-in data (this may run
// on a worker via QtConcurrent::run); it is silent unless new sets
// appeared mid-reload.
loadStatus = loader->loadCardDatabases(/* ignoreCache */ true);
if (loadStatus == Ok) {
QMetaObject::invokeMethod(this, &CardDatabase::checkUnknownSets, Qt::AutoConnection);
}
}
bool CardDatabase::saveCustomTokensToFile()
{
return loader->saveCustomTokensToFile();
}
void CardDatabase::refreshCachedReverseRelatedCards()
{
refreshCachedReverseRelatedCards(cards);
}
void CardDatabase::refreshCachedReverseRelatedCards(CardNameMap &cardMap)
{
for (const auto &card : cardMap) {
card->resetReverseRelatedCards2Me();
}
for (const auto &card : cardMap) {
for (auto *rel : card->getReverseRelatedCards()) {
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());
target->addReverseRelatedCards2Me(newRel);
}
}
}
}
void CardDatabase::addCard(const CardInfoPtr &card)
{
if (card == nullptr) {
qCWarning(CardDatabaseLog) << "CardDatabase::addCard(nullptr)";
return;
}
auto name = card->getName();
// If a card already exists, just add the new set property.
if (auto existing = cards.value(name)) {
for (const auto &printings : card->getSets()) {
for (const auto &printing : printings) {
existing->addToSet(printing.getSet(), printing);
}
}
return;
}
QMutexLocker locker(addCardMutex);
cards.insert(name, card);
simpleNameCards.insert(card->getSimpleName(), card);
emit cardAdded(card);
}
void CardDatabase::removeCard(const CardInfoPtr &card)
{
if (card.isNull()) {
qCWarning(CardDatabaseLog) << "CardDatabase::removeCard(nullptr)";
return;
}
// The set holds a strong reference back to the card, so detach it: leaving it there keeps both
// objects alive after the database drops the card. newInstance() appends through the first
// printing of every set, so detaching through that same printing mirrors the append.
for (const auto &printings : card->getSets()) {
if (printings.isEmpty()) {
continue;
}
if (const CardSetPtr set = printings.first().getSet()) {
set->removeAll(card);
}
}
unindexCard(card);
}
void CardDatabase::unindexCard(const CardInfoPtr &card)
{
for (auto *cardRelation : card->getRelatedCards()) {
cardRelation->deleteLater();
}
for (auto *cardRelation : card->getReverseRelatedCards()) {
cardRelation->deleteLater();
}
for (auto *cardRelation : card->getReverseRelatedCards2Me()) {
cardRelation->deleteLater();
}
// The relations are only scheduled for deletion; the CardInfo may outlive
// them through an external shared pointer, so drop the raw pointers now.
card->clearDirectRelations();
QMutexLocker locker(removeCardMutex);
cards.remove(card->getName());
simpleNameCards.remove(card->getSimpleName());
emit cardRemoved(card);
}
void CardDatabase::addSet(const CardSetPtr &set)
{
sets.insert(set->getShortName(), set);
}
CardSetPtr CardDatabase::getSet(const QString &setName)
{
if (sets.contains(setName)) {
return sets.value(setName);
} else {
CardSetPtr newSet = CardSet::newInstance(setPriorityController, setName);
sets.insert(setName, newSet);
return newSet;
}
}
CardSetList CardDatabase::getSetList() const
{
CardSetList result;
for (auto set : sets.values()) {
result << set;
}
return result;
}
void CardDatabase::checkUnknownSets()
{
auto _sets = getSetList();
if (_sets.getEnabledSetsNum()) {
// if some sets are first found on this run, ask the user
int numUnknownSets = _sets.getUnknownSetsNum();
QStringList unknownSetNames = _sets.getUnknownSetsNames();
if (numUnknownSets > 0) {
emit cardDatabaseNewSetsFound(numUnknownSets, unknownSetNames);
} else {
_sets.markAllAsKnownBatched(setPriorityController);
}
} else {
// No set enabled. Probably this is the first time running trice
_sets.enableAllBatched(setPriorityController);
_sets.sortByKey();
// No cardDatabaseEnabledSetsChanged here: the reload chained off
// cardDatabaseAllNewSetsEnabled re-parses with the sets enabled and its
// swap's cardDatabaseReset rebuilds the models in one batch. Emitting
// both back-to-back rebuilt the 36k-card models twice on the GUI thread,
// stalling startup. (When the card list is empty the slot returns early
// without reloading, but there is nothing to display either.)
emit cardDatabaseAllNewSetsEnabled();
}
}
void CardDatabase::enableAllUnknownSets()
{
auto _sets = getSetList();
_sets.enableAllUnknownBatched(setPriorityController);
}
void CardDatabase::markAllSetsAsKnown()
{
auto _sets = getSetList();
_sets.markAllAsKnownBatched(setPriorityController);
}
void CardDatabase::notifyEnabledSetsChanged(bool recomputeCachedSets)
{
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
emit cardDatabaseEnabledSetsChanged();
}
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();
}
// Replacement cards take over the database; old CardInfo objects may
// still be referenced by the UI, so clear their dangling relations.
card->clearDirectRelations();
}
// Break the CardSet <-> CardInfo cycle before replacing the containers, otherwise the previous
// snapshot survives the reload through the sets' strong card lists.
detachSetMembership();
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();
}