[Game] Playmats (#7101)

* [Game] Playmats

Took 19 seconds

Took 1 minute

* [Playmats] Add fixed override and configurable fallbacks to settings.

Took 29 minutes

Took 43 seconds

* Add main to test.

Took 1 minute

Took 29 seconds

* Move settings to own group

Took 11 minutes

* Some attempts to refresh macOS compositor

Took 2 minutes

* Try something else

Took 17 minutes

* Don't manipulate live list

Took 11 minutes

* Change things about resolution, address comments.

Took 45 minutes

Took 12 minutes

* Comments.

Took 14 minutes

Took 8 seconds

* Re-order settings menu location

Took 2 minutes

* Rename PlaymatResolution to Info and add enums

Took 8 minutes

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL 2026-08-21 10:40:49 +02:00 committed by GitHub
parent 74a454552a
commit 9eafd90a91
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
52 changed files with 1931 additions and 28 deletions

View file

@ -11,6 +11,7 @@ set(HEADERS
libcockatrice/deck_list/deck_list_history_manager.h
libcockatrice/deck_list/deck_list_node_tree.h
libcockatrice/deck_list/deck_list_memento.h
libcockatrice/deck_list/playmat_resolver.h
libcockatrice/deck_list/sideboard_plan.h
)
@ -26,6 +27,7 @@ add_library(
libcockatrice/deck_list/deck_list.cpp
libcockatrice/deck_list/deck_list_history_manager.cpp
libcockatrice/deck_list/deck_list_node_tree.cpp
libcockatrice/deck_list/playmat_resolver.cpp
libcockatrice/deck_list/sideboard_plan.cpp
)
@ -33,4 +35,7 @@ add_dependencies(libcockatrice_deck_list libcockatrice_protocol)
target_include_directories(libcockatrice_deck_list PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(libcockatrice_deck_list PUBLIC libcockatrice_protocol libcockatrice_utility ${QT_CORE_MODULE})
target_link_libraries(
libcockatrice_deck_list PUBLIC libcockatrice_interfaces libcockatrice_protocol libcockatrice_utility
${QT_CORE_MODULE}
)

View file

@ -25,7 +25,7 @@ static const QString CURRENT_SIDEBOARD_PLAN_KEY = "";
bool DeckList::Metadata::isEmpty() const
{
return name.isEmpty() && comments.isEmpty() && bannerCard.isEmpty() && tags.isEmpty();
return name.isEmpty() && comments.isEmpty() && bannerCard.isEmpty() && tags.isEmpty() && playmat.card.isEmpty();
}
DeckList::DeckList()
@ -74,6 +74,36 @@ bool DeckList::readElement(QXmlStreamReader *xml)
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();
bool ok;
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 = qBound(0.0, marginLStr.toDouble(&ok), 0.95);
if (!ok) {
playmat.params.marginPctL = 0.07;
}
playmat.params.marginPctR = qBound(0.0, marginRStr.toDouble(&ok), 0.95);
if (!ok) {
playmat.params.marginPctR = 0.07;
}
playmat.params.verticalOffset = qBound(0.0, vOffStr.toDouble(&ok), 1.0);
if (!ok) {
playmat.params.verticalOffset = 0.33;
}
playmat.params.zoom = qBound(0.1, zoomStr.toDouble(&ok), 4.0);
if (!ok) {
playmat.params.zoom = 1.0;
}
metadata.playmat = playmat;
} else if (childName == "tags") {
metadata.tags.clear(); // Clear existing tags
while (xml->readNextStartElement()) {
@ -104,6 +134,16 @@ static void writeMetadata(QXmlStreamWriter *xml, const DeckList::Metadata &metad
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

View file

@ -18,7 +18,7 @@
#include <QMap>
#include <QVector>
#include <QtCore/QXmlStreamReader>
#include <libcockatrice/utility/card_ref.h>
#include <libcockatrice/utility/playmat_params.h>
class AbstractDecklistNode;
class DecklistCardNode;
@ -70,6 +70,7 @@ public:
CardRef bannerCard; ///< Optional representative card for the deck.
QStringList tags; ///< User-defined tags for deck classification.
QString lastLoadedTimestamp; ///< Timestamp string of last load.
PlaymatInfo playmat; ///< Optional playmat background for table+stack zones.
/**
* @brief Checks if all values (except for lastLoadedTimestamp) in the metadata is empty.
@ -115,6 +116,10 @@ public:
{
metadata.bannerCard = _bannerCard;
}
void setPlaymat(const PlaymatInfo &_playmat = {})
{
metadata.playmat = _playmat;
}
void setLastLoadedTimestamp(const QString &_lastLoadedTimestamp = QString())
{
metadata.lastLoadedTimestamp = _lastLoadedTimestamp;
@ -170,6 +175,10 @@ public:
{
return metadata.bannerCard;
}
PlaymatInfo getPlaymat() const
{
return metadata.playmat;
}
QString getLastLoadedTimestamp() const
{
return metadata.lastLoadedTimestamp;

View file

@ -0,0 +1,54 @@
#include "playmat_resolver.h"
#include <QRandomGenerator>
PlaymatInfo resolveEffectivePlaymat(const DeckList &deck,
const PlaymatInfo &force,
const QList<PlaymatInfo> &fallbackList,
PlaymatFallbackMode fallbackMode,
int rotationIndex)
{
if (!force.card.isEmpty()) {
return force;
}
const PlaymatInfo &deckPlaymat = deck.getPlaymat();
if (!deckPlaymat.card.isEmpty()) {
return deckPlaymat;
}
if (fallbackList.isEmpty()) {
return {};
}
switch (fallbackMode) {
case PlaymatFallbackModeFixed:
return fallbackList.first();
case PlaymatFallbackModeRoundRobin:
return fallbackList.at(rotationIndex % fallbackList.size());
case PlaymatFallbackModeRandom:
return fallbackList.at(QRandomGenerator::global()->bounded(fallbackList.size()));
}
return {};
}
PlaymatInfo resolvePlaymatForDeck(const DeckList &deck,
const QList<PlaymatInfo> &fallbackList,
PlaymatMode mode,
PlaymatFallbackMode fallbackBehavior,
int rotationIndex)
{
switch (mode) {
case PlaymatModeOverrideDeck: {
const DeckList emptyDeck;
return resolveEffectivePlaymat(emptyDeck, {}, fallbackList, fallbackBehavior, rotationIndex);
}
case PlaymatModeFallback:
return resolveEffectivePlaymat(deck, {}, fallbackList, fallbackBehavior, rotationIndex);
case PlaymatModeDeckOnly:
return deck.getPlaymat();
}
return {};
}

View file

@ -0,0 +1,47 @@
#ifndef COCKATRICE_PLAYMAT_RESOLVER_H
#define COCKATRICE_PLAYMAT_RESOLVER_H
#include "deck_list.h"
#include <libcockatrice/interfaces/interface_interface_settings_provider.h>
/**
* @brief Resolves the effective playmat for a deck per the resolution chain:
* force override > deck-configured playmat > fallback list > none.
*
* @param deck The deck to resolve a playmat for.
* @param force An optional user-level override; wins over everything. Pass an
* empty @ref PlaymatInfo::card to skip it.
* @param fallbackList User-level fallback playmats, consulted only when the
* deck has no configured playmat.
* @param fallbackMode How @p fallbackList is consulted (ignored when empty).
* @param rotationIndex In/out cursor for @c PlaymatFallbackModeRoundRobin;
* advanced once per call. Unused for the other modes.
* @return The effective playmat; an empty @ref PlaymatInfo::card when
* nothing in the chain resolves.
*/
PlaymatInfo resolveEffectivePlaymat(const DeckList &deck,
const PlaymatInfo &force,
const QList<PlaymatInfo> &fallbackList,
PlaymatFallbackMode fallbackMode,
int rotationIndex);
/**
* @brief Resolves the playmat to display for a deck according to the user's
* collection mode (@ref PlaymatMode), combining the deck with the
* given fallback list.
*
* @param deck The deck to resolve a playmat for.
* @param fallbackList User-level fallback playmats.
* @param mode How the collection interacts with the deck-configured playmat.
* @param fallbackBehavior How @p fallbackList is picked from.
* @param rotationIndex Cursor for @c PlaymatFallbackModeRoundRobin.
* @return The effective playmat; an empty @ref PlaymatInfo::card when nothing resolves.
*/
PlaymatInfo resolvePlaymatForDeck(const DeckList &deck,
const QList<PlaymatInfo> &fallbackList,
PlaymatMode mode,
PlaymatFallbackMode fallbackBehavior,
int rotationIndex);
#endif // COCKATRICE_PLAYMAT_RESOLVER_H