[DeckList] Extract deck metadata XML serialization (#7306)

* [DeckList] Extract deck metadata XML serialization

DeckList still serialized its metadata inline: a ten-branch
readElement dispatch and a static writeMetadata that duplicated the
tree plumbing. The metadata arms (name, comments, format, timestamp,
banner card, playmat, tags) move to DeckListMetadataXml free
functions over the Metadata struct, leaving DeckList::readElement a
thin dispatcher between metadata, zones and sideboard plans. The
playmat clamping helper moves along with the element that uses it.

* [DeckList] Make deck metadata XML serialization instance methods

* [DeckList] Inline deck metadata XML serialization

Fold DeckList::Metadata::readElement and write back into deck_list.cpp
alongside isEmpty(), and drop the separate deck_list_metadata_xml
translation unit. The metadata arms are instance methods of the nested
Metadata struct, so keeping them in the same file as its other method
keeps the class from being scattered across two .cpp files; the rest of
the refactor (readElement as a thin dispatcher, element-wise reads,
clamped playmat params) is unchanged.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL 2026-09-18 11:58:31 +02:00 committed by GitHub
parent 69d32ed853
commit 5ace88c111
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 94 additions and 67 deletions

View file

@ -43,6 +43,78 @@ bool DeckList::Metadata::isEmpty() const
return name.isEmpty() && comments.isEmpty() && bannerCard.isEmpty() && tags.isEmpty() && playmat.card.isEmpty(); return name.isEmpty() && comments.isEmpty() && bannerCard.isEmpty() && tags.isEmpty() && playmat.card.isEmpty();
} }
bool DeckList::Metadata::readElement(QXmlStreamReader *xml, const QString &childName)
{
if (childName == "lastLoadedTimestamp") {
lastLoadedTimestamp = xml->readElementText();
} else if (childName == "deckname") {
name = xml->readElementText();
} else if (childName == "format") {
gameFormat = xml->readElementText();
} else if (childName == "comments") {
comments = xml->readElementText();
} else if (childName == "bannerCard") {
QString providerId = xml->attributes().value("providerId").toString();
QString cardName = xml->readElementText();
bannerCard = {cardName, providerId};
} else if (childName == "playmatCard") {
QString providerId = xml->attributes().value("providerId").toString();
// Attributes are read before readElementText consumes the element.
QString marginLStr = xml->attributes().value("marginPctL").toString();
QString marginRStr = xml->attributes().value("marginPctR").toString();
QString vOffStr = xml->attributes().value("verticalOffset").toString();
QString zoomStr = xml->attributes().value("zoom").toString();
QString cardName = xml->readElementText();
playmat.card = {cardName, providerId};
// Clamp to the same ranges as the settings dialog and the remote
// player-properties path so malformed deck files cannot produce
// degenerate art rectangles (e.g. a zoom of 0 dividing by zero).
playmat.params.marginPctL = parseClampedParam(marginLStr, 0.07, 0.0, 0.95);
playmat.params.marginPctR = parseClampedParam(marginRStr, 0.07, 0.0, 0.95);
playmat.params.verticalOffset = parseClampedParam(vOffStr, 0.33, 0.0, 1.0);
playmat.params.zoom = parseClampedParam(zoomStr, 1.0, 0.1, 4.0);
} else if (childName == "tags") {
tags.clear(); // Clear existing tags
while (xml->readNextStartElement()) {
if (xml->name().toString() == "tag") {
tags.append(xml->readElementText());
}
}
} else {
return false;
}
return true;
}
void DeckList::Metadata::write(QXmlStreamWriter *xml) const
{
xml->writeTextElement("lastLoadedTimestamp", lastLoadedTimestamp);
xml->writeTextElement("deckname", name);
xml->writeTextElement("format", gameFormat);
xml->writeStartElement("bannerCard");
xml->writeAttribute("providerId", bannerCard.providerId);
xml->writeCharacters(bannerCard.name);
xml->writeEndElement();
if (!playmat.card.isEmpty()) {
xml->writeStartElement("playmatCard");
xml->writeAttribute("providerId", playmat.card.providerId);
xml->writeAttribute("marginPctL", QString::number(playmat.params.marginPctL, 'f', 4));
xml->writeAttribute("marginPctR", QString::number(playmat.params.marginPctR, 'f', 4));
xml->writeAttribute("verticalOffset", QString::number(playmat.params.verticalOffset, 'f', 4));
xml->writeAttribute("zoom", QString::number(playmat.params.zoom, 'f', 4));
xml->writeCharacters(playmat.card.name);
xml->writeEndElement();
}
xml->writeTextElement("comments", comments);
// Write tags
xml->writeStartElement("tags");
for (const QString &tag : tags) {
xml->writeTextElement("tag", tag);
}
xml->writeEndElement();
}
DeckList::DeckList() DeckList::DeckList()
{ {
} }
@ -77,43 +149,10 @@ bool DeckList::readElement(QXmlStreamReader *xml)
{ {
const QString childName = xml->name().toString(); const QString childName = xml->name().toString();
if (xml->isStartElement()) { if (xml->isStartElement()) {
if (childName == "lastLoadedTimestamp") { if (metadata.readElement(xml, childName)) {
metadata.lastLoadedTimestamp = xml->readElementText(); return true;
} else if (childName == "deckname") { }
metadata.name = xml->readElementText(); if (childName == "zone") {
} else if (childName == "format") {
metadata.gameFormat = xml->readElementText();
} else if (childName == "comments") {
metadata.comments = xml->readElementText();
} else if (childName == "bannerCard") {
QString providerId = xml->attributes().value("providerId").toString();
QString cardName = xml->readElementText();
metadata.bannerCard = {cardName, providerId};
} else if (childName == "playmatCard") {
QString providerId = xml->attributes().value("providerId").toString();
QString marginLStr = xml->attributes().value("marginPctL").toString();
QString marginRStr = xml->attributes().value("marginPctR").toString();
QString vOffStr = xml->attributes().value("verticalOffset").toString();
QString zoomStr = xml->attributes().value("zoom").toString();
QString cardName = xml->readElementText();
PlaymatInfo playmat;
playmat.card = {cardName, providerId};
// Clamp to the same ranges as the settings dialog and the remote
// player-properties path so malformed deck files cannot produce
// degenerate art rectangles (e.g. a zoom of 0 dividing by zero).
playmat.params.marginPctL = parseClampedParam(marginLStr, 0.07, 0.0, 0.95);
playmat.params.marginPctR = parseClampedParam(marginRStr, 0.07, 0.0, 0.95);
playmat.params.verticalOffset = parseClampedParam(vOffStr, 0.33, 0.0, 1.0);
playmat.params.zoom = parseClampedParam(zoomStr, 1.0, 0.1, 4.0);
metadata.playmat = playmat;
} else if (childName == "tags") {
metadata.tags.clear(); // Clear existing tags
while (xml->readNextStartElement()) {
if (xml->name().toString() == "tag") {
metadata.tags.append(xml->readElementText());
}
}
} else if (childName == "zone") {
tree.readZoneElement(xml); tree.readZoneElement(xml);
} else if (childName == "sideboard_plan") { } else if (childName == "sideboard_plan") {
SideboardPlan newSideboardPlan; SideboardPlan newSideboardPlan;
@ -127,41 +166,12 @@ bool DeckList::readElement(QXmlStreamReader *xml)
return true; return true;
} }
static void writeMetadata(QXmlStreamWriter *xml, const DeckList::Metadata &metadata)
{
xml->writeTextElement("lastLoadedTimestamp", metadata.lastLoadedTimestamp);
xml->writeTextElement("deckname", metadata.name);
xml->writeTextElement("format", metadata.gameFormat);
xml->writeStartElement("bannerCard");
xml->writeAttribute("providerId", metadata.bannerCard.providerId);
xml->writeCharacters(metadata.bannerCard.name);
xml->writeEndElement();
if (!metadata.playmat.card.isEmpty()) {
xml->writeStartElement("playmatCard");
xml->writeAttribute("providerId", metadata.playmat.card.providerId);
xml->writeAttribute("marginPctL", QString::number(metadata.playmat.params.marginPctL, 'f', 4));
xml->writeAttribute("marginPctR", QString::number(metadata.playmat.params.marginPctR, 'f', 4));
xml->writeAttribute("verticalOffset", QString::number(metadata.playmat.params.verticalOffset, 'f', 4));
xml->writeAttribute("zoom", QString::number(metadata.playmat.params.zoom, 'f', 4));
xml->writeCharacters(metadata.playmat.card.name);
xml->writeEndElement();
}
xml->writeTextElement("comments", metadata.comments);
// Write tags
xml->writeStartElement("tags");
for (const QString &tag : metadata.tags) {
xml->writeTextElement("tag", tag);
}
xml->writeEndElement();
}
void DeckList::write(QXmlStreamWriter *xml) const void DeckList::write(QXmlStreamWriter *xml) const
{ {
xml->writeStartElement("cockatrice_deck"); xml->writeStartElement("cockatrice_deck");
xml->writeAttribute("version", "1"); xml->writeAttribute("version", "1");
writeMetadata(xml, metadata); metadata.write(xml);
// Write zones // Write zones
tree.write(xml); tree.write(xml);

View file

@ -76,6 +76,23 @@ public:
* @brief Checks if all values (except for lastLoadedTimestamp) in the metadata is empty. * @brief Checks if all values (except for lastLoadedTimestamp) in the metadata is empty.
*/ */
bool isEmpty() const; bool isEmpty() const;
/**
* @brief Reads a single deck metadata element from a Cockatrice deck XML stream.
*
* @param xml Reader positioned at the element.
* @param childName Name of the current element.
* @return true if a metadata element was consumed, false if @p childName is
* not a metadata element.
*/
bool readElement(QXmlStreamReader *xml, const QString &childName);
/**
* @brief Writes the deck metadata section of a Cockatrice deck XML file.
*
* @param xml Writer to append the metadata elements to.
*/
void write(QXmlStreamWriter *xml) const;
}; };
private: private: