mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-21 00:55:09 -07:00
* [Client] Show localized card names, texts and pictures
Localization wiring now runs end to end: the oracle importer collects
foreignData for the configured language and the client renders it.
- [Oracle] Import localized names and rules texts for the selected cardLang
- single-face cards store their foreignData name and full text
- multi-face (split/adventure/aftermath/prepare) cards collect the joined
name once and join each face's translated text with the same separator
as the English merge; an incomplete translation falls back to English;
the joined text follows the same highest-priority-set policy as the
single-face path and is only collected when localization is enabled
- the wizard switching languages re-imports the card database
- [Client] Display localized card info throughout the client
- card info text/picture widgets and the game board re-render on language
change
- pictures resolve cardLang art through Scryfall's named endpoint using the
localized name, falling back to id-based art when no match exists
- deck editor keeps canonical English names as card identity (EditRole)
while showing localized names (DisplayRole), so decks and wire names
stay stable
- [Card] Add CardLocalization-backed name/text lookup and cards.xml v4
localization elements with a bounded-size translation cache
- [Tests] Cover oracle foreignData import (incl. multi-face joins, priority
and fallback paths), XML v4 localization parsing, deck model localized
display and the language-aware settings default
Existing installations need to re-run Oracle to see translations: localized
data only lands in cards.xml when the Oracle app is started with the
preferred language selected — launch the separate "Oracle" program that
ships with Cockatrice, pick the language in the wizard and let it re-import
the card database.
The client's database cache (cards.xml.cache) is invalidated by the cache
format bump and the source-hash checks, but a cache written before the
re-import can still hold English-only entries (the hash uses file size and
mtime, so a same-size/same-timestamp rewrite may be served as-is); delete
cards.xml.cache and relaunch if no localized names/texts show up after
re-importing.
* [Card] Pass localized card names and texts into CardInfo construction
Address review: instead of constructing the card and then calling
setLocalizedName/setLocalizedText (which emit a cardInfoChanged signal per
language), both constructors, both newInstance overloads and their callers
(cards.xml v4 parser and the binary cache reader) now pass the localized maps
as constructor arguments.
* [Client] Rename LocalizedCard:: helpers namespace to CardLocalization
The namespace now matches its header file name, as the review pointed out;
LocalizedCard reads more like a class or struct. Callers (card info text
widget, board card name rendering) are updated to match.
* [Client] Drop unused info member from the card info text widget
The CardInfoPtr member was only ever initialized to nullptr and never read;
remove it together with its initializer.
* [PictureLoader] Add the localized picture URL explicitly, not implicitly
Address review: silently prepending the Scryfall named-picture URL to the
download list whenever a non-English card language was active was surprising,
consumed quota per card when it failed, and could grab the wrong (canon) art on
name collisions, with no way to turn it off.
The insert is now opt-in and user-controlled: changing the card language adds
the template to the top of the download URLs once (persisted, documented in the
re-import prompt, and editable/removable in the deck editor settings), while the
picture loader no longer injects it at request time.
* [Card] Show card languages in the same native (English) format as the UI
Address review: the card text & images language dropdown listed bare native
names, some in inconsistent lowercase (e.g. "čeština", "español de España"),
which makes the languages easy to mix up for users that do not read the script
(e.g. 日本語 vs 한국어). It now mirrors the UI language dropdown and always pairs
the native name with its English name (e.g. "Deutsch (German)",
"日本語 (Japanese)"), using the same fixed casing.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
723 lines
28 KiB
C++
723 lines
28 KiB
C++
#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();
|
|
}
|
|
}
|
|
|
|
void CockatriceXml4Parser::parseFileInto(QIODevice &device, CardDatabaseData &data)
|
|
{
|
|
targetData = &data;
|
|
parseFile(device);
|
|
targetData = nullptr;
|
|
}
|
|
|
|
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);
|
|
if (targetData) {
|
|
targetData->formats.insert(rulesPtr->formatName.toLower(), rulesPtr);
|
|
} else {
|
|
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));
|
|
}
|
|
}
|
|
}
|
|
|
|
QHash<QString, QString> CockatriceXml4Parser::loadCardPropertiesFromXml(QXmlStreamReader &xml)
|
|
{
|
|
QHash<QString, QString> properties;
|
|
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("");
|
|
QHash<QString, QString> properties;
|
|
QMap<QString, QString> localizedNames;
|
|
QMap<QString, QString> localizedTexts;
|
|
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);
|
|
// localized card data
|
|
} else if (xmlName == "localizations") {
|
|
while (!xml.atEnd()) {
|
|
if (xml.readNextStartElement()) {
|
|
const QString elementName = xml.name().toString();
|
|
if (elementName == "localization") {
|
|
const QString lang = xml.attributes().value("lang").toString();
|
|
QString localizedName;
|
|
QString localizedText;
|
|
while (!xml.atEnd()) {
|
|
if (xml.readNext() == QXmlStreamReader::EndElement) {
|
|
break;
|
|
}
|
|
if (xml.isStartElement()) {
|
|
const QString childName = xml.name().toString();
|
|
QString value = xml.readElementText(QXmlStreamReader::IncludeChildElements);
|
|
if (childName == "name") {
|
|
localizedName = value;
|
|
} else if (childName == "text") {
|
|
localizedText = value;
|
|
}
|
|
}
|
|
}
|
|
if (!lang.isEmpty()) {
|
|
if (!localizedName.isEmpty()) {
|
|
localizedNames.insert(lang, localizedName);
|
|
}
|
|
if (!localizedText.isEmpty()) {
|
|
localizedTexts.insert(lang, localizedText);
|
|
}
|
|
}
|
|
} else {
|
|
xml.skipCurrentElement();
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
// 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()) {
|
|
QHash<QString, QString> printingProps;
|
|
for (QXmlStreamAttribute attr : attrs) {
|
|
QString attrName = attr.name().toString();
|
|
if (attrName == "picURL") {
|
|
attrName = "picurl";
|
|
}
|
|
printingProps.insert(attrName, attr.value().toString());
|
|
}
|
|
PrintingInfo printingInfo(set, LazyPropertiesHash(printingProps));
|
|
|
|
// 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, std::move(localizedNames), std::move(localizedTexts));
|
|
if (targetData) {
|
|
// Mirror CardDatabase::addCard: if a card with this name already
|
|
// exists, merge the new printings into it instead of replacing.
|
|
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 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();
|
|
|
|
// localized card data
|
|
const QStringList localizedLanguages = info->localizationLanguages();
|
|
if (!localizedLanguages.isEmpty()) {
|
|
xml.writeStartElement("localizations");
|
|
const QMap<QString, QString> &localizedNames = info->getLocalizedNames();
|
|
const QMap<QString, QString> &localizedTexts = info->getLocalizedTexts();
|
|
for (const QString &lang : localizedLanguages) {
|
|
xml.writeStartElement("localization");
|
|
xml.writeAttribute("lang", lang);
|
|
if (localizedNames.contains(lang)) {
|
|
xml.writeTextElement("name", localizedNames.value(lang));
|
|
}
|
|
if (localizedTexts.contains(lang)) {
|
|
xml.writeTextElement("text", localizedTexts.value(lang));
|
|
}
|
|
xml.writeEndElement();
|
|
}
|
|
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;
|
|
}
|