Initial commit of mtgonline project
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
#include "card_database_parser.h"
|
||||
|
||||
#include <libcockatrice/interfaces/noop_card_set_priority_controller.h>
|
||||
|
||||
SetNameMap ICardDatabaseParser::sets;
|
||||
|
||||
ICardDatabaseParser::ICardDatabaseParser(ICardSetPriorityController *_cardSetPriorityController)
|
||||
: cardSetPriorityController(_cardSetPriorityController)
|
||||
{
|
||||
}
|
||||
void ICardDatabaseParser::clearSetlist()
|
||||
{
|
||||
sets.clear();
|
||||
}
|
||||
|
||||
CardSetPtr ICardDatabaseParser::internalAddSet(const QString &setName,
|
||||
const QString &longName,
|
||||
const QString &setType,
|
||||
const QDate &releaseDate,
|
||||
const CardSet::Priority priority)
|
||||
{
|
||||
if (sets.contains(setName)) {
|
||||
return sets.value(setName);
|
||||
}
|
||||
|
||||
CardSetPtr newSet = CardSet::newInstance(cardSetPriorityController, setName);
|
||||
newSet->setLongName(longName);
|
||||
newSet->setSetType(setType);
|
||||
newSet->setReleaseDate(releaseDate);
|
||||
newSet->setPriority(priority);
|
||||
|
||||
sets.insert(setName, newSet);
|
||||
emit addSet(newSet);
|
||||
return newSet;
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
#ifndef CARDDATABASE_PARSER_H
|
||||
#define CARDDATABASE_PARSER_H
|
||||
|
||||
#include "../../card_info.h"
|
||||
|
||||
#include <QIODevice>
|
||||
#include <QString>
|
||||
|
||||
#define COCKATRICE_XML_XSI_NAMESPACE "http://www.w3.org/2001/XMLSchema-instance"
|
||||
|
||||
/**
|
||||
* @class ICardDatabaseParser
|
||||
* @ingroup CardDatabase
|
||||
* @brief Defines the base parser interface (ICardDatabaseParser) for all card database parsers.
|
||||
*
|
||||
* Provides methods for checking file compatibility, parsing, and saving card databases.
|
||||
* Also provides shared access to the global set list for cross-referencing.
|
||||
*/
|
||||
class ICardDatabaseParser : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
ICardDatabaseParser(ICardSetPriorityController *cardSetPriorityController);
|
||||
~ICardDatabaseParser() override = default;
|
||||
|
||||
/**
|
||||
* @brief Checks whether this parser can parse the given file.
|
||||
* @param name File name (used for extension checks).
|
||||
* @param device QIODevice representing the file content.
|
||||
* @return true if the parser can handle this file.
|
||||
*/
|
||||
virtual bool getCanParseFile(const QString &name, QIODevice &device) = 0;
|
||||
|
||||
/**
|
||||
* @brief Parses a database file and emits addCard/addSet signals.
|
||||
* @param device QIODevice representing the file content.
|
||||
*/
|
||||
virtual void parseFile(QIODevice &device) = 0;
|
||||
|
||||
/**
|
||||
* @brief Saves card and set data to a file.
|
||||
* @param _formats
|
||||
* @param sets Map of sets to save.
|
||||
* @param cards Map of cards to save.
|
||||
* @param fileName Target file path.
|
||||
* @param sourceUrl Optional source URL of the database.
|
||||
* @param sourceVersion Optional version string of the source.
|
||||
* @return true if save succeeded.
|
||||
*/
|
||||
virtual bool saveToFile(FormatRulesNameMap _formats,
|
||||
SetNameMap sets,
|
||||
CardNameMap cards,
|
||||
const QString &fileName,
|
||||
const QString &sourceUrl = "unknown",
|
||||
const QString &sourceVersion = "unknown") = 0;
|
||||
|
||||
/** @brief Clears the cached global set list. */
|
||||
static void clearSetlist();
|
||||
|
||||
protected:
|
||||
/** @brief Cached global list of sets shared between all parsers. */
|
||||
static SetNameMap sets;
|
||||
ICardSetPriorityController *cardSetPriorityController;
|
||||
|
||||
/**
|
||||
* @brief Internal helper to add a set to the global set cache.
|
||||
* @param setName Short set name.
|
||||
* @param longName Optional full name.
|
||||
* @param setType Optional set type string.
|
||||
* @param releaseDate Optional release date.
|
||||
* @param priority Optional priority (fallback if not specified).
|
||||
* @return Pointer to the added or existing CardSet instance.
|
||||
*/
|
||||
CardSetPtr internalAddSet(const QString &setName,
|
||||
const QString &longName = "",
|
||||
const QString &setType = "",
|
||||
const QDate &releaseDate = QDate(),
|
||||
const CardSet::Priority priority = CardSet::PriorityFallback);
|
||||
|
||||
signals:
|
||||
/** Emitted when a card is loaded from the database. */
|
||||
void addCard(CardInfoPtr card);
|
||||
|
||||
/** Emitted when a set is loaded from the database. */
|
||||
void addSet(CardSetPtr set);
|
||||
|
||||
void addFormat(FormatRulesPtr format);
|
||||
};
|
||||
|
||||
Q_DECLARE_INTERFACE(ICardDatabaseParser, "ICardDatabaseParser")
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,501 @@
|
||||
#include "cockatrice_xml_3.h"
|
||||
|
||||
#include "../../relation/card_relation.h"
|
||||
#include "../../relation/card_relation_type.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDebug>
|
||||
#include <QFile>
|
||||
#include <QXmlStreamReader>
|
||||
#include <version_string.h>
|
||||
|
||||
#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::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("");
|
||||
QVariantHash properties = QVariantHash();
|
||||
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()) {
|
||||
PrintingInfo setInfo(set);
|
||||
if (attrs.hasAttribute("muId")) {
|
||||
setInfo.setProperty("muid", attrs.value("muId").toString());
|
||||
}
|
||||
|
||||
if (attrs.hasAttribute("uuId")) {
|
||||
setInfo.setProperty("uuid", attrs.value("uuId").toString());
|
||||
}
|
||||
|
||||
if (attrs.hasAttribute("picURL")) {
|
||||
setInfo.setProperty("picurl", attrs.value("picURL").toString());
|
||||
}
|
||||
|
||||
if (attrs.hasAttribute("num")) {
|
||||
setInfo.setProperty("num", attrs.value("num").toString());
|
||||
}
|
||||
|
||||
if (attrs.hasAttribute("rarity")) {
|
||||
setInfo.setProperty("rarity", attrs.value("rarity").toString());
|
||||
}
|
||||
_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);
|
||||
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 SetToPrintingsMap 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;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
#ifndef COCKATRICE_XML3_H
|
||||
#define COCKATRICE_XML3_H
|
||||
|
||||
#include "card_database_parser.h"
|
||||
|
||||
#include <QLoggingCategory>
|
||||
#include <QXmlStreamReader>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(CockatriceXml3Log, "cockatrice_xml.xml_3_parser");
|
||||
|
||||
/**
|
||||
* @class CockatriceXml3Parser
|
||||
* @ingroup CardDatabase
|
||||
* @brief Parses version 3 of the Cockatrice XML Schema.
|
||||
*
|
||||
* This parser reads a Cockatrice XML3 database and emits CardInfoPtr
|
||||
* and CardSetPtr objects. All card properties are read individually.
|
||||
*
|
||||
* @note Differences from v4:
|
||||
* - No <prop> block; properties are hardcoded (manacost, cmc, type, pt, loyalty, etc.).
|
||||
* - No set priority field.
|
||||
* - No support for rebalanced cards or preferences.
|
||||
* - Related cards support only attach, exclude, variable, and count attributes.
|
||||
*/
|
||||
class CockatriceXml3Parser : public ICardDatabaseParser
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
CockatriceXml3Parser(ICardSetPriorityController *cardSetPriorityController);
|
||||
~CockatriceXml3Parser() override = default;
|
||||
|
||||
/**
|
||||
* @brief Determines if the parser can handle this file.
|
||||
* @param name File name.
|
||||
* @param device Open QIODevice containing the XML.
|
||||
* @return True if the file is a Cockatrice XML3 database.
|
||||
*/
|
||||
bool getCanParseFile(const QString &name, QIODevice &device) override;
|
||||
|
||||
/**
|
||||
* @brief Parse the XML database.
|
||||
* @param device Open QIODevice positioned at start of file.
|
||||
*/
|
||||
void parseFile(QIODevice &device) override;
|
||||
|
||||
/**
|
||||
* @brief Save sets and cards back to an XML3 file.
|
||||
*/
|
||||
bool saveToFile(FormatRulesNameMap _formats,
|
||||
SetNameMap _sets,
|
||||
CardNameMap cards,
|
||||
const QString &fileName,
|
||||
const QString &sourceUrl = "unknown",
|
||||
const QString &sourceVersion = "unknown") override;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Load all <card> elements from the XML stream.
|
||||
* @param xml The open QXmlStreamReader positioned at the <cards> element.
|
||||
* Parses each <card> node and emits addCard signals for each CardInfoPtr created.
|
||||
*/
|
||||
void loadCardsFromXml(QXmlStreamReader &xml);
|
||||
|
||||
/**
|
||||
* @brief Load all <set> elements from the XML stream.
|
||||
* @param xml The open QXmlStreamReader positioned at the <sets> element.
|
||||
* Parses each <set> node and adds them to the shared set cache.
|
||||
*/
|
||||
void loadSetsFromXml(QXmlStreamReader &xml);
|
||||
|
||||
/**
|
||||
* @brief Extracts the main card type from a full type string.
|
||||
* @param type The full type string (e.g., "Legendary Artifact Creature - Golem")
|
||||
* @return The primary type (e.g., "Creature").
|
||||
*/
|
||||
QString getMainCardType(QString &type);
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,635 @@
|
||||
#include "cockatrice_xml_4.h"
|
||||
|
||||
#include "../../relation/card_relation.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDebug>
|
||||
#include <QFile>
|
||||
#include <QXmlStreamReader>
|
||||
#include <libcockatrice/card/format/format_legality_rules.h>
|
||||
#include <version_string.h>
|
||||
|
||||
#define COCKATRICE_XML4_TAGNAME "cockatrice_carddatabase"
|
||||
#define COCKATRICE_XML4_TAGVER 4
|
||||
#define COCKATRICE_XML4_SCHEMALOCATION \
|
||||
"https://raw.githubusercontent.com/Cockatrice/Cockatrice/master/doc/carddatabase_v4/cards.xsd"
|
||||
|
||||
CockatriceXml4Parser::CockatriceXml4Parser(ICardPreferenceProvider *_cardPreferenceProvider,
|
||||
ICardSetPriorityController *_cardSetPriorityController)
|
||||
: ICardDatabaseParser(_cardSetPriorityController), cardPreferenceProvider(_cardPreferenceProvider)
|
||||
{
|
||||
}
|
||||
|
||||
bool CockatriceXml4Parser::getCanParseFile(const QString &fileName, QIODevice &device)
|
||||
{
|
||||
qCInfo(CockatriceXml4Log) << "Trying to parse: " << fileName;
|
||||
|
||||
if (!fileName.endsWith(".xml", Qt::CaseInsensitive)) {
|
||||
qCInfo(CockatriceXml4Log) << "Parsing failed: wrong extension";
|
||||
return false;
|
||||
}
|
||||
|
||||
QXmlStreamReader xml(&device);
|
||||
while (!xml.atEnd()) {
|
||||
if (xml.readNext() == QXmlStreamReader::StartElement) {
|
||||
if (xml.name().toString() == COCKATRICE_XML4_TAGNAME) {
|
||||
int version = xml.attributes().value("version").toString().toInt();
|
||||
if (version == COCKATRICE_XML4_TAGVER) {
|
||||
return true;
|
||||
} else {
|
||||
qCInfo(CockatriceXml4Log) << "Parsing failed: wrong version" << version;
|
||||
return false;
|
||||
}
|
||||
|
||||
} else {
|
||||
qCInfo(CockatriceXml4Log) << "Parsing failed: wrong element tag" << xml.name();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void CockatriceXml4Parser::parseFile(QIODevice &device)
|
||||
{
|
||||
QXmlStreamReader xml(&device);
|
||||
while (!xml.atEnd()) {
|
||||
if (xml.readNext() == QXmlStreamReader::StartElement) {
|
||||
while (!xml.atEnd()) {
|
||||
if (xml.readNext() == QXmlStreamReader::EndElement) {
|
||||
break;
|
||||
}
|
||||
|
||||
auto xmlName = xml.name().toString();
|
||||
if (xmlName == "formats") {
|
||||
loadFormats(xml);
|
||||
} else if (xmlName == "sets") {
|
||||
loadSetsFromXml(xml);
|
||||
} else if (xmlName == "cards") {
|
||||
loadCardsFromXml(xml);
|
||||
} else if (!xmlName.isEmpty()) {
|
||||
qCInfo(CockatriceXml4Log) << "Unknown item" << xmlName << ", 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(CockatriceXml4Log).noquote() << preamble << xml.errorString();
|
||||
}
|
||||
}
|
||||
|
||||
static QSharedPointer<FormatRules> parseFormat(QXmlStreamReader &xml)
|
||||
{
|
||||
auto rulesPtr = FormatRulesPtr(new FormatRules());
|
||||
|
||||
if (xml.attributes().hasAttribute("formatName")) {
|
||||
rulesPtr->formatName = xml.attributes().value("formatName").toString();
|
||||
}
|
||||
|
||||
while (!xml.atEnd()) {
|
||||
auto token = xml.readNext();
|
||||
|
||||
if (token == QXmlStreamReader::EndElement && xml.name().toString() == "format") {
|
||||
break;
|
||||
}
|
||||
|
||||
if (token != QXmlStreamReader::StartElement) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QString xmlName = xml.name().toString();
|
||||
|
||||
if (xmlName == "minDeckSize") {
|
||||
rulesPtr->minDeckSize = xml.readElementText().toInt();
|
||||
} else if (xmlName == "maxDeckSize") {
|
||||
QString text = xml.readElementText();
|
||||
rulesPtr->maxDeckSize = text.toInt();
|
||||
} else if (xmlName == "maxSideboardSize") {
|
||||
rulesPtr->maxSideboardSize = xml.readElementText().toInt();
|
||||
} else if (xmlName == "allowedCounts") {
|
||||
while (!xml.atEnd()) {
|
||||
token = xml.readNext();
|
||||
|
||||
if (token == QXmlStreamReader::EndElement && xml.name().toString() == "allowedCounts") {
|
||||
break;
|
||||
}
|
||||
|
||||
if (token == QXmlStreamReader::StartElement && xml.name().toString() == "count") {
|
||||
|
||||
AllowedCount c;
|
||||
|
||||
QString maxAttr = xml.attributes().value("max").toString();
|
||||
c.max = (maxAttr == "unlimited") ? -1 : maxAttr.toInt();
|
||||
|
||||
c.label = xml.readElementText().trimmed();
|
||||
|
||||
rulesPtr->allowedCounts.append(c);
|
||||
}
|
||||
}
|
||||
} else if (xmlName == "exceptions") {
|
||||
while (!xml.atEnd()) {
|
||||
token = xml.readNext();
|
||||
|
||||
if (token == QXmlStreamReader::EndElement && xml.name().toString() == "exceptions") {
|
||||
break;
|
||||
}
|
||||
|
||||
if (token == QXmlStreamReader::StartElement && xml.name().toString() == "exception") {
|
||||
ExceptionRule ex;
|
||||
|
||||
while (!xml.atEnd()) {
|
||||
token = xml.readNext();
|
||||
|
||||
if (token == QXmlStreamReader::EndElement && xml.name().toString() == "exception") {
|
||||
break;
|
||||
}
|
||||
|
||||
if (token == QXmlStreamReader::StartElement) {
|
||||
QString ename = xml.name().toString();
|
||||
|
||||
if (ename == "maxCopies") {
|
||||
QString text = xml.readElementText();
|
||||
ex.maxCopies = (text == "unlimited") ? -1 : text.toInt();
|
||||
} else if (ename == "cardCondition") {
|
||||
CardCondition cond;
|
||||
cond.field = xml.attributes().value("field").toString();
|
||||
cond.matchType = xml.attributes().value("match").toString();
|
||||
cond.value = xml.attributes().value("value").toString();
|
||||
ex.conditions.append(cond);
|
||||
xml.skipCurrentElement();
|
||||
} else {
|
||||
xml.skipCurrentElement();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rulesPtr->exceptions.append(ex);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
xml.skipCurrentElement();
|
||||
}
|
||||
}
|
||||
|
||||
return rulesPtr;
|
||||
}
|
||||
|
||||
void CockatriceXml4Parser::loadFormats(QXmlStreamReader &xml)
|
||||
{
|
||||
while (!xml.atEnd()) {
|
||||
if (xml.readNext() == QXmlStreamReader::EndElement) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (xml.name().toString() == "format") {
|
||||
auto rulesPtr = parseFormat(xml);
|
||||
emit addFormat(rulesPtr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CockatriceXml4Parser::loadSetsFromXml(QXmlStreamReader &xml)
|
||||
{
|
||||
while (!xml.atEnd()) {
|
||||
if (xml.readNext() == QXmlStreamReader::EndElement) {
|
||||
break;
|
||||
}
|
||||
|
||||
auto xmlName = xml.name().toString();
|
||||
if (xmlName == "set") {
|
||||
QString shortName, longName, setType;
|
||||
QDate releaseDate;
|
||||
short priority;
|
||||
while (!xml.atEnd()) {
|
||||
if (xml.readNext() == QXmlStreamReader::EndElement) {
|
||||
break;
|
||||
}
|
||||
xmlName = xml.name().toString();
|
||||
|
||||
if (xmlName == "name") {
|
||||
shortName = xml.readElementText(QXmlStreamReader::IncludeChildElements);
|
||||
} else if (xmlName == "longname") {
|
||||
longName = xml.readElementText(QXmlStreamReader::IncludeChildElements);
|
||||
} else if (xmlName == "settype") {
|
||||
setType = xml.readElementText(QXmlStreamReader::IncludeChildElements);
|
||||
} else if (xmlName == "releasedate") {
|
||||
releaseDate =
|
||||
QDate::fromString(xml.readElementText(QXmlStreamReader::IncludeChildElements), Qt::ISODate);
|
||||
} else if (xmlName == "priority") {
|
||||
priority = xml.readElementText(QXmlStreamReader::IncludeChildElements).toShort();
|
||||
} else if (!xmlName.isEmpty()) {
|
||||
qCInfo(CockatriceXml4Log) << "Unknown set property" << xmlName << ", trying to continue anyway";
|
||||
xml.skipCurrentElement();
|
||||
}
|
||||
}
|
||||
|
||||
internalAddSet(shortName, longName, setType, releaseDate, static_cast<CardSet::Priority>(priority));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QVariantHash CockatriceXml4Parser::loadCardPropertiesFromXml(QXmlStreamReader &xml)
|
||||
{
|
||||
QVariantHash properties = QVariantHash();
|
||||
while (!xml.atEnd()) {
|
||||
if (xml.readNext() == QXmlStreamReader::EndElement) {
|
||||
break;
|
||||
}
|
||||
|
||||
auto xmlName = xml.name().toString();
|
||||
if (!xmlName.isEmpty()) {
|
||||
properties.insert(xmlName, xml.readElementText(QXmlStreamReader::IncludeChildElements));
|
||||
}
|
||||
}
|
||||
return properties;
|
||||
}
|
||||
|
||||
void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml)
|
||||
{
|
||||
bool includeRebalancedCards = cardPreferenceProvider->getIncludeRebalancedCards();
|
||||
while (!xml.atEnd()) {
|
||||
if (xml.readNext() == QXmlStreamReader::EndElement) {
|
||||
break;
|
||||
}
|
||||
|
||||
auto xmlName = xml.name().toString();
|
||||
|
||||
if (xmlName == "card") {
|
||||
QString name = QString("");
|
||||
QString text = QString("");
|
||||
QVariantHash properties = QVariantHash();
|
||||
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 == "token") {
|
||||
isToken = static_cast<bool>(xml.readElementText(QXmlStreamReader::IncludeChildElements).toInt());
|
||||
// generic properties
|
||||
} else if (xmlName == "prop") {
|
||||
properties = loadCardPropertiesFromXml(xml);
|
||||
// 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 but be read before readElementText()
|
||||
QXmlStreamAttributes attrs = xml.attributes();
|
||||
QString setName = xml.readElementText(QXmlStreamReader::IncludeChildElements);
|
||||
auto set = internalAddSet(setName);
|
||||
if (set->getEnabled()) {
|
||||
PrintingInfo printingInfo(set);
|
||||
for (QXmlStreamAttribute attr : attrs) {
|
||||
QString attrName = attr.name().toString();
|
||||
if (attrName == "picURL") {
|
||||
attrName = "picurl";
|
||||
}
|
||||
printingInfo.setProperty(attrName, attr.value().toString());
|
||||
}
|
||||
|
||||
// This is very much a hack and not the right place to
|
||||
// put this check, as it requires a reload of Cockatrice
|
||||
// to be apply.
|
||||
//
|
||||
// However, this is also true of the `set->getEnabled()`
|
||||
// check above (which is currently bugged as well), so
|
||||
// we'll fix both at the same time.
|
||||
if (includeRebalancedCards || printingInfo.getProperty("isRebalanced") != "true") {
|
||||
_sets[setName].append(printingInfo);
|
||||
}
|
||||
}
|
||||
// related cards
|
||||
} else if (xmlName == "related" || xmlName == "reverse-related") {
|
||||
CardRelationType attachType = CardRelationType::DoesNotAttach;
|
||||
bool exclude = false;
|
||||
bool variable = false;
|
||||
bool persistent = false;
|
||||
bool facedown = 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")) {
|
||||
attachType = attrs.value("attach").toString() == "transform" ? CardRelationType::TransformInto
|
||||
: CardRelationType::AttachTo;
|
||||
}
|
||||
|
||||
if (attrs.hasAttribute("exclude")) {
|
||||
exclude = true;
|
||||
}
|
||||
|
||||
if (attrs.hasAttribute("persistent")) {
|
||||
persistent = true;
|
||||
}
|
||||
|
||||
if (attrs.hasAttribute("facedown")) {
|
||||
facedown = true;
|
||||
}
|
||||
|
||||
auto *relation =
|
||||
new CardRelation(cardName, attachType, exclude, variable, count, persistent, facedown);
|
||||
if (xmlName == "reverse-related") {
|
||||
reverseRelatedCards << relation;
|
||||
} else {
|
||||
relatedCards << relation;
|
||||
}
|
||||
} else if (!xmlName.isEmpty()) {
|
||||
qCInfo(CockatriceXml4Log) << "Unknown card property" << xmlName << ", trying to continue anyway";
|
||||
xml.skipCurrentElement();
|
||||
}
|
||||
}
|
||||
|
||||
if (name.isEmpty()) {
|
||||
qCWarning(CockatriceXml4Log) << "Encountered card with empty name; skipping";
|
||||
continue;
|
||||
}
|
||||
|
||||
CardInfo::UiAttributes attributes = {.cipt = cipt,
|
||||
.landscapeOrientation = landscapeOrientation,
|
||||
.tableRow = tableRow,
|
||||
.upsideDownArt = upsideDown};
|
||||
CardInfoPtr newCard = CardInfo::newInstance(name, text, isToken, properties, relatedCards,
|
||||
reverseRelatedCards, _sets, attributes);
|
||||
emit addCard(newCard);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const QSharedPointer<FormatRules> &rulesPtr)
|
||||
{
|
||||
if (rulesPtr.isNull()) {
|
||||
qCWarning(CockatriceXml4Log) << "&operator<< FormatRules is nullptr";
|
||||
return xml;
|
||||
}
|
||||
|
||||
const FormatRules &rules = *rulesPtr;
|
||||
|
||||
xml.writeStartElement("format");
|
||||
if (!rules.formatName.isEmpty()) {
|
||||
xml.writeAttribute("formatName", rules.formatName);
|
||||
}
|
||||
|
||||
xml.writeTextElement("minDeckSize", QString::number(rules.minDeckSize));
|
||||
xml.writeTextElement("maxDeckSize", rules.maxDeckSize >= 0 ? QString::number(rules.maxDeckSize) : "0");
|
||||
xml.writeTextElement("maxSideboardSize", QString::number(rules.maxSideboardSize));
|
||||
if (!rules.allowedCounts.isEmpty()) {
|
||||
xml.writeStartElement("allowedCounts");
|
||||
|
||||
for (const AllowedCount &c : rules.allowedCounts) {
|
||||
xml.writeStartElement("count");
|
||||
xml.writeAttribute("max", c.max == -1 ? "unlimited" : QString::number(c.max));
|
||||
xml.writeCharacters(c.label);
|
||||
xml.writeEndElement(); // count
|
||||
}
|
||||
|
||||
xml.writeEndElement(); // allowedCounts
|
||||
}
|
||||
|
||||
if (!rules.exceptions.isEmpty()) {
|
||||
xml.writeStartElement("exceptions");
|
||||
for (const ExceptionRule &ex : rules.exceptions) {
|
||||
xml.writeStartElement("exception");
|
||||
xml.writeTextElement("maxCopies", ex.maxCopies == -1 ? "unlimited" : QString::number(ex.maxCopies));
|
||||
|
||||
for (const CardCondition &cond : ex.conditions) {
|
||||
xml.writeStartElement("cardCondition");
|
||||
xml.writeAttribute("field", cond.field);
|
||||
xml.writeAttribute("match", cond.matchType);
|
||||
xml.writeAttribute("value", cond.value);
|
||||
xml.writeEndElement(); // cardCondition
|
||||
}
|
||||
|
||||
xml.writeEndElement(); // exception
|
||||
}
|
||||
xml.writeEndElement(); // exceptions
|
||||
}
|
||||
|
||||
xml.writeEndElement(); // format
|
||||
return xml;
|
||||
}
|
||||
|
||||
static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardSetPtr &set)
|
||||
{
|
||||
if (set.isNull()) {
|
||||
qCWarning(CockatriceXml4Log) << "&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.writeTextElement("priority", QString::number(set->getPriority()));
|
||||
xml.writeEndElement();
|
||||
|
||||
return xml;
|
||||
}
|
||||
|
||||
static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardInfoPtr &info)
|
||||
{
|
||||
if (info.isNull()) {
|
||||
qCWarning(CockatriceXml4Log) << "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.writeStartElement("prop");
|
||||
for (QString propName : info->getProperties()) {
|
||||
xml.writeTextElement(propName, info->getProperty(propName));
|
||||
}
|
||||
xml.writeEndElement();
|
||||
|
||||
// sets
|
||||
for (const auto &printings : info->getSets()) {
|
||||
for (const PrintingInfo &set : printings) {
|
||||
xml.writeStartElement("set");
|
||||
for (const QString &propName : set.getProperties()) {
|
||||
xml.writeAttribute(propName, set.getProperty(propName));
|
||||
}
|
||||
|
||||
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", i->getAttachTypeAsString());
|
||||
}
|
||||
if (i->getIsCreateAllExclusion()) {
|
||||
xml.writeAttribute("exclude", "exclude");
|
||||
}
|
||||
if (i->getIsPersistent()) {
|
||||
xml.writeAttribute("persistent", "persistent");
|
||||
}
|
||||
if (i->getIsFaceDown()) {
|
||||
xml.writeAttribute("facedown", "facedown");
|
||||
}
|
||||
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", i->getAttachTypeAsString());
|
||||
}
|
||||
|
||||
if (i->getIsCreateAllExclusion()) {
|
||||
xml.writeAttribute("exclude", "exclude");
|
||||
}
|
||||
|
||||
if (i->getIsPersistent()) {
|
||||
xml.writeAttribute("persistent", "persistent");
|
||||
}
|
||||
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 CockatriceXml4Parser::saveToFile(FormatRulesNameMap _formats,
|
||||
SetNameMap _sets,
|
||||
CardNameMap cards,
|
||||
const QString &fileName,
|
||||
const QString &sourceUrl,
|
||||
const QString &sourceVersion)
|
||||
{
|
||||
QFile file(fileName);
|
||||
if (!file.open(QIODevice::WriteOnly)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QXmlStreamWriter xml(&file);
|
||||
|
||||
xml.setAutoFormatting(true);
|
||||
xml.writeStartDocument();
|
||||
xml.writeStartElement(COCKATRICE_XML4_TAGNAME);
|
||||
xml.writeAttribute("version", QString::number(COCKATRICE_XML4_TAGVER));
|
||||
xml.writeAttribute("xmlns:xsi", COCKATRICE_XML_XSI_NAMESPACE);
|
||||
xml.writeAttribute("xsi:schemaLocation", COCKATRICE_XML4_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 (_formats.count() > 0) {
|
||||
xml.writeStartElement("formats");
|
||||
for (FormatRulesPtr format : _formats) {
|
||||
xml << format;
|
||||
}
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
#ifndef COCKATRICE_XML4_H
|
||||
#define COCKATRICE_XML4_H
|
||||
|
||||
#include "card_database_parser.h"
|
||||
|
||||
#include <QLoggingCategory>
|
||||
#include <QXmlStreamReader>
|
||||
#include <libcockatrice/interfaces/interface_card_preference_provider.h>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(CockatriceXml4Log, "cockatrice_xml.xml_4_parser");
|
||||
|
||||
/**
|
||||
* @class CockatriceXml4Parser
|
||||
* @ingroup CardDatabase
|
||||
* @brief Parses version 4 of the Cockatrice XML Schema.
|
||||
*
|
||||
* This parser reads a Cockatrice XML4 database and emits CardInfoPtr
|
||||
* and CardSetPtr objects. Card properties are read inside <prop> blocks,
|
||||
* making the parser more extensible and schema-compliant.
|
||||
*
|
||||
* @note Differences from v3:
|
||||
* - Card properties are stored in <prop> blocks as a QVariantHash.
|
||||
* - Sets can include a <priority> element.
|
||||
* - Supports user preferences via ICardPreferenceProvider (e.g., skipping rebalanced cards).
|
||||
* - Related cards support persistent relations and multiple attach types (e.g., transform).
|
||||
* - More robust serialization; easier to extend schema in the future.
|
||||
*/
|
||||
class CockatriceXml4Parser : public ICardDatabaseParser
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit CockatriceXml4Parser(ICardPreferenceProvider *cardPreferenceProvider,
|
||||
ICardSetPriorityController *cardSetPriorityController);
|
||||
~CockatriceXml4Parser() override = default;
|
||||
|
||||
/**
|
||||
* @brief Determines if the parser can handle this file.
|
||||
* @param name File name.
|
||||
* @param device Open QIODevice containing the XML.
|
||||
* @return True if the file is a Cockatrice XML4 database.
|
||||
*/
|
||||
bool getCanParseFile(const QString &name, QIODevice &device) override;
|
||||
|
||||
/**
|
||||
* @brief Parse the XML database.
|
||||
* @param device Open QIODevice positioned at start of file.
|
||||
*/
|
||||
void parseFile(QIODevice &device) override;
|
||||
|
||||
/**
|
||||
* @brief Save sets and cards back to an XML4 file.
|
||||
*/
|
||||
bool saveToFile(FormatRulesNameMap _formats,
|
||||
SetNameMap _sets,
|
||||
CardNameMap cards,
|
||||
const QString &fileName,
|
||||
const QString &sourceUrl = "unknown",
|
||||
const QString &sourceVersion = "unknown") override;
|
||||
|
||||
private:
|
||||
ICardPreferenceProvider *cardPreferenceProvider; ///< Interface to handle user preferences
|
||||
|
||||
/**
|
||||
* @brief Loads a generic <prop> block from a <card> element.
|
||||
* @param xml The open QXmlStreamReader positioned at a <prop> element.
|
||||
* @return A QVariantHash mapping property names to values.
|
||||
*/
|
||||
QVariantHash loadCardPropertiesFromXml(QXmlStreamReader &xml);
|
||||
|
||||
/**
|
||||
* @brief Load all <card> elements from the XML stream.
|
||||
* @param xml The open QXmlStreamReader positioned at the <cards> element.
|
||||
* Honors the user's preference regarding rebalanced cards.
|
||||
*/
|
||||
void loadCardsFromXml(QXmlStreamReader &xml);
|
||||
|
||||
void loadFormats(QXmlStreamReader &xml);
|
||||
/**
|
||||
* @brief Load all <set> elements from the XML stream.
|
||||
* @param xml The open QXmlStreamReader positioned at the <sets> element.
|
||||
* Parses <set> nodes including priority information.
|
||||
*/
|
||||
void loadSetsFromXml(QXmlStreamReader &xml);
|
||||
};
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user