mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 09:33:25 -08:00
* [CardDB] Stream card properties from blobs Parsed and cached cards used to keep a materialized QHash<QString, QString> for their properties: LazyPropertiesHash only deferred building that hash until the first query, and the cache writer re-serialized it on save. - LazyPropertiesHash answers value(), contains(), size(), isEmpty() and keys() straight out of the serialized blob, so a property query no longer builds a hash. - serialize() is public, and the cache writer passes the stored blob through instead of serializing a hash it just rebuilt. - CardInfo and PrintingInfo expose getPropertiesBlob() and getPropertyCount(); PrintingInfo::operator== compares key by key without materializing either side. - The XML parsers build cards via newInstanceFromPropertiesBlob(), which derives the simple name and the alternate names that the parser used to keep next to the hash. The reload cycle fix stays out of this change; it depends on changing the CardInfo self pointer and is reviewed separately. * [CardDB] Address review comments on streaming properties * [CardDB] Address final review nits on property streaming Move BlobReader and the blob accessors into LazyPropertiesHash as private members so the parsing helper is declared with the class instead of hiding in the translation unit. Add LazyPropertiesHash::serializedFrom() and use it where a PrintingInfo is built from a fresh property hash, replacing the nested LazyPropertiesHash(LazyPropertiesHash::serialize(props)) construction. The pre-materialized QHash constructor stays: CardInfo's non-blob factory still uses it for the oracle import path, and the tests cover it directly. --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
539 lines
15 KiB
C++
539 lines
15 KiB
C++
#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 "../set/card_set.h"
|
|
#include "card_database_loader.h"
|
|
#include "libcockatrice/card/database/card_database_data.h"
|
|
#include "libcockatrice/card/lazy_properties_hash.h"
|
|
|
|
#include <QBuffer>
|
|
#include <QByteArray>
|
|
#include <QDataStream>
|
|
#include <QDate>
|
|
#include <QDebug>
|
|
#include <QElapsedTimer>
|
|
#include <QFile>
|
|
#include <QIODevice>
|
|
#include <QList>
|
|
#include <QLoggingCategory>
|
|
#include <QMap>
|
|
#include <QMessageLogger>
|
|
#include <QSaveFile>
|
|
#include <QSet>
|
|
#include <QSharedPointer>
|
|
#include <QtGlobal>
|
|
#include <qnamespace.h>
|
|
|
|
class ICardSetPriorityController;
|
|
enum class CardRelationType;
|
|
template <typename Key, typename T> class QHash;
|
|
|
|
namespace
|
|
{
|
|
constexpr quint32 CACHE_MAGIC = 0x43445243; // "CDRC"
|
|
constexpr quint32 CACHE_VERSION = 3;
|
|
|
|
// ---- Primitives -----------------------------------------------------------
|
|
|
|
void writeString(QDataStream &out, const QString &s)
|
|
{
|
|
out << s;
|
|
}
|
|
|
|
QString readString(QDataStream &in)
|
|
{
|
|
QString s;
|
|
in >> s;
|
|
return s;
|
|
}
|
|
|
|
// Stores a QHash<QString, QString> as a single pre-serialized blob. The reader keeps the
|
|
// blob as-is and materializes the QHash<QString, QString> lazily on first query, which is
|
|
// what removes the allocation storm from database load (see card_info.cpp /
|
|
// printing_info.cpp). Writing the already-serialized blob also keeps cache writes from
|
|
// materializing every card's property hash.
|
|
void writeBlob(QDataStream &out, const QByteArray &blob)
|
|
{
|
|
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;
|
|
}
|
|
|
|
void writeStringMap(QDataStream &out, const QMap<QString, QString> &map)
|
|
{
|
|
out << static_cast<quint32>(map.size());
|
|
for (auto it = map.constBegin(); it != map.constEnd(); ++it) {
|
|
writeString(out, it.key());
|
|
writeString(out, it.value());
|
|
}
|
|
}
|
|
|
|
QMap<QString, QString> readStringMap(QDataStream &in)
|
|
{
|
|
QMap<QString, QString> map;
|
|
quint32 count = 0;
|
|
in >> count;
|
|
if (in.status() != QDataStream::Ok) {
|
|
return map;
|
|
}
|
|
for (quint32 i = 0; i < count; ++i) {
|
|
QString key = readString(in);
|
|
QString value = readString(in);
|
|
if (in.status() != QDataStream::Ok) {
|
|
map.clear();
|
|
return map;
|
|
}
|
|
map.insert(key, value);
|
|
}
|
|
return map;
|
|
}
|
|
|
|
// ---- 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());
|
|
writeBlob(out, p.getPropertiesBlob());
|
|
}
|
|
|
|
PrintingInfo readPrinting(QDataStream &in, const SetNameMap &sets)
|
|
{
|
|
QString setName = readString(in);
|
|
QByteArray propsBlob = readHashBlob(in);
|
|
auto set = sets.value(setName);
|
|
|
|
return PrintingInfo(set, LazyPropertiesHash(propsBlob));
|
|
}
|
|
|
|
// ---- 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();
|
|
writeBlob(out, card->getPropertiesBlob());
|
|
|
|
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 auto &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);
|
|
}
|
|
|
|
// localized card data
|
|
writeStringMap(out, card->getLocalizedNames());
|
|
writeStringMap(out, card->getLocalizedTexts());
|
|
}
|
|
|
|
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));
|
|
}
|
|
|
|
const QMap<QString, QString> localizedNames = readStringMap(in);
|
|
if (in.status() != QDataStream::Ok) {
|
|
return nullptr;
|
|
}
|
|
const QMap<QString, QString> localizedTexts = readStringMap(in);
|
|
if (in.status() != QDataStream::Ok) {
|
|
return nullptr;
|
|
}
|
|
|
|
CardInfoPtr card = CardInfo::newInstance(name, text, isToken, propertiesBlob, related, reverse, cardSets, ui,
|
|
simpleName, altNames, false, localizedNames, localizedTexts);
|
|
|
|
return card;
|
|
}
|
|
|
|
// ---- 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;
|
|
}
|