Cockatrice/libcockatrice_card/libcockatrice/card/card_info.cpp
BruebachL ad4320cd7b
[CardDB] Stream card properties from blobs (#7608)
* [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>
2026-10-24 00:17:31 +02:00

313 lines
10 KiB
C++

#include "card_info.h"
#include "game_specific_terms.h"
#include "libcockatrice/card/lazy_properties_hash.h"
#include "printing/printing_info.h"
#include "relation/card_relation.h"
#include "set/card_set.h"
#include <QHashIterator>
#include <QRegularExpression>
#include <QSharedPointer>
#include <QString>
#include <utility>
using CardInfoPtr = QSharedPointer<CardInfo>;
const QHash<QString, QString> &CardInfo::getPropertiesHash() const
{
return properties.getProperties();
}
void CardInfo::setProperty(const QString &_name, const QString &_value)
{
bool changed = properties.insert(_name, _value);
if (!changed) {
return;
}
emit cardInfoChanged(smartThis);
}
CardInfo::CardInfo(const QString &_name,
const QString &_text,
bool _isToken,
const QHash<QString, QString> &_properties,
const QList<CardRelation *> &_relatedCards,
const QList<CardRelation *> &_reverseRelatedCards,
SetToPrintingsMap _sets,
const UiAttributes _uiAttributes,
QMap<QString, QString> _localizedNames,
QMap<QString, QString> _localizedTexts)
: name(_name), text(_text), isToken(_isToken), localizedNames(std::move(_localizedNames)),
localizedTexts(std::move(_localizedTexts)), properties(LazyPropertiesHash(_properties)),
relatedCards(_relatedCards), reverseRelatedCards(_reverseRelatedCards), setsToPrintings(std::move(_sets)),
uiAttributes(_uiAttributes)
{
simpleName = CardInfo::simplifyName(name);
refreshCachedSets();
}
CardInfo::CardInfo(const QString &_name,
const QString &_text,
bool _isToken,
const QByteArray &_propertiesBlob,
const QList<CardRelation *> &_relatedCards,
const QList<CardRelation *> &_reverseRelatedCards,
SetToPrintingsMap _sets,
const UiAttributes _uiAttributes,
QString _simpleName,
QSet<QString> _altNames,
QMap<QString, QString> _localizedNames,
QMap<QString, QString> _localizedTexts)
: name(_name), simpleName(std::move(_simpleName)), text(_text), isToken(_isToken),
localizedNames(std::move(_localizedNames)), localizedTexts(std::move(_localizedTexts)),
properties(LazyPropertiesHash(_propertiesBlob)), relatedCards(_relatedCards),
reverseRelatedCards(_reverseRelatedCards), setsToPrintings(std::move(_sets)), uiAttributes(_uiAttributes),
altNames(std::move(_altNames))
{
// propertiesBlob is materialized lazily on first query. The binary cache supplies simpleName
// and altNames to skip the Unicode normalization and the printing scan; the XML parsers leave
// them empty and get the same derived state the property-hash constructor computes.
if (simpleName.isEmpty()) {
simpleName = simplifyName(name);
}
refreshCachedSetNames();
if (altNames.isEmpty()) {
refreshCachedAltNames();
}
}
CardInfoPtr CardInfo::newInstance(const QString &_name)
{
return newInstance(_name, "", false, {}, {}, {}, {}, {});
}
CardInfoPtr CardInfo::newInstance(const QString &_name,
const QString &_text,
bool _isToken,
const QHash<QString, QString> &_properties,
const QList<CardRelation *> &_relatedCards,
const QList<CardRelation *> &_reverseRelatedCards,
SetToPrintingsMap _sets,
const UiAttributes _uiAttributes,
QMap<QString, QString> _localizedNames,
QMap<QString, QString> _localizedTexts)
{
CardInfoPtr ptr(new CardInfo(_name, _text, _isToken, _properties, _relatedCards, _reverseRelatedCards, _sets,
_uiAttributes, std::move(_localizedNames), std::move(_localizedTexts)));
ptr->setSmartPointer(ptr);
for (const auto &printings : _sets) {
for (const PrintingInfo &printing : printings) {
printing.getSet()->append(ptr);
break;
}
}
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,
QMap<QString, QString> _localizedNames,
QMap<QString, QString> _localizedTexts)
{
CardInfoPtr ptr(new CardInfo(_name, _text, _isToken, std::move(_propertiesBlob), _relatedCards,
_reverseRelatedCards, _sets, _uiAttributes, std::move(_simpleName),
std::move(_altNames), std::move(_localizedNames), std::move(_localizedTexts)));
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,
// other oses only disallow a subset of these so it covers all
static const QRegularExpression rmrx(R"(( // |[*<>:"\\?\x00-\x08\x10-\x1f]))");
static const QRegularExpression spacerx(R"([/\x09-\x0f])");
static const QString space(' ');
QString result = name;
// Fire // Ice, Circle of Protection: Red, "Ach! Hans, Run!", Who/What/When/Where/Why, Question Elemental?
return result.remove(rmrx).replace(spacerx, space);
}
QString CardInfo::getLegalityProp(const QString &format) const
{
return getProperty("format-" + format);
}
bool CardInfo::isLegalInFormat(const QString &format) const
{
if (format.isEmpty()) {
return true;
}
QString formatLegality = getLegalityProp(format);
return formatLegality == "legal" || formatLegality == "restricted";
}
void CardInfo::addToSet(const CardSetPtr &_set, const PrintingInfo &_info)
{
if (!_set->contains(smartThis)) {
_set->append(smartThis);
}
if (!setsToPrintings[_set->getShortName()].contains(_info)) {
setsToPrintings[_set->getShortName()].append(_info);
}
refreshCachedSets();
}
void CardInfo::combineLegalities(const QHash<QString, QString> &props)
{
QHashIterator it(props);
while (it.hasNext()) {
it.next();
if (it.key().startsWith("format-")) {
properties.insert(it.key(), it.value());
}
}
emit cardInfoChanged(smartThis);
}
void CardInfo::refreshCachedSets()
{
refreshCachedSetNames();
refreshCachedAltNames();
}
void CardInfo::refreshCachedSetNames()
{
QStringList setList;
// update the cached list of set names
for (const auto &printings : setsToPrintings) {
for (const auto &printing : printings) {
if (printing.getSet()->getEnabled()) {
setList << printing.getSet()->getShortName();
}
break;
}
}
setsNames = setList.join(", ");
}
void CardInfo::refreshCachedAltNames()
{
altNames.clear();
// update the altNames with the flavorNames
for (const auto &printings : setsToPrintings) {
for (const auto &printing : printings) {
QString flavorName = printing.getFlavorName();
if (!flavorName.isEmpty()) {
altNames.insert(flavorName);
}
}
}
}
QString CardInfo::simplifyName(const QString &name)
{
static const QRegularExpression spaceOrSplit("(\\s+|\\/\\/.*)");
static const QRegularExpression nonAlnum("[^a-z0-9]");
QString simpleName = name.toLower();
// remove spaces and right halves of split cards
simpleName.remove(spaceOrSplit);
// So Aetherling would work, but not Ætherling since 'Æ' would get replaced
// with nothing.
simpleName.replace("æ", "ae");
// Replace Jötun Grunt with Jotun Grunt.
simpleName = simpleName.normalized(QString::NormalizationForm_KD);
// remove all non alphanumeric characters from the name
simpleName.remove(nonAlnum);
return simpleName;
}
QChar CardInfo::getColorChar() const
{
const QString colors = getColors();
switch (colors.size()) {
case 0:
return QChar();
case 1:
return colors.at(0);
default:
return QChar('m');
}
}
void CardInfo::resetReverseRelatedCards2Me()
{
for (CardRelation *cardRelation : this->getReverseRelatedCards2Me()) {
cardRelation->deleteLater();
}
reverseRelatedCardsToMe = QList<CardRelation *>();
}
// Back-compatibility methods. Remove ASAP
QString CardInfo::getCardType() const
{
return getProperty(Mtg::CardType);
}
void CardInfo::setCardType(const QString &value)
{
setProperty(Mtg::CardType, value);
}
QString CardInfo::getCmc() const
{
return getProperty(Mtg::ConvertedManaCost);
}
QString CardInfo::getColors() const
{
return getProperty(Mtg::Colors);
}
void CardInfo::setColors(const QString &value)
{
setProperty(Mtg::Colors, value);
}
QString CardInfo::getLoyalty() const
{
return getProperty(Mtg::Loyalty);
}
QString CardInfo::getMainCardType() const
{
return getProperty(Mtg::MainCardType);
}
QString CardInfo::getManaCost() const
{
return getProperty(Mtg::ManaCost);
}
QString CardInfo::getPowTough() const
{
return getProperty(Mtg::PowTough);
}
void CardInfo::setPowTough(const QString &value)
{
setProperty(Mtg::PowTough, value);
}