mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 01:23:24 -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] Avoid copying printing maps and property strings Reading a card's sets was a measured hotspot: `SetToPrintingsMap setMap = card->getSets();` copies the whole QMap, and then iterating it with a non-const iterator detaches and deep-copies every PrintingInfo, including its property hash, on first use. A micro-benchmark of that exact pattern (copy plus non-const iteration) came out about 19x slower than a const reference over the same data. The querier, the cache writer, the XML writer, the picture loader and the printing/set selection widgets now take `getSets()` by const reference and only use const iterators. The tests that iterate generated data are covered by the existing card database and picture loader suites. String accessors were also returning by value where the value lives in a hash that has already been materialized: - CardInfo::getProperty() and the legacy getCardType()/getColors()/ getManaCost()/getCmc()/getPowTough()/getLoyalty() accessors, - PrintingInfo::getProperty(), getUuid(), getFlavorName(), getArtist(), and getSet(), - ExactCard::getPrinting() and getCardPtr(), - CardSet::getShortName(), getLongName() and getSetType(). getPrinting() in particular copied a PrintingInfo on every call, and a copy of an unmaterialized PrintingInfo has to take the properties mutex. Callers that need an owned copy still get one; the rest just read. Other allocation removals in the same path: - LazyPropertiesHash::ensureMaterialized() checks an atomic flag before taking the mutex, so property reads after the first one do not lock. The flag is release-stored after materialization and acquire-loaded by readers/copies, preserving the existing locking for the first read. - CardSet::getCorrectedShortName() no longer rebuilds its QSet of reserved Windows file names on every call. - CardSet::getIsKnownIgnored() no longer formats the release date into a throwaway string just to check its length. - The querier builds its card-type/format count caches without `cards.values()` or a properties key list, reserves its result lists, and picks a random card without materializing `cards.keys()`. - The querier's pinned-printing path reuses the CardInfoPtr it already has instead of looking the card up again by name. - CardDatabaseCache/XML serialization copy the alt-name QSet by reference. - The picture loader compiles its !prop!/!set! regular expressions once instead of per URL template, and walks the transform map with an iterator instead of keys() plus operator[]. Full build and ctest (628 tests) pass. * [CardDB] Address review comments PrintingInfo is a value type: it is stored in QLists and returned by value from the querier, so a const-ref getter can hand out a reference into a temporary. Return the QStrings and the CardSetPtr by value again. The constFind lookup stays, so a property read still never materializes the lazy property hash - only the last shared-data refcount is paid. * [CardDB] Undo the const-ref QString return changes QString is implicitly shared, so returning by value costs only a refcount and avoids handing out references into a lazily materialized hash. The constFind lookups stay, so a property read still never materializes the hash. * [CardDB] Use QMap::asKeyValueRange() in transformUrl The range is a view over the map's own iterators and yields references to the stored key and value, so it keeps the no-allocation property of the manual const iteration. --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
542 lines
20 KiB
C++
542 lines
20 KiB
C++
#include "cockatrice_xml_3.h"
|
|
|
|
#include "../../relation/card_relation.h"
|
|
#include "../../relation/card_relation_type.h"
|
|
#include "libcockatrice/card/database/card_database_data.h"
|
|
#include "libcockatrice/card/database/parser/card_database_parser.h"
|
|
#include "libcockatrice/card/lazy_properties_hash.h"
|
|
#include "libcockatrice/card/printing/printing_info.h"
|
|
#include "libcockatrice/card/set/card_set.h"
|
|
|
|
#include <QChar>
|
|
#include <QCoreApplication>
|
|
#include <QDate>
|
|
#include <QDateTime>
|
|
#include <QDebug>
|
|
#include <QFile>
|
|
#include <QHash>
|
|
#include <QIODevice>
|
|
#include <QList>
|
|
#include <QMessageLogger>
|
|
#include <QSharedPointer>
|
|
#include <QStringView>
|
|
#include <QXmlStreamAttributes>
|
|
#include <QXmlStreamReader>
|
|
#include <QXmlStreamWriter>
|
|
#include <QtGlobal>
|
|
#include <qnamespace.h>
|
|
#include <version_string.h>
|
|
|
|
class ICardSetPriorityController;
|
|
|
|
#define COCKATRICE_XML3_TAGNAME "cockatrice_carddatabase"
|
|
#define COCKATRICE_XML3_TAGVER 3
|
|
#define COCKATRICE_XML3_SCHEMALOCATION \
|
|
"https://raw.githubusercontent.com/Cockatrice/Cockatrice/master/doc/carddatabase_v3/cards.xsd"
|
|
|
|
CockatriceXml3Parser::CockatriceXml3Parser(ICardSetPriorityController *_cardSetPriorityController)
|
|
: ICardDatabaseParser(_cardSetPriorityController)
|
|
{
|
|
}
|
|
|
|
bool CockatriceXml3Parser::getCanParseFile(const QString &fileName, QIODevice &device)
|
|
{
|
|
qCInfo(CockatriceXml3Log) << "Trying to parse: " << fileName;
|
|
|
|
if (!fileName.endsWith(".xml", Qt::CaseInsensitive)) {
|
|
qCInfo(CockatriceXml3Log) << "Parsing failed: wrong extension";
|
|
return false;
|
|
}
|
|
|
|
QXmlStreamReader xml(&device);
|
|
while (!xml.atEnd()) {
|
|
if (xml.readNext() == QXmlStreamReader::StartElement) {
|
|
if (xml.name().toString() == COCKATRICE_XML3_TAGNAME) {
|
|
int version = xml.attributes().value("version").toString().toInt();
|
|
if (version == COCKATRICE_XML3_TAGVER) {
|
|
return true;
|
|
} else {
|
|
qCInfo(CockatriceXml3Log) << "Parsing failed: wrong version" << version;
|
|
return false;
|
|
}
|
|
|
|
} else {
|
|
qCInfo(CockatriceXml3Log) << "Parsing failed: wrong element tag" << xml.name();
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void CockatriceXml3Parser::parseFile(QIODevice &device)
|
|
{
|
|
QXmlStreamReader xml(&device);
|
|
while (!xml.atEnd()) {
|
|
if (xml.readNext() == QXmlStreamReader::StartElement) {
|
|
while (!xml.atEnd()) {
|
|
if (xml.readNext() == QXmlStreamReader::EndElement) {
|
|
break;
|
|
}
|
|
|
|
auto name = xml.name().toString();
|
|
if (name == "sets") {
|
|
loadSetsFromXml(xml);
|
|
} else if (name == "cards") {
|
|
loadCardsFromXml(xml);
|
|
} else if (!name.isEmpty()) {
|
|
qCInfo(CockatriceXml3Log) << "Unknown item" << name << ", trying to continue anyway";
|
|
xml.skipCurrentElement();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (xml.hasError()) {
|
|
QString preamble = tr("Parse error at line %1 col %2:").arg(xml.lineNumber()).arg(xml.columnNumber());
|
|
qCWarning(CockatriceXml3Log).noquote() << preamble << xml.errorString();
|
|
}
|
|
}
|
|
|
|
void CockatriceXml3Parser::parseFileInto(QIODevice &device, CardDatabaseData &data)
|
|
{
|
|
targetData = &data;
|
|
parseFile(device);
|
|
targetData = nullptr;
|
|
}
|
|
|
|
void CockatriceXml3Parser::loadSetsFromXml(QXmlStreamReader &xml)
|
|
{
|
|
while (!xml.atEnd()) {
|
|
if (xml.readNext() == QXmlStreamReader::EndElement) {
|
|
break;
|
|
}
|
|
|
|
auto name = xml.name().toString();
|
|
if (name == "set") {
|
|
QString shortName, longName, setType;
|
|
QDate releaseDate;
|
|
while (!xml.atEnd()) {
|
|
if (xml.readNext() == QXmlStreamReader::EndElement) {
|
|
break;
|
|
}
|
|
name = xml.name().toString();
|
|
|
|
if (name == "name") {
|
|
shortName = xml.readElementText(QXmlStreamReader::IncludeChildElements);
|
|
} else if (name == "longname") {
|
|
longName = xml.readElementText(QXmlStreamReader::IncludeChildElements);
|
|
} else if (name == "settype") {
|
|
setType = xml.readElementText(QXmlStreamReader::IncludeChildElements);
|
|
} else if (name == "releasedate") {
|
|
releaseDate =
|
|
QDate::fromString(xml.readElementText(QXmlStreamReader::IncludeChildElements), Qt::ISODate);
|
|
} else if (!name.isEmpty()) {
|
|
qCInfo(CockatriceXml3Log) << "Unknown set property" << name << ", trying to continue anyway";
|
|
xml.skipCurrentElement();
|
|
}
|
|
}
|
|
|
|
internalAddSet(shortName, longName, setType, releaseDate);
|
|
}
|
|
}
|
|
}
|
|
|
|
QString CockatriceXml3Parser::getMainCardType(QString &type)
|
|
{
|
|
QString result = type;
|
|
/*
|
|
Legendary Artifact Creature - Golem
|
|
Instant // Instant
|
|
*/
|
|
|
|
int pos;
|
|
if ((pos = result.indexOf('-')) != -1) {
|
|
result.remove(pos, result.length());
|
|
}
|
|
|
|
if ((pos = result.indexOf("—")) != -1) {
|
|
result.remove(pos, result.length());
|
|
}
|
|
|
|
if ((pos = result.indexOf("//")) != -1) {
|
|
result.remove(pos, result.length());
|
|
}
|
|
|
|
result = result.simplified();
|
|
/*
|
|
Legendary Artifact Creature
|
|
Instant
|
|
*/
|
|
|
|
if ((pos = result.lastIndexOf(' ')) != -1) {
|
|
result = result.mid(pos + 1);
|
|
}
|
|
/*
|
|
Creature
|
|
Instant
|
|
*/
|
|
|
|
return result;
|
|
}
|
|
|
|
void CockatriceXml3Parser::loadCardsFromXml(QXmlStreamReader &xml)
|
|
{
|
|
while (!xml.atEnd()) {
|
|
if (xml.readNext() == QXmlStreamReader::EndElement) {
|
|
break;
|
|
}
|
|
|
|
auto xmlName = xml.name().toString();
|
|
if (xmlName == "card") {
|
|
QString name = QString("");
|
|
QString text = QString("");
|
|
QHash<QString, QString> properties;
|
|
QString colors = QString("");
|
|
QList<CardRelation *> relatedCards, reverseRelatedCards;
|
|
auto _sets = SetToPrintingsMap();
|
|
int tableRow = 0;
|
|
bool cipt = false;
|
|
bool landscapeOrientation = false;
|
|
bool isToken = false;
|
|
bool upsideDown = false;
|
|
|
|
while (!xml.atEnd()) {
|
|
if (xml.readNext() == QXmlStreamReader::EndElement) {
|
|
break;
|
|
}
|
|
xmlName = xml.name().toString();
|
|
|
|
// variable - assigned properties
|
|
if (xmlName == "name") {
|
|
name = xml.readElementText(QXmlStreamReader::IncludeChildElements);
|
|
} else if (xmlName == "text") {
|
|
text = xml.readElementText(QXmlStreamReader::IncludeChildElements);
|
|
} else if (xmlName == "color" || xmlName == "colors") {
|
|
colors.append(xml.readElementText(QXmlStreamReader::IncludeChildElements));
|
|
} else if (xmlName == "token") {
|
|
isToken = static_cast<bool>(xml.readElementText(QXmlStreamReader::IncludeChildElements).toInt());
|
|
// generic properties
|
|
} else if (xmlName == "manacost") {
|
|
properties.insert("manacost", xml.readElementText(QXmlStreamReader::IncludeChildElements));
|
|
} else if (xmlName == "cmc") {
|
|
properties.insert("cmc", xml.readElementText(QXmlStreamReader::IncludeChildElements));
|
|
} else if (xmlName == "type") {
|
|
QString type = xml.readElementText(QXmlStreamReader::IncludeChildElements);
|
|
properties.insert("type", type);
|
|
properties.insert("maintype", getMainCardType(type));
|
|
} else if (xmlName == "pt") {
|
|
properties.insert("pt", xml.readElementText(QXmlStreamReader::IncludeChildElements));
|
|
} else if (xmlName == "loyalty") {
|
|
properties.insert("loyalty", xml.readElementText(QXmlStreamReader::IncludeChildElements));
|
|
// positioning info
|
|
} else if (xmlName == "tablerow") {
|
|
tableRow = xml.readElementText(QXmlStreamReader::IncludeChildElements).toInt();
|
|
} else if (xmlName == "cipt") {
|
|
cipt = (xml.readElementText(QXmlStreamReader::IncludeChildElements) == "1");
|
|
} else if (xmlName == "landscapeOrientation") {
|
|
landscapeOrientation = (xml.readElementText(QXmlStreamReader::IncludeChildElements) == "1");
|
|
} else if (xmlName == "upsidedown") {
|
|
upsideDown = (xml.readElementText(QXmlStreamReader::IncludeChildElements) == "1");
|
|
// sets
|
|
} else if (xmlName == "set") {
|
|
// NOTE: attributes must be read before readElementText()
|
|
QXmlStreamAttributes attrs = xml.attributes();
|
|
QString setName = xml.readElementText(QXmlStreamReader::IncludeChildElements);
|
|
auto set = internalAddSet(setName);
|
|
// Only load printings from sets the user has enabled, matching the v4 loader's
|
|
// behaviour. Without this check, disabling a set has no effect on v3 databases.
|
|
if (set->getEnabled()) {
|
|
QHash<QString, QString> printingProps;
|
|
if (attrs.hasAttribute("muId")) {
|
|
printingProps.insert("muid", attrs.value("muId").toString());
|
|
}
|
|
|
|
if (attrs.hasAttribute("uuId")) {
|
|
printingProps.insert("uuid", attrs.value("uuId").toString());
|
|
}
|
|
|
|
if (attrs.hasAttribute("picURL")) {
|
|
printingProps.insert("picurl", attrs.value("picURL").toString());
|
|
}
|
|
|
|
if (attrs.hasAttribute("num")) {
|
|
printingProps.insert("num", attrs.value("num").toString());
|
|
}
|
|
|
|
if (attrs.hasAttribute("rarity")) {
|
|
printingProps.insert("rarity", attrs.value("rarity").toString());
|
|
}
|
|
PrintingInfo setInfo(set, LazyPropertiesHash(printingProps));
|
|
_sets[setName].append(setInfo);
|
|
}
|
|
// related cards
|
|
} else if (xmlName == "related" || xmlName == "reverse-related") {
|
|
CardRelationType attach = CardRelationType::DoesNotAttach;
|
|
bool exclude = false;
|
|
bool variable = false;
|
|
int count = 1;
|
|
QXmlStreamAttributes attrs = xml.attributes();
|
|
QString cardName = xml.readElementText(QXmlStreamReader::IncludeChildElements);
|
|
if (attrs.hasAttribute("count")) {
|
|
if (attrs.value("count").toString().indexOf("x=") == 0) {
|
|
variable = true;
|
|
count = attrs.value("count").toString().remove(0, 2).toInt();
|
|
} else if (attrs.value("count").toString().indexOf("x") == 0) {
|
|
variable = true;
|
|
} else {
|
|
count = attrs.value("count").toString().toInt();
|
|
}
|
|
|
|
if (count < 1) {
|
|
count = 1;
|
|
}
|
|
}
|
|
|
|
if (attrs.hasAttribute("attach")) {
|
|
attach = CardRelationType::AttachTo;
|
|
}
|
|
|
|
if (attrs.hasAttribute("exclude")) {
|
|
exclude = true;
|
|
}
|
|
|
|
auto *relation = new CardRelation(cardName, attach, exclude, variable, count);
|
|
if (xmlName == "reverse-related") {
|
|
reverseRelatedCards << relation;
|
|
} else {
|
|
relatedCards << relation;
|
|
}
|
|
} else if (!xmlName.isEmpty()) {
|
|
qCInfo(CockatriceXml3Log) << "Unknown card property" << xmlName << ", trying to continue anyway";
|
|
xml.skipCurrentElement();
|
|
}
|
|
}
|
|
|
|
if (name.isEmpty()) {
|
|
qCWarning(CockatriceXml3Log) << "Encountered card with empty name; skipping";
|
|
continue;
|
|
}
|
|
|
|
properties.insert("colors", colors);
|
|
|
|
CardInfo::UiAttributes attributes = {.cipt = cipt,
|
|
.landscapeOrientation = landscapeOrientation,
|
|
.tableRow = tableRow,
|
|
.upsideDownArt = upsideDown};
|
|
CardInfoPtr newCard = CardInfo::newInstance(name, text, isToken, properties, relatedCards,
|
|
reverseRelatedCards, _sets, attributes);
|
|
if (targetData) {
|
|
if (auto existing = targetData->cards.value(name)) {
|
|
for (const auto &printings : newCard->getSets()) {
|
|
for (const auto &printing : printings) {
|
|
existing->addToSet(printing.getSet(), printing);
|
|
}
|
|
}
|
|
} else {
|
|
targetData->cards.insert(name, newCard);
|
|
targetData->simpleNameCards.insert(newCard->getSimpleName(), newCard);
|
|
}
|
|
} else {
|
|
emit addCard(newCard);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardSetPtr &set)
|
|
{
|
|
if (set.isNull()) {
|
|
qCWarning(CockatriceXml3Log) << "&operator<< set is nullptr";
|
|
return xml;
|
|
}
|
|
|
|
xml.writeStartElement("set");
|
|
xml.writeTextElement("name", set->getShortName());
|
|
xml.writeTextElement("longname", set->getLongName());
|
|
xml.writeTextElement("settype", set->getSetType());
|
|
xml.writeTextElement("releasedate", set->getReleaseDate().toString(Qt::ISODate));
|
|
xml.writeEndElement();
|
|
|
|
return xml;
|
|
}
|
|
|
|
static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardInfoPtr &info)
|
|
{
|
|
if (info.isNull()) {
|
|
qCWarning(CockatriceXml3Log) << "operator<< info is nullptr";
|
|
return xml;
|
|
}
|
|
|
|
QString tmpString;
|
|
|
|
xml.writeStartElement("card");
|
|
|
|
// variable - assigned properties
|
|
xml.writeTextElement("name", info->getName());
|
|
xml.writeTextElement("text", info->getText());
|
|
if (info->getIsToken()) {
|
|
xml.writeTextElement("token", "1");
|
|
}
|
|
|
|
// generic properties
|
|
xml.writeTextElement("manacost", info->getProperty("manacost"));
|
|
xml.writeTextElement("cmc", info->getProperty("cmc"));
|
|
xml.writeTextElement("type", info->getProperty("type"));
|
|
|
|
int colorSize = info->getColors().size();
|
|
for (int i = 0; i < colorSize; ++i) {
|
|
xml.writeTextElement("color", info->getColors().at(i));
|
|
}
|
|
|
|
tmpString = info->getProperty("pt");
|
|
if (!tmpString.isEmpty()) {
|
|
xml.writeTextElement("pt", tmpString);
|
|
}
|
|
|
|
tmpString = info->getProperty("loyalty");
|
|
if (!tmpString.isEmpty()) {
|
|
xml.writeTextElement("loyalty", tmpString);
|
|
}
|
|
|
|
// sets
|
|
const auto &setMap = info->getSets();
|
|
for (const auto &printings : setMap) {
|
|
for (const PrintingInfo &set : printings) {
|
|
xml.writeStartElement("set");
|
|
xml.writeAttribute("rarity", set.getProperty("rarity"));
|
|
xml.writeAttribute("muId", set.getProperty("muid"));
|
|
xml.writeAttribute("uuId", set.getProperty("uuid"));
|
|
|
|
tmpString = set.getProperty("num");
|
|
if (!tmpString.isEmpty()) {
|
|
xml.writeAttribute("num", tmpString);
|
|
}
|
|
|
|
tmpString = set.getProperty("picurl");
|
|
if (!tmpString.isEmpty()) {
|
|
xml.writeAttribute("picURL", tmpString);
|
|
}
|
|
|
|
xml.writeCharacters(set.getSet()->getShortName());
|
|
xml.writeEndElement();
|
|
}
|
|
}
|
|
|
|
// related cards
|
|
const QList<CardRelation *> related = info->getRelatedCards();
|
|
for (auto i : related) {
|
|
xml.writeStartElement("related");
|
|
if (i->getDoesAttach()) {
|
|
xml.writeAttribute("attach", "attach");
|
|
}
|
|
if (i->getIsCreateAllExclusion()) {
|
|
xml.writeAttribute("exclude", "exclude");
|
|
}
|
|
|
|
if (i->getIsVariable()) {
|
|
if (1 == i->getDefaultCount()) {
|
|
xml.writeAttribute("count", "x");
|
|
} else {
|
|
xml.writeAttribute("count", "x=" + QString::number(i->getDefaultCount()));
|
|
}
|
|
} else if (1 != i->getDefaultCount()) {
|
|
xml.writeAttribute("count", QString::number(i->getDefaultCount()));
|
|
}
|
|
xml.writeCharacters(i->getName());
|
|
xml.writeEndElement();
|
|
}
|
|
const QList<CardRelation *> reverseRelated = info->getReverseRelatedCards();
|
|
for (auto i : reverseRelated) {
|
|
xml.writeStartElement("reverse-related");
|
|
if (i->getDoesAttach()) {
|
|
xml.writeAttribute("attach", "attach");
|
|
}
|
|
|
|
if (i->getIsCreateAllExclusion()) {
|
|
xml.writeAttribute("exclude", "exclude");
|
|
}
|
|
|
|
if (i->getIsVariable()) {
|
|
if (1 == i->getDefaultCount()) {
|
|
xml.writeAttribute("count", "x");
|
|
} else {
|
|
xml.writeAttribute("count", "x=" + QString::number(i->getDefaultCount()));
|
|
}
|
|
} else if (1 != i->getDefaultCount()) {
|
|
xml.writeAttribute("count", QString::number(i->getDefaultCount()));
|
|
}
|
|
xml.writeCharacters(i->getName());
|
|
xml.writeEndElement();
|
|
}
|
|
|
|
// positioning
|
|
const CardInfo::UiAttributes &attributes = info->getUiAttributes();
|
|
xml.writeTextElement("tablerow", QString::number(attributes.tableRow));
|
|
if (attributes.cipt) {
|
|
xml.writeTextElement("cipt", "1");
|
|
}
|
|
if (attributes.landscapeOrientation) {
|
|
xml.writeTextElement("landscapeOrientation", "1");
|
|
}
|
|
if (attributes.upsideDownArt) {
|
|
xml.writeTextElement("upsidedown", "1");
|
|
}
|
|
|
|
xml.writeEndElement(); // card
|
|
|
|
return xml;
|
|
}
|
|
|
|
bool CockatriceXml3Parser::saveToFile(FormatRulesNameMap _formats,
|
|
SetNameMap _sets,
|
|
CardNameMap cards,
|
|
const QString &fileName,
|
|
const QString &sourceUrl,
|
|
const QString &sourceVersion)
|
|
{
|
|
Q_UNUSED(_formats);
|
|
|
|
QFile file(fileName);
|
|
if (!file.open(QIODevice::WriteOnly)) {
|
|
return false;
|
|
}
|
|
|
|
QXmlStreamWriter xml(&file);
|
|
|
|
xml.setAutoFormatting(true);
|
|
xml.writeStartDocument();
|
|
xml.writeStartElement(COCKATRICE_XML3_TAGNAME);
|
|
xml.writeAttribute("version", QString::number(COCKATRICE_XML3_TAGVER));
|
|
xml.writeAttribute("xmlns:xsi", COCKATRICE_XML_XSI_NAMESPACE);
|
|
xml.writeAttribute("xsi:schemaLocation", COCKATRICE_XML3_SCHEMALOCATION);
|
|
|
|
xml.writeStartElement("info");
|
|
xml.writeTextElement("author", QCoreApplication::applicationName() + QString(" %1").arg(VERSION_STRING));
|
|
xml.writeTextElement("createdAt", QDateTime::currentDateTimeUtc().toString(Qt::ISODate));
|
|
xml.writeTextElement("sourceUrl", sourceUrl);
|
|
xml.writeTextElement("sourceVersion", sourceVersion);
|
|
xml.writeEndElement();
|
|
|
|
if (_sets.count() > 0) {
|
|
xml.writeStartElement("sets");
|
|
for (CardSetPtr set : _sets) {
|
|
xml << set;
|
|
}
|
|
xml.writeEndElement();
|
|
}
|
|
|
|
if (cards.count() > 0) {
|
|
xml.writeStartElement("cards");
|
|
for (CardInfoPtr card : cards) {
|
|
xml << card;
|
|
}
|
|
xml.writeEndElement();
|
|
}
|
|
|
|
xml.writeEndElement(); // cockatrice_carddatabase
|
|
xml.writeEndDocument();
|
|
|
|
return true;
|
|
}
|