* Better support Double-Faced Cards This patch allows cards to be (virtually) transformed into other cards while preserving their state, essentially implemeting the MTG mechanic of the same name. On the server side, this is implemented by allowing cards to be "stashed away". A card that is stashed away is not in any zone, but is instead owned by another card. When a token is destroyed due to a zone change, if it had a card stashed away, that card is placed in the target zone instead of the token. On the database side, `attach="transform"` is used on `<reverse>` and `<reverse-related>` to indicate that the created token should be transformed this way. Old servers ignore the new field in `Command_CreateToken` and will perform a regular attachment, as currently. * Address review comments * Prevent tokens from being stashed * format.sh
406 lines
15 KiB
C++
406 lines
15 KiB
C++
#include "cockatricexml4.h"
|
|
|
|
#include <QCoreApplication>
|
|
#include <QDebug>
|
|
#include <QFile>
|
|
#include <QXmlStreamReader>
|
|
#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"
|
|
|
|
bool CockatriceXml4Parser::getCanParseFile(const QString &fileName, QIODevice &device)
|
|
{
|
|
qDebug() << "[CockatriceXml4Parser] Trying to parse: " << fileName;
|
|
|
|
if (!fileName.endsWith(".xml", Qt::CaseInsensitive)) {
|
|
qDebug() << "[CockatriceXml4Parser] 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 {
|
|
qDebug() << "[CockatriceXml4Parser] Parsing failed: wrong version" << version;
|
|
return false;
|
|
}
|
|
|
|
} else {
|
|
qDebug() << "[CockatriceXml4Parser] 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 == "sets") {
|
|
loadSetsFromXml(xml);
|
|
} else if (xmlName == "cards") {
|
|
loadCardsFromXml(xml);
|
|
} else if (!xmlName.isEmpty()) {
|
|
qDebug() << "[CockatriceXml4Parser] Unknown item" << xmlName << ", trying to continue anyway";
|
|
xml.skipCurrentElement();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
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.isEmpty()) {
|
|
qDebug() << "[CockatriceXml4Parser] Unknown set property" << xmlName
|
|
<< ", trying to continue anyway";
|
|
xml.skipCurrentElement();
|
|
}
|
|
}
|
|
|
|
internalAddSet(shortName, longName, setType, releaseDate);
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
{
|
|
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;
|
|
CardInfoPerSetMap sets = CardInfoPerSetMap();
|
|
int tableRow = 0;
|
|
bool cipt = 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 == "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()) {
|
|
CardInfoPerSet setInfo(set);
|
|
for (QXmlStreamAttribute attr : attrs) {
|
|
QString attrName = attr.name().toString();
|
|
if (attrName == "picURL")
|
|
attrName = "picurl";
|
|
setInfo.setProperty(attrName, attr.value().toString());
|
|
}
|
|
sets.insert(setName, setInfo);
|
|
}
|
|
// related cards
|
|
} else if (xmlName == "related" || xmlName == "reverse-related") {
|
|
CardRelation::AttachType attachType = CardRelation::DoesNotAttach;
|
|
bool exclude = false;
|
|
bool variable = false;
|
|
bool persistent = 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" ? CardRelation::TransformInto
|
|
: CardRelation::AttachTo;
|
|
}
|
|
|
|
if (attrs.hasAttribute("exclude")) {
|
|
exclude = true;
|
|
}
|
|
|
|
if (attrs.hasAttribute("persistent")) {
|
|
persistent = true;
|
|
}
|
|
|
|
auto *relation = new CardRelation(cardName, attachType, exclude, variable, count, persistent);
|
|
if (xmlName == "reverse-related") {
|
|
reverseRelatedCards << relation;
|
|
} else {
|
|
relatedCards << relation;
|
|
}
|
|
} else if (!xmlName.isEmpty()) {
|
|
qDebug() << "[CockatriceXml4Parser] Unknown card property" << xmlName
|
|
<< ", trying to continue anyway";
|
|
xml.skipCurrentElement();
|
|
}
|
|
}
|
|
|
|
CardInfoPtr newCard = CardInfo::newInstance(name, text, isToken, properties, relatedCards,
|
|
reverseRelatedCards, sets, cipt, tableRow, upsideDown);
|
|
emit addCard(newCard);
|
|
}
|
|
}
|
|
}
|
|
|
|
static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardSetPtr &set)
|
|
{
|
|
if (set.isNull()) {
|
|
qDebug() << "&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()) {
|
|
qDebug() << "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 (CardInfoPerSet set : info->getSets()) {
|
|
xml.writeStartElement("set");
|
|
for (QString propName : set.getProperties()) {
|
|
xml.writeAttribute(propName, set.getProperty(propName));
|
|
}
|
|
|
|
xml.writeCharacters(set.getPtr()->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->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
|
|
xml.writeTextElement("tablerow", QString::number(info->getTableRow()));
|
|
if (info->getCipt()) {
|
|
xml.writeTextElement("cipt", "1");
|
|
}
|
|
if (info->getUpsideDownArt()) {
|
|
xml.writeTextElement("upsidedown", "1");
|
|
}
|
|
|
|
xml.writeEndElement(); // card
|
|
|
|
return xml;
|
|
}
|
|
|
|
bool CockatriceXml4Parser::saveToFile(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 (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;
|
|
}
|