mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 09:33:25 -08:00
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] 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>
328 lines
11 KiB
C++
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();
|
|
}
|