[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.
This commit is contained in:
Lukas Brübach 2026-09-17 15:19:48 +02:00 committed by GitHub
parent 9e312a0ef4
commit 6669c341a9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 150 additions and 90 deletions

View file

@ -9,6 +9,7 @@ set(HEADERS
libcockatrice/deck_list/tree/inner_deck_list_node.h
libcockatrice/deck_list/deck_list.h
libcockatrice/deck_list/deck_list_history_manager.h
libcockatrice/deck_list/deck_list_metadata_xml.h
libcockatrice/deck_list/deck_list_node_tree.h
libcockatrice/deck_list/deck_list_memento.h
libcockatrice/deck_list/deck_list_plain_text_parser.h
@ -27,6 +28,7 @@ add_library(
libcockatrice/deck_list/tree/inner_deck_list_node.cpp
libcockatrice/deck_list/deck_list.cpp
libcockatrice/deck_list/deck_list_history_manager.cpp
libcockatrice/deck_list/deck_list_metadata_xml.cpp
libcockatrice/deck_list/deck_list_node_tree.cpp
libcockatrice/deck_list/deck_list_plain_text_parser.cpp
libcockatrice/deck_list/playmat_resolver.cpp

View file

@ -1,6 +1,7 @@
#include "deck_list.h"
#include "deck_list_memento.h"
#include "deck_list_metadata_xml.h"
#include "deck_list_plain_text_parser.h"
#include "tree/abstract_deck_list_node.h"
#include "tree/deck_list_card_node.h"
@ -15,29 +16,6 @@
static const QString CURRENT_SIDEBOARD_PLAN_KEY = "";
/**
* @brief Parses a floating point XML attribute into a clamped playmat parameter.
*
* Falls back to @p fallback when the attribute is missing or malformed, so
* malformed deck files cannot produce degenerate art rectangles (e.g. a zoom
* of 0 dividing by zero).
*
* @param valueString Raw attribute text.
* @param fallback Value used when the text cannot be parsed.
* @param min Lower clamp bound.
* @param max Upper clamp bound.
* @return The parsed value clamped to [min, max], or @p fallback.
*/
static double parseClampedParam(const QString &valueString, double fallback, double min, double max)
{
bool ok = false;
const double value = valueString.toDouble(&ok);
if (!ok) {
return fallback;
}
return qBound(min, value, max);
}
bool DeckList::Metadata::isEmpty() const
{
return name.isEmpty() && comments.isEmpty() && bannerCard.isEmpty() && tags.isEmpty() && playmat.card.isEmpty();
@ -77,43 +55,10 @@ bool DeckList::readElement(QXmlStreamReader *xml)
{
const QString childName = xml->name().toString();
if (xml->isStartElement()) {
if (childName == "lastLoadedTimestamp") {
metadata.lastLoadedTimestamp = xml->readElementText();
} else if (childName == "deckname") {
metadata.name = xml->readElementText();
} 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") {
if (DeckListMetadataXml::readElement(xml, childName, metadata)) {
return true;
}
if (childName == "zone") {
tree.readZoneElement(xml);
} else if (childName == "sideboard_plan") {
SideboardPlan newSideboardPlan;
@ -127,41 +72,12 @@ bool DeckList::readElement(QXmlStreamReader *xml)
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
{
xml->writeStartElement("cockatrice_deck");
xml->writeAttribute("version", "1");
writeMetadata(xml, metadata);
DeckListMetadataXml::write(xml, metadata);
// Write zones
tree.write(xml);

View file

@ -0,0 +1,107 @@
#include "deck_list_metadata_xml.h"
#include <QtCore/QXmlStreamReader>
#include <QtCore/QXmlStreamWriter>
#include <libcockatrice/utility/playmat_params.h>
namespace DeckListMetadataXml
{
/**
* @brief Parses a floating point XML attribute into a clamped playmat parameter.
*
* Falls back to @p fallback when the attribute is missing or malformed, so
* malformed deck files cannot produce degenerate art rectangles (e.g. a zoom
* of 0 dividing by zero).
*
* @param valueString Raw attribute text.
* @param fallback Value used when the text cannot be parsed.
* @param min Lower clamp bound.
* @param max Upper clamp bound.
* @return The parsed value clamped to [min, max], or @p fallback.
*/
static double parseClampedParam(const QString &valueString, double fallback, double min, double max)
{
bool ok = false;
const double value = valueString.toDouble(&ok);
if (!ok) {
return fallback;
}
return qBound(min, value, max);
}
bool readElement(QXmlStreamReader *xml, const QString &childName, DeckList::Metadata &metadata)
{
if (childName == "lastLoadedTimestamp") {
metadata.lastLoadedTimestamp = xml->readElementText();
} else if (childName == "deckname") {
metadata.name = xml->readElementText();
} 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();
// 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();
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 {
return false;
}
return true;
}
void write(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();
}
} // namespace DeckListMetadataXml

View file

@ -0,0 +1,35 @@
#ifndef COCKATRICE_DECK_LIST_METADATA_XML_H
#define COCKATRICE_DECK_LIST_METADATA_XML_H
#include "deck_list.h"
#include <QString>
class QXmlStreamReader;
class QXmlStreamWriter;
namespace DeckListMetadataXml
{
/**
* @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.
* @param metadata Metadata to fill in.
* @return true if a metadata element was consumed, false if @p childName is
* not a metadata element.
*/
bool readElement(QXmlStreamReader *xml, const QString &childName, DeckList::Metadata &metadata);
/**
* @brief Writes the deck metadata section of a Cockatrice deck XML file.
*
* @param xml Writer to append the metadata elements to.
* @param metadata The metadata to serialize.
*/
void write(QXmlStreamWriter *xml, const DeckList::Metadata &metadata);
} // namespace DeckListMetadataXml
#endif // COCKATRICE_DECK_LIST_METADATA_XML_H