Change things about resolution, address comments.

Took 45 minutes

Took 12 minutes
This commit is contained in:
Lukas Brübach 2026-08-17 21:37:50 +02:00
parent cb8fe8305e
commit 80d44da729
36 changed files with 393 additions and 413 deletions

View file

@ -244,8 +244,6 @@ set(cockatrice_SOURCES
src/interface/widgets/playmat/playmat_collection_dialog.h
src/interface/widgets/playmat/playmat_preview_widget.cpp
src/interface/widgets/playmat/playmat_settings_dialog.cpp
src/interface/widgets/playmat/playmat_settings_utils.cpp
src/interface/widgets/playmat/playmat_settings_utils.h
src/interface/widgets/quick_settings/settings_button_widget.cpp
src/interface/widgets/quick_settings/settings_popup_widget.cpp
src/interface/widgets/replay/replay_manager.cpp

View file

@ -286,7 +286,7 @@ void GameEventHandler::eventGameStateChanged(const Event_GameStateChanged &event
}
player->processPlayerInfo(playerInfo);
// Extract playmat from player properties for opponent display
if (prop.has_playmat_card_name()) {
if (prop.has_playmat_params()) {
player->setPlaymatFromProperties(prop);
}
if (player->getPlayerInfo()->getLocal()) {
@ -356,7 +356,7 @@ void GameEventHandler::eventPlayerPropertiesChanged(const Event_PlayerProperties
emit playerPropertiesChanged(prop, eventPlayerId);
// Update playmat from player properties
if (prop.has_playmat_card_name()) {
if (prop.has_playmat_params()) {
player->setPlaymatFromProperties(prop);
}

View file

@ -252,12 +252,11 @@ void PlayerLogic::setDeck(const DeckList &_deck)
void PlayerLogic::setPlaymatFromProperties(const ServerInfo_PlayerProperties &props)
{
if (props.has_playmat_card_name() && !props.playmat_card_name().empty()) {
remotePlaymatCard = {QString::fromStdString(props.playmat_card_name()),
QString::fromStdString(props.playmat_card_provider_id())};
remotePlaymatParams = {
qBound(0.0, props.playmat_margin_pct_l(), 0.95), qBound(0.0, props.playmat_margin_pct_r(), 0.95),
qBound(0.0, props.playmat_vertical_offset(), 1.0), qBound(0.1, props.playmat_zoom(), 4.0)};
if (props.has_playmat_params() && !props.playmat_params().card_name().empty()) {
const auto &pp = props.playmat_params();
remotePlaymatCard = {QString::fromStdString(pp.card_name()), QString::fromStdString(pp.card_provider_id())};
remotePlaymatParams = {qBound(0.0, pp.margin_pct_l(), 0.95), qBound(0.0, pp.margin_pct_r(), 0.95),
qBound(0.0, pp.vertical_offset(), 1.0), qBound(0.1, pp.zoom(), 4.0)};
hasRemotePlaymat = true;
} else {
remotePlaymatCard = CardRef{};

View file

@ -8,19 +8,21 @@
#include "../../interface/widgets/dialogs/dlg_load_deck_from_clipboard.h"
#include "../../interface/widgets/dialogs/dlg_load_deck_from_website.h"
#include "../../interface/widgets/dialogs/dlg_load_remote_deck.h"
#include "../../interface/widgets/playmat/playmat_settings_utils.h"
#include "../../interface/widgets/tabs/tab_game.h"
#include "deck_view.h"
#include <QMessageBox>
#include <libcockatrice/card/database/card_database.h>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/deck_list/playmat_resolver.h>
#include <libcockatrice/protocol/pb/command_deck_select.pb.h>
#include <libcockatrice/protocol/pb/command_ready_start.pb.h>
#include <libcockatrice/protocol/pb/command_set_playmat.pb.h>
#include <libcockatrice/protocol/pb/command_set_sideboard_lock.pb.h>
#include <libcockatrice/protocol/pb/command_set_sideboard_plan.pb.h>
#include <libcockatrice/protocol/pb/response_deck_download.pb.h>
#include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/settings/interface_settings.h>
#include <libcockatrice/settings/visual_deck_storage_settings.h>
#include <libcockatrice/utility/string_limits.h>
@ -101,6 +103,9 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent)
connect(&SettingsCache::instance().visualDeckStorage(), &VisualDeckStorageSettings::visualDeckStorageInGameChanged,
this, &DeckViewContainer::setVisualDeckStorageExists);
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::playmatSettingsChanged, this,
&DeckViewContainer::onPlaymatSettingsChanged);
switchToDeckSelectView();
}
@ -278,11 +283,9 @@ void DeckViewContainer::loadDeckFromFile(const QString &filePath)
void DeckViewContainer::loadDeckFromDeckList(const DeckList &deck)
{
// Bake the user-level playmat resolution (override > deck > fallback list)
// into the uploaded deck so the server, local and remote players, and
// replays all derive the same effective playmat.
const DeckList effectiveDeck = applyUserPlaymatSettings(deck, SettingsCache::instance().userInterface());
QString deckString = effectiveDeck.writeToString_Native();
currentDeck = deck;
QString deckString = deck.writeToString_Native();
if (deckString.length() > MAX_FILE_LENGTH) {
QMessageBox::critical(this, tr("Error"), tr("Deck is greater than maximum file size."));
@ -294,6 +297,65 @@ void DeckViewContainer::loadDeckFromDeckList(const DeckList &deck)
PendingCommand *pend = parentGame->getGame()->getGameEventHandler()->prepareGameCommand(cmd);
connect(pend, &PendingCommand::finished, this, &DeckViewContainer::deckSelectFinished);
parentGame->getGame()->getGameEventHandler()->sendGameCommand(pend, playerId);
resolveAndSendPlaymat();
}
void DeckViewContainer::resolveAndSendPlaymat()
{
if (currentDeck.getCardRefList().isEmpty() && currentDeck.getPlaymat().card.isEmpty()) {
return;
}
const auto &settings = SettingsCache::instance().userInterface();
const auto fallbackBehavior = static_cast<PlaymatFallbackMode>(settings.getPlaymatFallbackBehavior());
PlaymatResolution resolved;
QList<PlaymatResolution> fallbackList = settings.getPlaymatFallbackList();
// In random mode with 2+ entries, remove the last-resolved mat to avoid repeats.
if (fallbackBehavior == PlaymatFallbackMode::Random && fallbackList.size() > 1) {
fallbackList.removeAll(lastResolvedPlaymat);
}
switch (settings.getPlaymatMode()) {
case 0: { // Override deck playmat — always use collection
DeckList emptyDeck;
resolved = resolveEffectivePlaymat(emptyDeck, {}, fallbackList, fallbackBehavior, playmatRotationIndex);
break;
}
case 1: // Fallback if deck has none — deck > collection > none
resolved = resolveEffectivePlaymat(currentDeck, {}, fallbackList, fallbackBehavior, playmatRotationIndex);
break;
case 2: // Deck only, ignore collection
resolved = currentDeck.getPlaymat();
break;
default:
break;
}
lastResolvedPlaymat = resolved;
Command_SetPlaymat playmatCmd;
auto *pp = playmatCmd.mutable_playmat_params();
pp->set_card_name(resolved.card.name.toStdString());
pp->set_card_provider_id(resolved.card.providerId.toStdString());
pp->set_margin_pct_l(resolved.params.marginPctL);
pp->set_margin_pct_r(resolved.params.marginPctR);
pp->set_vertical_offset(resolved.params.verticalOffset);
pp->set_zoom(resolved.params.zoom);
PendingCommand *playmatPend = parentGame->getGame()->getGameEventHandler()->prepareGameCommand(playmatCmd);
parentGame->getGame()->getGameEventHandler()->sendGameCommand(playmatPend, playerId);
}
void DeckViewContainer::onPlaymatSettingsChanged()
{
resolveAndSendPlaymat();
}
void DeckViewContainer::advancePlaymatRotation()
{
playmatRotationIndex++;
}
void DeckViewContainer::loadRemoteDeck()
@ -384,6 +446,10 @@ void DeckViewContainer::sideboardPlanChanged()
*/
void DeckViewContainer::sendReadyStartCommand(bool ready)
{
if (ready) {
resolveAndSendPlaymat();
}
Command_ReadyStart cmd;
cmd.set_ready(ready);
parentGame->getGame()->getGameEventHandler()->sendGameCommand(cmd, playerId);
@ -421,6 +487,7 @@ void DeckViewContainer::setSideboardLocked(bool locked)
void DeckViewContainer::setDeck(const DeckList &deck)
{
currentDeck = deck;
deckView->setDeck(deck);
switchToDeckLoadedView();
}

View file

@ -57,6 +57,9 @@ private:
VisualDeckStorageWidget *visualDeckStorageWidget;
TabGame *parentGame;
int playerId;
int playmatRotationIndex = 0; ///< Per-match cursor for round-robin playmat mode.
DeckList currentDeck; ///< Cached deck for live settings re-resolution.
PlaymatResolution lastResolvedPlaymat; ///< Tracks last sent playmat to avoid repeats in random mode.
void tryCreateVisualDeckStorageWidget();
void sendReadyStartCommand(bool ready);
@ -75,6 +78,7 @@ private slots:
void sideboardLockButtonClicked();
void updateSideboardLockButtonText();
void refreshShortcuts();
void onPlaymatSettingsChanged();
signals:
void newCardAdded(AbstractCardItem *card);
void notIdle();
@ -87,6 +91,8 @@ public:
void setSideboardLocked(bool locked);
void setDeck(const DeckList &deck);
void setVisualDeckStorageExists(bool exists);
void advancePlaymatRotation();
void resolveAndSendPlaymat();
public slots:
void loadDeckFromFile(const QString &filePath);

View file

@ -379,15 +379,18 @@ void PlayerGraphicsItem::updatePlaymat()
CardRef playmatCard;
PlaymatParams params;
// Check local deck first (for local player)
const DeckList &deck = player->getDeck();
if (!deck.getPlaymatCard().isEmpty()) {
playmatCard = deck.getPlaymatCard();
params = deck.getPlaymatParams();
} else if (visibility == 2 && player->getHasRemotePlaymat()) {
// "Show all" — also show remote playmats from opponents
if (player->getHasRemotePlaymat()) {
// Prefer the server-confirmed playmat (updated by Command_SetPlaymat).
playmatCard = player->getRemotePlaymatCard();
params = player->getRemotePlaymatParams();
} else {
// Fall back to the locally baked-in deck playmat.
const DeckList &deck = player->getDeck();
const PlaymatResolution &deckPlaymat = deck.getPlaymat();
if (!deckPlaymat.card.isEmpty()) {
playmatCard = deckPlaymat.card;
params = deckPlaymat.params;
}
}
if (playmatCard.isEmpty()) {

View file

@ -3,7 +3,6 @@
#include "../../../client/settings/cache_settings.h"
#include "../../../client/settings/shortcuts_settings.h"
#include "../playmat/playmat_settings_dialog.h"
#include "../playmat/playmat_settings_utils.h"
#include "../settings_page/user_interface_settings_page.h"
#include "../tabs/api/commander_spellbook/commander_bracket_widget.h"
#include "deck_list_style_proxy.h"
@ -454,29 +453,25 @@ void DeckEditorDeckDockWidget::writeBannerCard(int index)
void DeckEditorDeckDockWidget::openPlaymatSettings()
{
CardRef currentCard = deckStateManager->getMetadata().playmatCard;
PlaymatParams currentParams = deckStateManager->getMetadata().playmatParams;
PlaymatResolution current = deckStateManager->getMetadata().playmat;
PlaymatSettingsDialog dialog(currentCard, currentParams, this);
PlaymatSettingsDialog dialog(current.card, current.params, this);
if (dialog.exec() == QDialog::Accepted) {
CardRef newCard = dialog.card();
PlaymatParams newParams = dialog.params();
if (newCard.isEmpty()) {
// User clicked "Remove Playmat"
deckStateManager->setPlaymatCard(CardRef{});
deckStateManager->setPlaymatParams(PlaymatParams{});
deckStateManager->setPlaymat(PlaymatResolution{});
} else {
deckStateManager->setPlaymatCard(newCard);
deckStateManager->setPlaymatParams(newParams);
deckStateManager->setPlaymat({newCard, newParams});
}
// "Also set as my default playmat": make this the fixed user-level
// fallback, so decks without a playmat of their own use it.
if (dialog.useAsDefault() && !newCard.isEmpty()) {
auto &interfaceSettings = SettingsCache::instance().userInterface();
interfaceSettings.setPlaymatFallbackList({storedFromResolution({newCard, newParams})});
interfaceSettings.setPlaymatFallbackMode(0); // PlaymatFallbackMode::Fixed
interfaceSettings.setPlaymatFallbackList({{newCard, newParams}});
interfaceSettings.setPlaymatFallbackBehavior(0); // PlaymatFallbackMode::Fixed
}
updatePlaymatLabel();
}
@ -484,7 +479,7 @@ void DeckEditorDeckDockWidget::openPlaymatSettings()
void DeckEditorDeckDockWidget::updatePlaymatLabel()
{
CardRef playmat = deckStateManager->getMetadata().playmatCard;
CardRef playmat = deckStateManager->getMetadata().playmat.card;
if (playmat.isEmpty()) {
playmatSettingsButton->setText(tr("Edit Playmat..."));
} else {

View file

@ -142,28 +142,15 @@ void DeckStateManager::setBannerCard(const CardRef &bannerCard)
doMetadataModified();
}
void DeckStateManager::setPlaymatCard(const CardRef &playmatCard)
void DeckStateManager::setPlaymat(const PlaymatResolution &playmat)
{
CardRef previous = deckList->getPlaymatCard();
if (previous == playmatCard) {
PlaymatResolution previous = deckList->getPlaymat();
if (previous == playmat) {
return;
}
requestHistorySave(tr("Set playmat card to %1 (%2)").arg(playmatCard.name).arg(playmatCard.providerId));
deckList->setPlaymatCard(playmatCard);
doMetadataModified();
}
void DeckStateManager::setPlaymatParams(const PlaymatParams &playmatParams)
{
PlaymatParams previous = deckList->getPlaymatParams();
if (previous == playmatParams) {
return;
}
requestHistorySave(tr("Updated playmat positioning"));
deckList->setPlaymatParams(playmatParams);
requestHistorySave(tr("Set playmat to %1").arg(playmat.card.name));
deckList->setPlaymat(playmat);
doMetadataModified();
}

View file

@ -171,8 +171,7 @@ public:
void setName(const QString &name);
void setComments(const QString &comments);
void setBannerCard(const CardRef &bannerCard);
void setPlaymatCard(const CardRef &playmatCard);
void setPlaymatParams(const PlaymatParams &playmatParams);
void setPlaymat(const PlaymatResolution &playmat);
void setTags(const QStringList &tags);
void setFormat(const QString &format);
///@}

View file

@ -2,7 +2,6 @@
#include "../../../client/settings/cache_settings.h"
#include "playmat_settings_dialog.h"
#include "playmat_settings_utils.h"
#include <QComboBox>
#include <QDialogButtonBox>
@ -24,7 +23,7 @@ void PlaymatCollectionDialog::accept()
{
auto &interfaceSettings = SettingsCache::instance().userInterface();
interfaceSettings.setPlaymatFallbackList(playmats);
interfaceSettings.setPlaymatFallbackMode(modeCombo->currentData().toInt());
interfaceSettings.setPlaymatFallbackBehavior(modeCombo->currentData().toInt());
QDialog::accept();
}
@ -39,8 +38,8 @@ void PlaymatCollectionDialog::setupUi()
playmats = interfaceSettings.getPlaymatFallbackList();
playmatList = new QListWidget;
for (const StoredPlaymat &entry : playmats) {
playmatList->addItem(entry.name);
for (const PlaymatResolution &entry : playmats) {
playmatList->addItem(entry.card.name);
}
connect(playmatList, &QListWidget::itemSelectionChanged, this, &PlaymatCollectionDialog::selectionChanged);
connect(playmatList, &QListWidget::itemDoubleClicked, this, [this](QListWidgetItem *) { editPlaymat(); });
@ -73,7 +72,7 @@ void PlaymatCollectionDialog::setupUi()
modeCombo->addItem(QString(), 0);
modeCombo->addItem(QString(), 1);
modeCombo->addItem(QString(), 2);
const int modeIndex = modeCombo->findData(interfaceSettings.getPlaymatFallbackMode());
const int modeIndex = modeCombo->findData(interfaceSettings.getPlaymatFallbackBehavior());
if (modeIndex >= 0) {
modeCombo->setCurrentIndex(modeIndex);
}
@ -106,20 +105,16 @@ void PlaymatCollectionDialog::selectionChanged()
moveDownButton->setEnabled(hasSelection && playmatList->currentRow() < playmatList->count() - 1);
}
void PlaymatCollectionDialog::appendEntry(const StoredPlaymat &entry)
{
playmats.append(entry);
playmatList->addItem(entry.name);
playmatList->setCurrentRow(playmatList->count() - 1);
}
void PlaymatCollectionDialog::addPlaymat()
{
PlaymatSettingsDialog dialog(CardRef{}, PlaymatParams{}, this);
if (dialog.exec() == QDialog::Accepted) {
const CardRef card = dialog.card();
if (!card.isEmpty()) {
appendEntry(storedFromResolution({card, dialog.params()}));
PlaymatResolution res = {card, dialog.params()};
playmats.append(res);
playmatList->addItem(res.card.name);
playmatList->setCurrentRow(playmatList->count() - 1);
}
}
}
@ -131,14 +126,14 @@ void PlaymatCollectionDialog::editPlaymat()
return;
}
const PlaymatResolution current = resolutionFromStoredPlaymat(playmats.at(row));
const PlaymatResolution &current = playmats.at(row);
PlaymatSettingsDialog dialog(current.card, current.params, this);
if (dialog.exec() == QDialog::Accepted) {
const CardRef card = dialog.card();
if (card.isEmpty()) {
return; // Removal is handled by the Remove button
}
playmats[row] = storedFromResolution({card, dialog.params()});
playmats[row] = {card, dialog.params()};
playmatList->item(row)->setText(card.name);
}
}
@ -186,8 +181,8 @@ void PlaymatCollectionDialog::retranslateUi()
removeButton->setText(tr("Remove"));
moveUpButton->setText(tr("Move Up"));
moveDownButton->setText(tr("Move Down"));
modeLabel->setText(tr("When a deck has no playmat:"));
modeCombo->setItemText(0, tr("Always use the first playmat"));
modeCombo->setItemText(1, tr("Cycle through playmats (one per game)"));
modeCombo->setItemText(2, tr("Pick a random playmat per game"));
modeLabel->setText(tr("List mode:"));
modeCombo->setItemText(0, tr("Fixed (always the first entry)"));
modeCombo->setItemText(1, tr("Round-robin (cycle through entries)"));
modeCombo->setItemText(2, tr("Random (pick one per game)"));
}

View file

@ -2,7 +2,7 @@
#define COCKATRICE_PLAYMAT_COLLECTION_DIALOG_H
#include <QDialog>
#include <libcockatrice/interfaces/interface_interface_settings_provider.h>
#include <libcockatrice/utility/playmat_params.h>
class QComboBox;
class QLabel;
@ -39,9 +39,8 @@ private:
void setupUi();
void retranslateUi();
int currentRow() const;
void appendEntry(const StoredPlaymat &entry);
QList<StoredPlaymat> playmats; ///< Working copy edited by the dialog.
QList<PlaymatResolution> playmats; ///< Working copy edited by the dialog.
QListWidget *playmatList;
QComboBox *modeCombo;
QLabel *modeLabel;

View file

@ -1,48 +0,0 @@
#include "playmat_settings_utils.h"
#include <libcockatrice/interfaces/interface_interface_settings_provider.h>
#include <libcockatrice/settings/interface_settings.h>
PlaymatResolution resolutionFromStoredPlaymat(const StoredPlaymat &stored)
{
PlaymatResolution resolution;
resolution.card = {stored.name, stored.providerId};
resolution.params = {qBound(0.0, stored.marginPctL, 0.95), qBound(0.0, stored.marginPctR, 0.95),
qBound(0.0, stored.verticalOffset, 1.0), qBound(0.1, stored.zoom, 4.0)};
return resolution;
}
StoredPlaymat storedFromResolution(const PlaymatResolution &resolution)
{
StoredPlaymat stored;
stored.name = resolution.card.name;
stored.providerId = resolution.card.providerId;
stored.marginPctL = resolution.params.marginPctL;
stored.marginPctR = resolution.params.marginPctR;
stored.verticalOffset = resolution.params.verticalOffset;
stored.zoom = resolution.params.zoom;
return stored;
}
DeckList applyUserPlaymatSettings(const DeckList &deck, const InterfaceSettings &settings)
{
// Per-session cursor for the round-robin mode; advancing per resolution
// (per deck upload, i.e. roughly per game) cycles deterministically.
static int rotationIndex = 0;
QList<PlaymatResolution> fallbackList;
const QList<StoredPlaymat> storedList = settings.getPlaymatFallbackList();
fallbackList.reserve(storedList.size());
for (const StoredPlaymat &stored : storedList) {
fallbackList.append(resolutionFromStoredPlaymat(stored));
}
const PlaymatResolution resolved =
resolveEffectivePlaymat(deck, resolutionFromStoredPlaymat(settings.getPlaymatOverride()), fallbackList,
static_cast<PlaymatFallbackMode>(settings.getPlaymatFallbackMode()), rotationIndex);
DeckList effectiveDeck = deck;
effectiveDeck.setPlaymatCard(resolved.card);
effectiveDeck.setPlaymatParams(resolved.params);
return effectiveDeck;
}

View file

@ -1,32 +0,0 @@
#ifndef COCKATRICE_PLAYMAT_SETTINGS_UTILS_H
#define COCKATRICE_PLAYMAT_SETTINGS_UTILS_H
#include <libcockatrice/deck_list/deck_list.h>
#include <libcockatrice/deck_list/playmat_resolver.h>
#include <libcockatrice/interfaces/interface_interface_settings_provider.h>
class InterfaceSettings;
/**
* @brief Converts a stored (settings-level) playmat entry into resolver types,
* clamping the positioning parameters to their valid ranges.
*/
PlaymatResolution resolutionFromStoredPlaymat(const StoredPlaymat &stored);
/**
* @brief Converts a resolved playmat into a stored (settings-level) entry.
*/
StoredPlaymat storedFromResolution(const PlaymatResolution &resolution);
/**
* @brief Applies the user-level playmat settings (override, fallback list and
* mode) to a copy of the deck, returning the deck with the effective
* playmat baked into its metadata.
*
* The resolution chain is: override > deck-configured playmat > fallback list.
* The deck passed in is never modified; the returned copy is intended for
* upload to the server so local and remote players see the same effective mat.
*/
DeckList applyUserPlaymatSettings(const DeckList &deck, const InterfaceSettings &settings);
#endif // COCKATRICE_PLAYMAT_SETTINGS_UTILS_H

View file

@ -9,7 +9,6 @@
#include "../interface/widgets/general/background_sources.h"
#include "../playmat/playmat_collection_dialog.h"
#include "../playmat/playmat_settings_dialog.h"
#include "../playmat/playmat_settings_utils.h"
#include <QApplication>
#include <QColorDialog>
@ -342,22 +341,28 @@ AppearanceSettingsPage::AppearanceSettingsPage()
});
playmatVisibilityLabel.setBuddy(&playmatVisibilityCombo);
// User-level playmat settings: override (force all decks) and fallback collection.
connect(&playmatOverrideEditButton, &QPushButton::clicked, this,
&AppearanceSettingsPage::openOverridePlaymatSettings);
connect(&playmatOverrideClearButton, &QPushButton::clicked, this, &AppearanceSettingsPage::clearOverridePlaymat);
// Playmat mode: Override / Fallback / Deck-only
playmatModeCombo.addItem(tr("Override deck playmat"), 0);
playmatModeCombo.addItem(tr("Fallback if deck has none"), 1);
playmatModeCombo.addItem(tr("Deck only, ignore collection"), 2);
int modeIdx = playmatModeCombo.findData(settings.userInterface().getPlaymatMode());
if (modeIdx >= 0) {
playmatModeCombo.setCurrentIndex(modeIdx);
}
connect(&playmatModeCombo, qOverload<int>(&QComboBox::currentIndexChanged), this, [this](int index) {
SettingsCache::instance().userInterface().setPlaymatMode(playmatModeCombo.itemData(index).toInt());
});
playmatModeLabel.setBuddy(&playmatModeCombo);
// User-level playmat settings: fallback collection.
connect(&playmatDefaultEditButton, &QPushButton::clicked, this,
&AppearanceSettingsPage::openPlaymatCollectionDialog);
auto *overrideButtons = new QHBoxLayout;
overrideButtons->addWidget(&playmatOverrideEditButton);
overrideButtons->addWidget(&playmatOverrideClearButton);
auto *playmatGrid = new QGridLayout;
playmatGrid->addWidget(&playmatVisibilityLabel, 0, 0, 1, 1);
playmatGrid->addWidget(&playmatVisibilityCombo, 0, 1, 1, 1);
playmatGrid->addWidget(&playmatOverrideLabel, 1, 0, 1, 1);
playmatGrid->addLayout(overrideButtons, 1, 1, 1, 1);
playmatGrid->addWidget(&playmatModeLabel, 1, 0, 1, 1);
playmatGrid->addWidget(&playmatModeCombo, 1, 1, 1, 1);
playmatGrid->addWidget(&playmatDefaultLabel, 2, 0, 1, 1);
playmatGrid->addWidget(&playmatDefaultEditButton, 2, 1, 1, 1);
@ -471,40 +476,10 @@ void AppearanceSettingsPage::cardViewExpandedRowsMaxChanged(int value)
}
}
void AppearanceSettingsPage::openOverridePlaymatSettings()
{
auto &interfaceSettings = SettingsCache::instance().userInterface();
const PlaymatResolution current = resolutionFromStoredPlaymat(interfaceSettings.getPlaymatOverride());
PlaymatSettingsDialog dialog(current.card, current.params, this);
if (dialog.exec() == QDialog::Accepted) {
const CardRef card = dialog.card();
if (card.isEmpty()) {
interfaceSettings.setPlaymatOverride(StoredPlaymat{});
} else {
interfaceSettings.setPlaymatOverride(storedFromResolution({card, dialog.params()}));
}
updatePlaymatSummary();
}
}
void AppearanceSettingsPage::clearOverridePlaymat()
{
SettingsCache::instance().userInterface().setPlaymatOverride(StoredPlaymat{});
updatePlaymatSummary();
}
void AppearanceSettingsPage::openPlaymatCollectionDialog()
{
PlaymatCollectionDialog dialog(this);
dialog.exec();
updatePlaymatSummary();
}
void AppearanceSettingsPage::updatePlaymatSummary()
{
const StoredPlaymat overridePlaymat = SettingsCache::instance().userInterface().getPlaymatOverride();
playmatOverrideEditButton.setText(overridePlaymat.isEmpty() ? tr("Set...")
: tr("Change (%1)").arg(overridePlaymat.name));
}
void AppearanceSettingsPage::retranslateUi()
@ -567,9 +542,7 @@ void AppearanceSettingsPage::retranslateUi()
minPlayersForMultiColumnLayoutLabel.setText(tr("Minimum player count for multi-column layout:"));
playmatGroupBox->setTitle(tr("Playmat settings"));
playmatVisibilityLabel.setText(tr("Playmat visibility:"));
playmatOverrideLabel.setText(tr("Force playmat for all decks:"));
playmatOverrideClearButton.setText(tr("Clear"));
playmatDefaultLabel.setText(tr("Default playmats (when a deck has none):"));
playmatModeLabel.setText(tr("Default collection behavior:"));
playmatDefaultLabel.setText(tr("Default playmat collection:"));
playmatDefaultEditButton.setText(tr("Edit..."));
updatePlaymatSummary();
}

View file

@ -24,10 +24,7 @@ private slots:
void cardViewInitialRowsMaxChanged(int value);
void cardViewExpandedRowsMaxChanged(int value);
void openOverridePlaymatSettings();
void clearOverridePlaymat();
void openPlaymatCollectionDialog();
void updatePlaymatSummary();
private:
QLabel themeLabel;
@ -65,9 +62,8 @@ private:
QCheckBox invertVerticalCoordinateCheckBox;
QLabel playmatVisibilityLabel;
QComboBox playmatVisibilityCombo;
QLabel playmatOverrideLabel;
QPushButton playmatOverrideEditButton;
QPushButton playmatOverrideClearButton;
QLabel playmatModeLabel;
QComboBox playmatModeCombo;
QLabel playmatDefaultLabel;
QPushButton playmatDefaultEditButton;
QGroupBox *themeGroupBox;

View file

@ -911,6 +911,7 @@ void TabGame::stopGame()
QMapIterator<int, TabbedDeckViewContainer *> i(deckViewContainers);
while (i.hasNext()) {
i.next();
i.value()->playerDeckView->advancePlaymatRotation();
i.value()->show();
}

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() && playmatCard.isEmpty();
return name.isEmpty() && comments.isEmpty() && bannerCard.isEmpty() && tags.isEmpty() && playmat.card.isEmpty();
}
DeckList::DeckList()
@ -82,28 +82,28 @@ bool DeckList::readElement(QXmlStreamReader *xml)
QString vOffStr = xml->attributes().value("verticalOffset").toString();
QString zoomStr = xml->attributes().value("zoom").toString();
QString cardName = xml->readElementText();
metadata.playmatCard = {cardName, providerId};
PlaymatParams params;
PlaymatResolution 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).
params.marginPctL = qBound(0.0, marginLStr.toDouble(&ok), 0.95);
playmat.params.marginPctL = qBound(0.0, marginLStr.toDouble(&ok), 0.95);
if (!ok) {
params.marginPctL = 0.07;
playmat.params.marginPctL = 0.07;
}
params.marginPctR = qBound(0.0, marginRStr.toDouble(&ok), 0.95);
playmat.params.marginPctR = qBound(0.0, marginRStr.toDouble(&ok), 0.95);
if (!ok) {
params.marginPctR = 0.07;
playmat.params.marginPctR = 0.07;
}
params.verticalOffset = qBound(0.0, vOffStr.toDouble(&ok), 1.0);
playmat.params.verticalOffset = qBound(0.0, vOffStr.toDouble(&ok), 1.0);
if (!ok) {
params.verticalOffset = 0.33;
playmat.params.verticalOffset = 0.33;
}
params.zoom = qBound(0.1, zoomStr.toDouble(&ok), 4.0);
playmat.params.zoom = qBound(0.1, zoomStr.toDouble(&ok), 4.0);
if (!ok) {
params.zoom = 1.0;
playmat.params.zoom = 1.0;
}
metadata.playmatParams = params;
metadata.playmat = playmat;
} else if (childName == "tags") {
metadata.tags.clear(); // Clear existing tags
while (xml->readNextStartElement()) {
@ -134,14 +134,14 @@ static void writeMetadata(QXmlStreamWriter *xml, const DeckList::Metadata &metad
xml->writeAttribute("providerId", metadata.bannerCard.providerId);
xml->writeCharacters(metadata.bannerCard.name);
xml->writeEndElement();
if (!metadata.playmatCard.isEmpty()) {
if (!metadata.playmat.card.isEmpty()) {
xml->writeStartElement("playmatCard");
xml->writeAttribute("providerId", metadata.playmatCard.providerId);
xml->writeAttribute("marginPctL", QString::number(metadata.playmatParams.marginPctL, 'f', 4));
xml->writeAttribute("marginPctR", QString::number(metadata.playmatParams.marginPctR, 'f', 4));
xml->writeAttribute("verticalOffset", QString::number(metadata.playmatParams.verticalOffset, 'f', 4));
xml->writeAttribute("zoom", QString::number(metadata.playmatParams.zoom, 'f', 4));
xml->writeCharacters(metadata.playmatCard.name);
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);

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;
@ -27,34 +27,6 @@ class QIODevice;
class QTextStream;
class InnerDecklistNode;
/**
* @struct PlaymatParams
* @ingroup Decks
* @brief Positioning parameters for a playmat card image.
*
* Controls how the cropped card art is positioned within the
* combined table+stack play area. The coordinate system is
* relative to the cropped art source image.
*/
struct PlaymatParams
{
double marginPctL = 0.07; ///< Left margin as fraction of card width (0.0–0.95).
double marginPctR = 0.07; ///< Right margin as fraction of card width (0.0–0.95).
double verticalOffset = 0.33; ///< Vertical position within card (0.0=top, 1.0=bottom).
double zoom = 1.0; ///< Scale factor (0.1–4.0).
bool operator==(const PlaymatParams &other) const
{
return qFuzzyCompare(marginPctL, other.marginPctL) && qFuzzyCompare(marginPctR, other.marginPctR) &&
qFuzzyCompare(verticalOffset, other.verticalOffset) && qFuzzyCompare(zoom, other.zoom);
}
bool operator!=(const PlaymatParams &other) const
{
return !(*this == other);
}
};
/**
* @class DeckList
* @ingroup Decks
@ -98,8 +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.
CardRef playmatCard; ///< Optional card used as playmat background for table+stack zones.
PlaymatParams playmatParams; ///< Positioning parameters for the playmat card image.
PlaymatResolution playmat; ///< Optional playmat background for table+stack zones.
/**
* @brief Checks if all values (except for lastLoadedTimestamp) in the metadata is empty.
@ -145,13 +116,9 @@ public:
{
metadata.bannerCard = _bannerCard;
}
void setPlaymatCard(const CardRef &_playmatCard = {})
void setPlaymat(const PlaymatResolution &_playmat = {})
{
metadata.playmatCard = _playmatCard;
}
void setPlaymatParams(const PlaymatParams &_playmatParams = {})
{
metadata.playmatParams = _playmatParams;
metadata.playmat = _playmat;
}
void setLastLoadedTimestamp(const QString &_lastLoadedTimestamp = QString())
{
@ -208,13 +175,9 @@ public:
{
return metadata.bannerCard;
}
CardRef getPlaymatCard() const
PlaymatResolution getPlaymat() const
{
return metadata.playmatCard;
}
PlaymatParams getPlaymatParams() const
{
return metadata.playmatParams;
return metadata.playmat;
}
QString getLastLoadedTimestamp() const
{

View file

@ -6,14 +6,15 @@ PlaymatResolution resolveEffectivePlaymat(const DeckList &deck,
const PlaymatResolution &force,
const QList<PlaymatResolution> &fallbackList,
PlaymatFallbackMode fallbackMode,
int &rotationIndex)
int rotationIndex)
{
if (!force.card.isEmpty()) {
return force;
}
if (!deck.getPlaymatCard().isEmpty()) {
return {deck.getPlaymatCard(), deck.getPlaymatParams()};
const PlaymatResolution &deckPlaymat = deck.getPlaymat();
if (!deckPlaymat.card.isEmpty()) {
return deckPlaymat;
}
if (fallbackList.isEmpty()) {
@ -24,7 +25,7 @@ PlaymatResolution resolveEffectivePlaymat(const DeckList &deck,
case PlaymatFallbackMode::Fixed:
return fallbackList.first();
case PlaymatFallbackMode::RoundRobin:
return fallbackList.at(rotationIndex++ % fallbackList.size());
return fallbackList.at(rotationIndex % fallbackList.size());
case PlaymatFallbackMode::Random:
return fallbackList.at(QRandomGenerator::global()->bounded(fallbackList.size()));
}

View file

@ -14,22 +14,6 @@ enum class PlaymatFallbackMode
Random = 2 ///< Pick a random entry per resolution.
};
/**
* @struct PlaymatResolution
* @ingroup Decks
* @brief A resolved playmat (card + positioning parameters).
*/
struct PlaymatResolution
{
CardRef card; ///< The card whose art is used as playmat.
PlaymatParams params; ///< Positioning parameters for the playmat card image.
bool operator==(const PlaymatResolution &other) const
{
return card == other.card && params == other.params;
}
};
/**
* @brief Resolves the effective playmat for a deck per the resolution chain:
* force override > deck-configured playmat > fallback list > none.
@ -49,6 +33,6 @@ PlaymatResolution resolveEffectivePlaymat(const DeckList &deck,
const PlaymatResolution &force,
const QList<PlaymatResolution> &fallbackList,
PlaymatFallbackMode fallbackMode,
int &rotationIndex);
int rotationIndex);
#endif // COCKATRICE_PLAYMAT_RESOLVER_H

View file

@ -30,4 +30,4 @@ add_library(libcockatrice_interfaces STATIC ${MOC_SOURCES})
target_include_directories(libcockatrice_interfaces PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(libcockatrice_interfaces PUBLIC ${QT_CORE_MODULE})
target_link_libraries(libcockatrice_interfaces PUBLIC libcockatrice_utility ${QT_CORE_MODULE})

View file

@ -4,26 +4,7 @@
#include <QList>
#include <QString>
#include <QStringList>
/**
* @struct StoredPlaymat
* @brief A user-level playmat entry as stored in the settings, independent of
* any deck. Mirrors a deck-configured playmat (card + parameters).
*/
struct StoredPlaymat
{
QString name; ///< Card name whose art is used as playmat.
QString providerId; ///< Printing provider id (uuid); may be empty.
double marginPctL = 0.07; ///< Left margin as fraction of card width (0.0-0.95).
double marginPctR = 0.07; ///< Right margin as fraction of card width (0.0-0.95).
double verticalOffset = 0.33; ///< Vertical position within card (0.0=top, 1.0=bottom).
double zoom = 1.0; ///< Scale factor (0.1-4.0).
bool isEmpty() const
{
return name.isEmpty();
}
};
#include <libcockatrice/utility/playmat_params.h>
class IInterfaceSettingsProvider
{
@ -66,14 +47,16 @@ public:
[[nodiscard]] virtual QStringList getUserListExpandedSections() const = 0;
[[nodiscard]] virtual int getPlaymatVisibility() const = 0;
/** @brief User-level fallback playmats, used when a deck has none configured. */
[[nodiscard]] virtual QList<StoredPlaymat> getPlaymatFallbackList() const = 0;
/** @brief User-level playmat collection. Used either as a forced playmat
* (mode == Always) or as a fallback when a deck has none (mode == Fallback). */
[[nodiscard]] virtual QList<PlaymatResolution> getPlaymatFallbackList() const = 0;
/** @brief User-level override playmat applied to all decks, or empty to disable. */
[[nodiscard]] virtual StoredPlaymat getPlaymatOverride() const = 0;
/** @brief How the fallback list is applied: Always (0), Fallback (1), Never (2). */
[[nodiscard]] virtual int getPlaymatMode() const = 0;
/** @brief How the fallback list is consulted (0 Fixed, 1 Round-robin, 2 Random). */
[[nodiscard]] virtual int getPlaymatFallbackMode() const = 0;
/** @brief How the fallback list is picked from when mode is Fallback
* (0 Fixed, 1 Round-robin, 2 Random). */
[[nodiscard]] virtual int getPlaymatFallbackBehavior() const = 0;
};
#endif // COCKATRICE_INTERFACE_INTERFACE_SETTINGS_PROVIDER_H

View file

@ -37,6 +37,7 @@
#include <libcockatrice/protocol/pb/command_set_card_attr.pb.h>
#include <libcockatrice/protocol/pb/command_set_card_counter.pb.h>
#include <libcockatrice/protocol/pb/command_set_counter.pb.h>
#include <libcockatrice/protocol/pb/command_set_playmat.pb.h>
#include <libcockatrice/protocol/pb/command_set_sideboard_lock.pb.h>
#include <libcockatrice/protocol/pb/command_set_sideboard_plan.pb.h>
#include <libcockatrice/protocol/pb/command_shuffle.pb.h>
@ -144,6 +145,13 @@ Response::ResponseCode Server_AbstractParticipant::cmdSetSideboardLock(const Com
return Response::RespFunctionNotAllowed;
}
Response::ResponseCode Server_AbstractParticipant::cmdSetPlaymat(const Command_SetPlaymat & /*cmd*/,
ResponseContainer & /*rc*/,
GameEventStorage & /*ges*/)
{
return Response::RespFunctionNotAllowed;
}
Response::ResponseCode Server_AbstractParticipant::cmdConcede(const Command_Concede & /*cmd*/,
ResponseContainer & /*rc*/,
GameEventStorage & /*ges*/)
@ -525,6 +533,9 @@ Server_AbstractParticipant::processGameCommand(const GameCommand &command, Respo
case GameCommand::REVERSE_TURN:
return cmdReverseTurn(command.GetExtension(Command_ReverseTurn::ext), rc, ges);
break;
case GameCommand::SET_PLAYMAT:
return cmdSetPlaymat(command.GetExtension(Command_SetPlaymat::ext), rc, ges);
break;
default:
return Response::RespInvalidCommand;
}

View file

@ -52,6 +52,7 @@ class Command_SetSideboardPlan;
class Command_DeckSelect;
class Command_SetSideboardLock;
class Command_ChangeZoneProperties;
class Command_SetPlaymat;
class Server_AbstractParticipant : public Server_ArrowTarget, public ServerInfo_User_Container
{
@ -124,6 +125,8 @@ public:
cmdSetSideboardPlan(const Command_SetSideboardPlan &cmd, ResponseContainer &rc, GameEventStorage &ges);
virtual Response::ResponseCode
cmdSetSideboardLock(const Command_SetSideboardLock &cmd, ResponseContainer &rc, GameEventStorage &ges);
virtual Response::ResponseCode
cmdSetPlaymat(const Command_SetPlaymat &cmd, ResponseContainer &rc, GameEventStorage &ges);
virtual Response::ResponseCode cmdGameSay(const Command_GameSay &cmd, ResponseContainer &rc, GameEventStorage &ges);
virtual Response::ResponseCode cmdShuffle(const Command_Shuffle &cmd, ResponseContainer &rc, GameEventStorage &ges);
virtual Response::ResponseCode

View file

@ -1653,13 +1653,13 @@ void Server_AbstractPlayer::getPlayerProperties(ServerInfo_PlayerProperties &res
result.set_ready_start(readyStart);
if (deck) {
result.set_deck_hash(deck->getDeckHash().toStdString());
const auto &playmatCard = deck->getPlaymatCard();
const auto &params = deck->getPlaymatParams();
result.set_playmat_card_name(playmatCard.name.toStdString());
result.set_playmat_card_provider_id(playmatCard.providerId.toStdString());
result.set_playmat_margin_pct_l(params.marginPctL);
result.set_playmat_margin_pct_r(params.marginPctR);
result.set_playmat_vertical_offset(params.verticalOffset);
result.set_playmat_zoom(params.zoom);
const auto &playmat = deck->getPlaymat();
auto *playmatParams = result.mutable_playmat_params();
playmatParams->set_card_name(playmat.card.name.toStdString());
playmatParams->set_card_provider_id(playmat.card.providerId.toStdString());
playmatParams->set_margin_pct_l(playmat.params.marginPctL);
playmatParams->set_margin_pct_r(playmat.params.marginPctR);
playmatParams->set_vertical_offset(playmat.params.verticalOffset);
playmatParams->set_zoom(playmat.params.zoom);
}
}

View file

@ -27,6 +27,7 @@
#include <libcockatrice/protocol/pb/command_mulligan.pb.h>
#include <libcockatrice/protocol/pb/command_set_active_phase.pb.h>
#include <libcockatrice/protocol/pb/command_set_counter.pb.h>
#include <libcockatrice/protocol/pb/command_set_playmat.pb.h>
#include <libcockatrice/protocol/pb/command_set_sideboard_lock.pb.h>
#include <libcockatrice/protocol/pb/command_set_sideboard_plan.pb.h>
#include <libcockatrice/protocol/pb/command_shuffle.pb.h>
@ -250,14 +251,14 @@ Server_Player::cmdDeckSelect(const Command_DeckSelect &cmd, ResponseContainer &r
Event_PlayerPropertiesChanged event;
event.mutable_player_properties()->set_sideboard_locked(true);
event.mutable_player_properties()->set_deck_hash(deck->getDeckHash().toStdString());
const auto &playmatCard = deck->getPlaymatCard();
const auto &params = deck->getPlaymatParams();
event.mutable_player_properties()->set_playmat_card_name(playmatCard.name.toStdString());
event.mutable_player_properties()->set_playmat_card_provider_id(playmatCard.providerId.toStdString());
event.mutable_player_properties()->set_playmat_margin_pct_l(params.marginPctL);
event.mutable_player_properties()->set_playmat_margin_pct_r(params.marginPctR);
event.mutable_player_properties()->set_playmat_vertical_offset(params.verticalOffset);
event.mutable_player_properties()->set_playmat_zoom(params.zoom);
const auto &playmat = deck->getPlaymat();
auto *playmatParams = event.mutable_player_properties()->mutable_playmat_params();
playmatParams->set_card_name(playmat.card.name.toStdString());
playmatParams->set_card_provider_id(playmat.card.providerId.toStdString());
playmatParams->set_margin_pct_l(playmat.params.marginPctL);
playmatParams->set_margin_pct_r(playmat.params.marginPctR);
playmatParams->set_vertical_offset(playmat.params.verticalOffset);
playmatParams->set_zoom(playmat.params.zoom);
ges.enqueueGameEvent(event, playerId);
Context_DeckSelect context;
@ -602,6 +603,41 @@ Server_Player::cmdReverseTurn(const Command_ReverseTurn &cmd, ResponseContainer
return Server_AbstractParticipant::cmdReverseTurn(cmd, rc, ges);
}
Response::ResponseCode
Server_Player::cmdSetPlaymat(const Command_SetPlaymat &cmd, ResponseContainer &rc, GameEventStorage &ges)
{
Q_UNUSED(rc);
if (!deck) {
return Response::RespContextError;
}
const auto &pp = cmd.playmat_params();
PlaymatResolution playmat;
playmat.card.name = QString::fromStdString(pp.card_name());
playmat.card.providerId = QString::fromStdString(pp.card_provider_id());
playmat.params.marginPctL = qBound(0.0, pp.margin_pct_l(), 0.95);
playmat.params.marginPctR = qBound(0.0, pp.margin_pct_r(), 0.95);
playmat.params.verticalOffset = qBound(0.0, pp.vertical_offset(), 1.0);
playmat.params.zoom = qBound(0.1, pp.zoom(), 4.0);
deck->setPlaymat(playmat);
Event_PlayerPropertiesChanged event;
auto *props = event.mutable_player_properties();
props->set_sideboard_locked(sideboardLocked);
props->set_deck_hash(deck->getDeckHash().toStdString());
auto *playmatParams = props->mutable_playmat_params();
playmatParams->set_card_name(playmat.card.name.toStdString());
playmatParams->set_card_provider_id(playmat.card.providerId.toStdString());
playmatParams->set_margin_pct_l(playmat.params.marginPctL);
playmatParams->set_margin_pct_r(playmat.params.marginPctR);
playmatParams->set_vertical_offset(playmat.params.verticalOffset);
playmatParams->set_zoom(playmat.params.zoom);
ges.enqueueGameEvent(event, playerId);
return Response::RespOk;
}
void Server_Player::getInfo(ServerInfo_Player *info,
Server_AbstractParticipant *recipient,
bool omniscient,

View file

@ -62,6 +62,8 @@ public:
cmdSetActivePhase(const Command_SetActivePhase &cmd, ResponseContainer &rc, GameEventStorage &ges) override;
Response::ResponseCode
cmdReverseTurn(const Command_ReverseTurn & /*cmd*/, ResponseContainer & /*rc*/, GameEventStorage &ges) override;
Response::ResponseCode
cmdSetPlaymat(const Command_SetPlaymat &cmd, ResponseContainer &rc, GameEventStorage &ges) override;
Response::ResponseCode cmdChangeZoneProperties(const Command_ChangeZoneProperties &cmd,
ResponseContainer &rc,
GameEventStorage &ges) override;

View file

@ -46,6 +46,7 @@ set(PROTO_FILES
command_set_card_attr.proto
command_set_card_counter.proto
command_set_counter.proto
command_set_playmat.proto
command_set_sideboard_lock.proto
command_set_sideboard_plan.proto
command_shuffle.proto

View file

@ -0,0 +1,9 @@
syntax = "proto2";
import "game_commands.proto";
import "serverinfo_playerproperties.proto";
message Command_SetPlaymat {
extend GameCommand {
optional Command_SetPlaymat ext = 1035;
}
optional ServerInfo_PlayerProperties.PlaymatParams playmat_params = 1;
}

View file

@ -175,6 +175,11 @@ message GameCommand {
/// Server: Server_Player::cmdReverseTurn
/// Client: reflected via subsequent turn events
REVERSE_TURN = 1034;
/// Set the player's playmat independently of the deck.
/// Server: Server_Player::cmdSetPlaymat
/// Client: reflected via player properties changed event
SET_PLAYMAT = 1035;
}
extensions 100 to max;

View file

@ -2,6 +2,15 @@ syntax = "proto2";
import "serverinfo_user.proto";
message ServerInfo_PlayerProperties {
message PlaymatParams {
optional string card_name = 1;
optional string card_provider_id = 2;
optional double margin_pct_l = 3 [default = 0.07];
optional double margin_pct_r = 4 [default = 0.07];
optional double vertical_offset = 5 [default = 0.33];
optional double zoom = 6 [default = 1.0];
}
optional sint32 player_id = 1;
optional ServerInfo_User user_info = 2;
optional bool spectator = 3;
@ -11,10 +20,5 @@ message ServerInfo_PlayerProperties {
optional sint32 ping_seconds = 7;
optional bool sideboard_locked = 8;
optional bool judge = 9;
optional string playmat_card_name = 10;
optional string playmat_card_provider_id = 11;
optional double playmat_margin_pct_l = 12 [default = 0.07];
optional double playmat_margin_pct_r = 13 [default = 0.07];
optional double playmat_vertical_offset = 14 [default = 0.33];
optional double playmat_zoom = 15 [default = 1.0];
optional PlaymatParams playmat_params = 10;
}

View file

@ -2,30 +2,31 @@
namespace
{
const QChar PLAYMAT_FIELD_SEP = QChar(0x1F); ///< Separator between StoredPlaymat fields.
const QChar PLAYMAT_FIELD_SEP = QChar(0x1F); ///< Separator between PlaymatResolution fields.
QString encodeStoredPlaymat(const StoredPlaymat &stored)
QString encodePlaymatResolution(const PlaymatResolution &res)
{
return stored.name + PLAYMAT_FIELD_SEP + stored.providerId + PLAYMAT_FIELD_SEP +
QString::number(stored.marginPctL, 'f', 4) + PLAYMAT_FIELD_SEP + QString::number(stored.marginPctR, 'f', 4) +
PLAYMAT_FIELD_SEP + QString::number(stored.verticalOffset, 'f', 4) + PLAYMAT_FIELD_SEP +
QString::number(stored.zoom, 'f', 4);
return res.card.name + PLAYMAT_FIELD_SEP + res.card.providerId + PLAYMAT_FIELD_SEP +
QString::number(res.params.marginPctL, 'f', 4) + PLAYMAT_FIELD_SEP +
QString::number(res.params.marginPctR, 'f', 4) + PLAYMAT_FIELD_SEP +
QString::number(res.params.verticalOffset, 'f', 4) + PLAYMAT_FIELD_SEP +
QString::number(res.params.zoom, 'f', 4);
}
StoredPlaymat decodeStoredPlaymat(const QString &encoded)
PlaymatResolution decodePlaymatResolution(const QString &encoded)
{
const QStringList fields = encoded.split(PLAYMAT_FIELD_SEP);
if (fields.size() != 6) {
return {};
}
StoredPlaymat stored;
stored.name = fields.at(0);
stored.providerId = fields.at(1);
stored.marginPctL = fields.at(2).toDouble();
stored.marginPctR = fields.at(3).toDouble();
stored.verticalOffset = fields.at(4).toDouble();
stored.zoom = fields.at(5).toDouble();
return stored;
PlaymatResolution res;
res.card.name = fields.at(0);
res.card.providerId = fields.at(1);
res.params.marginPctL = fields.at(2).toDouble();
res.params.marginPctR = fields.at(3).toDouble();
res.params.verticalOffset = fields.at(4).toDouble();
res.params.zoom = fields.at(5).toDouble();
return res;
}
} // namespace
@ -194,28 +195,28 @@ int InterfaceSettings::getPlaymatVisibility() const
return getValue("playmatvisibility", QString(), QString(), 2).toInt();
}
QList<StoredPlaymat> InterfaceSettings::getPlaymatFallbackList() const
QList<PlaymatResolution> InterfaceSettings::getPlaymatFallbackList() const
{
const QStringList entries = getValue("playmatFallbackList", QString(), QString(), QStringList()).toStringList();
QList<StoredPlaymat> result;
QList<PlaymatResolution> result;
result.reserve(entries.size());
for (const QString &entry : entries) {
const StoredPlaymat stored = decodeStoredPlaymat(entry);
if (!stored.isEmpty()) {
result.append(stored);
const PlaymatResolution res = decodePlaymatResolution(entry);
if (!res.card.isEmpty()) {
result.append(res);
}
}
return result;
}
StoredPlaymat InterfaceSettings::getPlaymatOverride() const
int InterfaceSettings::getPlaymatMode() const
{
return decodeStoredPlaymat(getValue("playmatOverride", QString(), QString(), QString()).toString());
return qBound(0, getValue("playmatMode", QString(), QString(), 1).toInt(), 2);
}
int InterfaceSettings::getPlaymatFallbackMode() const
int InterfaceSettings::getPlaymatFallbackBehavior() const
{
return qBound(0, getValue("playmatFallbackMode", QString(), QString(), 1).toInt(), 2);
return qBound(0, getValue("playmatFallbackBehavior", QString(), QString(), 0).toInt(), 2);
}
bool InterfaceSettings::getLifeCounterAnimationsEnabled() const
@ -410,30 +411,34 @@ void InterfaceSettings::setPlaymatVisibility(int _visibility)
emit playmatVisibilityChanged(_visibility);
}
void InterfaceSettings::setPlaymatFallbackList(const QList<StoredPlaymat> &_fallbackList)
void InterfaceSettings::setPlaymatFallbackList(const QList<PlaymatResolution> &_fallbackList)
{
QStringList entries;
entries.reserve(_fallbackList.size());
for (const StoredPlaymat &stored : _fallbackList) {
entries.append(encodeStoredPlaymat(stored));
for (const PlaymatResolution &res : _fallbackList) {
entries.append(encodePlaymatResolution(res));
}
setValue(entries, "playmatFallbackList");
emit playmatSettingsChanged();
}
void InterfaceSettings::setPlaymatOverride(const StoredPlaymat &_override)
void InterfaceSettings::setPlaymatMode(int _mode)
{
setValue(_override.isEmpty() ? QString() : encodeStoredPlaymat(_override), "playmatOverride");
const int mode = qBound(0, _mode, 2);
if (getPlaymatMode() == mode) {
return;
}
setValue(mode, "playmatMode");
emit playmatSettingsChanged();
}
void InterfaceSettings::setPlaymatFallbackMode(int _fallbackMode)
void InterfaceSettings::setPlaymatFallbackBehavior(int _behavior)
{
const int mode = qBound(0, _fallbackMode, 2);
if (getPlaymatFallbackMode() == mode) {
const int behavior = qBound(0, _behavior, 2);
if (getPlaymatFallbackBehavior() == behavior) {
return;
}
setValue(mode, "playmatFallbackMode");
setValue(behavior, "playmatFallbackBehavior");
emit playmatSettingsChanged();
}

View file

@ -43,9 +43,9 @@ public:
[[nodiscard]] bool getShowShortcuts() const override;
[[nodiscard]] bool getShowGameSelectorFilterToolbar() const override;
[[nodiscard]] int getPlaymatVisibility() const override;
[[nodiscard]] QList<StoredPlaymat> getPlaymatFallbackList() const override;
[[nodiscard]] StoredPlaymat getPlaymatOverride() const override;
[[nodiscard]] int getPlaymatFallbackMode() const override;
[[nodiscard]] QList<PlaymatResolution> getPlaymatFallbackList() const override;
[[nodiscard]] int getPlaymatMode() const override;
[[nodiscard]] int getPlaymatFallbackBehavior() const override;
[[nodiscard]] bool getLifeCounterAnimationsEnabled() const override;
[[nodiscard]] bool getBattlefieldFlashEnabled() const override;
[[nodiscard]] QStringList getUserListExpandedSections() const override;
@ -82,9 +82,9 @@ public:
void setShowShortcuts(bool _showShortcuts);
void setShowGameSelectorFilterToolbar(bool _showGameSelectorFilterToolbar);
void setPlaymatVisibility(int _visibility);
void setPlaymatFallbackList(const QList<StoredPlaymat> &_fallbackList);
void setPlaymatOverride(const StoredPlaymat &_override);
void setPlaymatFallbackMode(int _fallbackMode);
void setPlaymatFallbackList(const QList<PlaymatResolution> &_fallbackList);
void setPlaymatMode(int _mode);
void setPlaymatFallbackBehavior(int _behavior);
void setLifeCounterAnimationsEnabled(bool _lifeCounterAnimationsEnabled);
void setBattlefieldFlashEnabled(bool _battlefieldFlashEnabled);
void setUserListExpandedSections(const QStringList &sections);

View file

@ -10,11 +10,13 @@ set(UTILITY_SOURCES libcockatrice/utility/expression.cpp libcockatrice/utility/l
)
set(UTILITY_HEADERS
libcockatrice/utility/card_ref.h
libcockatrice/utility/color.h
libcockatrice/utility/expression.h
libcockatrice/utility/levenshtein.h
libcockatrice/utility/macros.h
libcockatrice/utility/passwordhasher.h
libcockatrice/utility/playmat_params.h
libcockatrice/utility/string_limits.h
libcockatrice/utility/dice_limits.h
libcockatrice/utility/counter_limits.h

View file

@ -0,0 +1,52 @@
#ifndef COCKATRICE_PLAYMAT_PARAMS_H
#define COCKATRICE_PLAYMAT_PARAMS_H
#include "card_ref.h"
#include <cmath>
/**
* @struct PlaymatParams
* @ingroup Decks
* @brief Positioning parameters for a playmat card image.
*
* Controls how the cropped card art is positioned within the
* combined table+stack play area. The coordinate system is
* relative to the cropped art source image.
*/
struct PlaymatParams
{
double marginPctL = 0.07; ///< Left margin as fraction of card width (0.0–0.95).
double marginPctR = 0.07; ///< Right margin as fraction of card width (0.0–0.95).
double verticalOffset = 0.33; ///< Vertical position within card (0.0=top, 1.0=bottom).
double zoom = 1.0; ///< Scale factor (0.1–4.0).
bool operator==(const PlaymatParams &other) const
{
return qFuzzyCompare(marginPctL, other.marginPctL) && qFuzzyCompare(marginPctR, other.marginPctR) &&
qFuzzyCompare(verticalOffset, other.verticalOffset) && qFuzzyCompare(zoom, other.zoom);
}
bool operator!=(const PlaymatParams &other) const
{
return !(*this == other);
}
};
/**
* @struct PlaymatResolution
* @ingroup Decks
* @brief A resolved playmat (card + positioning parameters).
*/
struct PlaymatResolution
{
CardRef card; ///< The card whose art is used as playmat.
PlaymatParams params; ///< Positioning parameters for the playmat card image.
bool operator==(const PlaymatResolution &other) const
{
return card == other.card && params == other.params;
}
};
#endif // COCKATRICE_PLAYMAT_PARAMS_H

View file

@ -17,22 +17,19 @@ PlaymatResolution makeResolution(const QString &name, const QString &providerId
TEST(PlaymatResolverTest, EmptyChainReturnsEmpty)
{
DeckList deck;
int rotationIndex = 0;
const PlaymatResolution resolved = resolveEffectivePlaymat(deck, {}, {}, PlaymatFallbackMode::Fixed, rotationIndex);
const PlaymatResolution resolved = resolveEffectivePlaymat(deck, {}, {}, PlaymatFallbackMode::Fixed, 0);
EXPECT_TRUE(resolved.card.isEmpty());
}
TEST(PlaymatResolverTest, OverrideWinsOverDeckAndFallback)
{
DeckList deck;
deck.setPlaymatCard({QStringLiteral("Deck Mat"), QStringLiteral("deck-provider")});
deck.setPlaymat({{QStringLiteral("Deck Mat"), QStringLiteral("deck-provider")}, {}});
const PlaymatResolution force = makeResolution(QStringLiteral("Force Mat"), QStringLiteral("force-provider"));
const QList<PlaymatResolution> fallback = {makeResolution(QStringLiteral("Fallback Mat"))};
int rotationIndex = 0;
const PlaymatResolution resolved =
resolveEffectivePlaymat(deck, force, fallback, PlaymatFallbackMode::Fixed, rotationIndex);
const PlaymatResolution resolved = resolveEffectivePlaymat(deck, force, fallback, PlaymatFallbackMode::Fixed, 0);
EXPECT_EQ(resolved.card.name, QStringLiteral("Force Mat"));
EXPECT_EQ(resolved.card.providerId, QStringLiteral("force-provider"));
}
@ -40,14 +37,11 @@ TEST(PlaymatResolverTest, OverrideWinsOverDeckAndFallback)
TEST(PlaymatResolverTest, DeckWinsOverFallback)
{
DeckList deck;
deck.setPlaymatCard({QStringLiteral("Deck Mat"), QStringLiteral("deck-provider")});
deck.setPlaymatParams({0.1, 0.2, 0.3, 1.5});
deck.setPlaymat({{QStringLiteral("Deck Mat"), QStringLiteral("deck-provider")}, {0.1, 0.2, 0.3, 1.5}});
const QList<PlaymatResolution> fallback = {makeResolution(QStringLiteral("Fallback Mat"))};
int rotationIndex = 0;
const PlaymatResolution resolved =
resolveEffectivePlaymat(deck, {}, fallback, PlaymatFallbackMode::Fixed, rotationIndex);
const PlaymatResolution resolved = resolveEffectivePlaymat(deck, {}, fallback, PlaymatFallbackMode::Fixed, 0);
EXPECT_EQ(resolved.card.name, QStringLiteral("Deck Mat"));
EXPECT_EQ(resolved.card.providerId, QStringLiteral("deck-provider"));
EXPECT_DOUBLE_EQ(resolved.params.marginPctL, 0.1);
@ -59,9 +53,7 @@ TEST(PlaymatResolverTest, FallbackUsedWhenDeckHasNone)
DeckList deck;
const QList<PlaymatResolution> fallback = {makeResolution(QStringLiteral("Fallback Mat"))};
int rotationIndex = 0;
const PlaymatResolution resolved =
resolveEffectivePlaymat(deck, {}, fallback, PlaymatFallbackMode::Fixed, rotationIndex);
const PlaymatResolution resolved = resolveEffectivePlaymat(deck, {}, fallback, PlaymatFallbackMode::Fixed, 0);
EXPECT_EQ(resolved.card.name, QStringLiteral("Fallback Mat"));
}
@ -71,10 +63,8 @@ TEST(PlaymatResolverTest, FixedAlwaysUsesFirst)
const QList<PlaymatResolution> fallback = {makeResolution(QStringLiteral("First")),
makeResolution(QStringLiteral("Second"))};
int rotationIndex = 3; // ignored in Fixed mode
for (int i = 0; i < 5; ++i) {
const PlaymatResolution resolved =
resolveEffectivePlaymat(deck, {}, fallback, PlaymatFallbackMode::Fixed, rotationIndex);
const PlaymatResolution resolved = resolveEffectivePlaymat(deck, {}, fallback, PlaymatFallbackMode::Fixed, i);
EXPECT_EQ(resolved.card.name, QStringLiteral("First"));
}
}
@ -86,28 +76,23 @@ TEST(PlaymatResolverTest, RoundRobinCyclesAndWraps)
makeResolution(QStringLiteral("Second")),
makeResolution(QStringLiteral("Third"))};
int rotationIndex = 0;
const QStringList expected = {QStringLiteral("First"), QStringLiteral("Second"), QStringLiteral("Third"),
QStringLiteral("First"), QStringLiteral("Second"), QStringLiteral("Third")};
for (const QString &name : expected) {
for (int i = 0; i < expected.size(); ++i) {
const PlaymatResolution resolved =
resolveEffectivePlaymat(deck, {}, fallback, PlaymatFallbackMode::RoundRobin, rotationIndex);
EXPECT_EQ(resolved.card.name, name);
resolveEffectivePlaymat(deck, {}, fallback, PlaymatFallbackMode::RoundRobin, i);
EXPECT_EQ(resolved.card.name, expected.at(i));
}
EXPECT_EQ(rotationIndex, expected.size());
}
TEST(PlaymatResolverTest, RoundRobinRespectsExistingCursor)
TEST(PlaymatResolverTest, RoundRobinRespectsCursor)
{
DeckList deck;
const QList<PlaymatResolution> fallback = {makeResolution(QStringLiteral("First")),
makeResolution(QStringLiteral("Second"))};
int rotationIndex = 5;
const PlaymatResolution resolved =
resolveEffectivePlaymat(deck, {}, fallback, PlaymatFallbackMode::RoundRobin, rotationIndex);
const PlaymatResolution resolved = resolveEffectivePlaymat(deck, {}, fallback, PlaymatFallbackMode::RoundRobin, 5);
EXPECT_EQ(resolved.card.name, QStringLiteral("Second")); // 5 % 2 == 1
EXPECT_EQ(rotationIndex, 6);
}
TEST(PlaymatResolverTest, RandomStaysWithinList)
@ -117,10 +102,8 @@ TEST(PlaymatResolverTest, RandomStaysWithinList)
makeResolution(QStringLiteral("Second")),
makeResolution(QStringLiteral("Third"))};
int rotationIndex = 0;
for (int i = 0; i < 50; ++i) {
const PlaymatResolution resolved =
resolveEffectivePlaymat(deck, {}, fallback, PlaymatFallbackMode::Random, rotationIndex);
const PlaymatResolution resolved = resolveEffectivePlaymat(deck, {}, fallback, PlaymatFallbackMode::Random, i);
ASSERT_FALSE(resolved.card.name.isEmpty());
EXPECT_TRUE(fallback.contains(resolved));
}
@ -129,13 +112,11 @@ TEST(PlaymatResolverTest, RandomStaysWithinList)
TEST(PlaymatResolverTest, ForceWithEmptyCardIgnoresFallbackParamsButNotFallback)
{
DeckList deck;
deck.setPlaymatCard({QStringLiteral("Deck Mat")});
deck.setPlaymat({{QStringLiteral("Deck Mat")}, {}});
// An empty force entry must not mask the deck-configured playmat.
const PlaymatResolution emptyForce;
int rotationIndex = 0;
const PlaymatResolution resolved =
resolveEffectivePlaymat(deck, emptyForce, {}, PlaymatFallbackMode::Fixed, rotationIndex);
const PlaymatResolution resolved = resolveEffectivePlaymat(deck, emptyForce, {}, PlaymatFallbackMode::Fixed, 0);
EXPECT_EQ(resolved.card.name, QStringLiteral("Deck Mat"));
}