From 5ace88c11125b0315356946d9c7e606297284f52 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Fri, 18 Sep 2026 11:58:31 +0200 Subject: [PATCH] [DeckList] Extract deck metadata XML serialization (#7306) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [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 --- .../libcockatrice/deck_list/deck_list.cpp | 144 ++++++++++-------- .../libcockatrice/deck_list/deck_list.h | 17 +++ 2 files changed, 94 insertions(+), 67 deletions(-) diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp index c2134c21d..0013cb475 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp @@ -43,6 +43,78 @@ bool DeckList::Metadata::isEmpty() const 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() { } @@ -77,43 +149,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 (metadata.readElement(xml, childName)) { + return true; + } + if (childName == "zone") { tree.readZoneElement(xml); } else if (childName == "sideboard_plan") { SideboardPlan newSideboardPlan; @@ -127,41 +166,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); + metadata.write(xml); // Write zones tree.write(xml); diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h index 475d99560..229e2077c 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h @@ -76,6 +76,23 @@ public: * @brief Checks if all values (except for lastLoadedTimestamp) in the metadata is empty. */ 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: