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_collection_dialog.h
src/interface/widgets/playmat/playmat_preview_widget.cpp src/interface/widgets/playmat/playmat_preview_widget.cpp
src/interface/widgets/playmat/playmat_settings_dialog.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_button_widget.cpp
src/interface/widgets/quick_settings/settings_popup_widget.cpp src/interface/widgets/quick_settings/settings_popup_widget.cpp
src/interface/widgets/replay/replay_manager.cpp src/interface/widgets/replay/replay_manager.cpp

View file

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

View file

@ -252,12 +252,11 @@ void PlayerLogic::setDeck(const DeckList &_deck)
void PlayerLogic::setPlaymatFromProperties(const ServerInfo_PlayerProperties &props) void PlayerLogic::setPlaymatFromProperties(const ServerInfo_PlayerProperties &props)
{ {
if (props.has_playmat_card_name() && !props.playmat_card_name().empty()) { if (props.has_playmat_params() && !props.playmat_params().card_name().empty()) {
remotePlaymatCard = {QString::fromStdString(props.playmat_card_name()), const auto &pp = props.playmat_params();
QString::fromStdString(props.playmat_card_provider_id())}; remotePlaymatCard = {QString::fromStdString(pp.card_name()), QString::fromStdString(pp.card_provider_id())};
remotePlaymatParams = { remotePlaymatParams = {qBound(0.0, pp.margin_pct_l(), 0.95), qBound(0.0, pp.margin_pct_r(), 0.95),
qBound(0.0, props.playmat_margin_pct_l(), 0.95), qBound(0.0, props.playmat_margin_pct_r(), 0.95), qBound(0.0, pp.vertical_offset(), 1.0), qBound(0.1, pp.zoom(), 4.0)};
qBound(0.0, props.playmat_vertical_offset(), 1.0), qBound(0.1, props.playmat_zoom(), 4.0)};
hasRemotePlaymat = true; hasRemotePlaymat = true;
} else { } else {
remotePlaymatCard = CardRef{}; 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_clipboard.h"
#include "../../interface/widgets/dialogs/dlg_load_deck_from_website.h" #include "../../interface/widgets/dialogs/dlg_load_deck_from_website.h"
#include "../../interface/widgets/dialogs/dlg_load_remote_deck.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 "../../interface/widgets/tabs/tab_game.h"
#include "deck_view.h" #include "deck_view.h"
#include <QMessageBox> #include <QMessageBox>
#include <libcockatrice/card/database/card_database.h> #include <libcockatrice/card/database/card_database.h>
#include <libcockatrice/card/database/card_database_manager.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_deck_select.pb.h>
#include <libcockatrice/protocol/pb/command_ready_start.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_lock.pb.h>
#include <libcockatrice/protocol/pb/command_set_sideboard_plan.pb.h> #include <libcockatrice/protocol/pb/command_set_sideboard_plan.pb.h>
#include <libcockatrice/protocol/pb/response_deck_download.pb.h> #include <libcockatrice/protocol/pb/response_deck_download.pb.h>
#include <libcockatrice/protocol/pending_command.h> #include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/settings/interface_settings.h>
#include <libcockatrice/settings/visual_deck_storage_settings.h> #include <libcockatrice/settings/visual_deck_storage_settings.h>
#include <libcockatrice/utility/string_limits.h> #include <libcockatrice/utility/string_limits.h>
@ -101,6 +103,9 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent)
connect(&SettingsCache::instance().visualDeckStorage(), &VisualDeckStorageSettings::visualDeckStorageInGameChanged, connect(&SettingsCache::instance().visualDeckStorage(), &VisualDeckStorageSettings::visualDeckStorageInGameChanged,
this, &DeckViewContainer::setVisualDeckStorageExists); this, &DeckViewContainer::setVisualDeckStorageExists);
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::playmatSettingsChanged, this,
&DeckViewContainer::onPlaymatSettingsChanged);
switchToDeckSelectView(); switchToDeckSelectView();
} }
@ -278,11 +283,9 @@ void DeckViewContainer::loadDeckFromFile(const QString &filePath)
void DeckViewContainer::loadDeckFromDeckList(const DeckList &deck) void DeckViewContainer::loadDeckFromDeckList(const DeckList &deck)
{ {
// Bake the user-level playmat resolution (override > deck > fallback list) currentDeck = deck;
// into the uploaded deck so the server, local and remote players, and
// replays all derive the same effective playmat. QString deckString = deck.writeToString_Native();
const DeckList effectiveDeck = applyUserPlaymatSettings(deck, SettingsCache::instance().userInterface());
QString deckString = effectiveDeck.writeToString_Native();
if (deckString.length() > MAX_FILE_LENGTH) { if (deckString.length() > MAX_FILE_LENGTH) {
QMessageBox::critical(this, tr("Error"), tr("Deck is greater than maximum file size.")); 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); PendingCommand *pend = parentGame->getGame()->getGameEventHandler()->prepareGameCommand(cmd);
connect(pend, &PendingCommand::finished, this, &DeckViewContainer::deckSelectFinished); connect(pend, &PendingCommand::finished, this, &DeckViewContainer::deckSelectFinished);
parentGame->getGame()->getGameEventHandler()->sendGameCommand(pend, playerId); 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() void DeckViewContainer::loadRemoteDeck()
@ -384,6 +446,10 @@ void DeckViewContainer::sideboardPlanChanged()
*/ */
void DeckViewContainer::sendReadyStartCommand(bool ready) void DeckViewContainer::sendReadyStartCommand(bool ready)
{ {
if (ready) {
resolveAndSendPlaymat();
}
Command_ReadyStart cmd; Command_ReadyStart cmd;
cmd.set_ready(ready); cmd.set_ready(ready);
parentGame->getGame()->getGameEventHandler()->sendGameCommand(cmd, playerId); parentGame->getGame()->getGameEventHandler()->sendGameCommand(cmd, playerId);
@ -421,6 +487,7 @@ void DeckViewContainer::setSideboardLocked(bool locked)
void DeckViewContainer::setDeck(const DeckList &deck) void DeckViewContainer::setDeck(const DeckList &deck)
{ {
currentDeck = deck;
deckView->setDeck(deck); deckView->setDeck(deck);
switchToDeckLoadedView(); switchToDeckLoadedView();
} }

View file

@ -57,6 +57,9 @@ private:
VisualDeckStorageWidget *visualDeckStorageWidget; VisualDeckStorageWidget *visualDeckStorageWidget;
TabGame *parentGame; TabGame *parentGame;
int playerId; 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 tryCreateVisualDeckStorageWidget();
void sendReadyStartCommand(bool ready); void sendReadyStartCommand(bool ready);
@ -75,6 +78,7 @@ private slots:
void sideboardLockButtonClicked(); void sideboardLockButtonClicked();
void updateSideboardLockButtonText(); void updateSideboardLockButtonText();
void refreshShortcuts(); void refreshShortcuts();
void onPlaymatSettingsChanged();
signals: signals:
void newCardAdded(AbstractCardItem *card); void newCardAdded(AbstractCardItem *card);
void notIdle(); void notIdle();
@ -87,6 +91,8 @@ public:
void setSideboardLocked(bool locked); void setSideboardLocked(bool locked);
void setDeck(const DeckList &deck); void setDeck(const DeckList &deck);
void setVisualDeckStorageExists(bool exists); void setVisualDeckStorageExists(bool exists);
void advancePlaymatRotation();
void resolveAndSendPlaymat();
public slots: public slots:
void loadDeckFromFile(const QString &filePath); void loadDeckFromFile(const QString &filePath);

View file

@ -379,15 +379,18 @@ void PlayerGraphicsItem::updatePlaymat()
CardRef playmatCard; CardRef playmatCard;
PlaymatParams params; PlaymatParams params;
// Check local deck first (for local player) if (player->getHasRemotePlaymat()) {
const DeckList &deck = player->getDeck(); // Prefer the server-confirmed playmat (updated by Command_SetPlaymat).
if (!deck.getPlaymatCard().isEmpty()) {
playmatCard = deck.getPlaymatCard();
params = deck.getPlaymatParams();
} else if (visibility == 2 && player->getHasRemotePlaymat()) {
// "Show all" — also show remote playmats from opponents
playmatCard = player->getRemotePlaymatCard(); playmatCard = player->getRemotePlaymatCard();
params = player->getRemotePlaymatParams(); 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()) { if (playmatCard.isEmpty()) {

View file

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

View file

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

View file

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

View file

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

View file

@ -2,7 +2,7 @@
#define COCKATRICE_PLAYMAT_COLLECTION_DIALOG_H #define COCKATRICE_PLAYMAT_COLLECTION_DIALOG_H
#include <QDialog> #include <QDialog>
#include <libcockatrice/interfaces/interface_interface_settings_provider.h> #include <libcockatrice/utility/playmat_params.h>
class QComboBox; class QComboBox;
class QLabel; class QLabel;
@ -39,9 +39,8 @@ private:
void setupUi(); void setupUi();
void retranslateUi(); void retranslateUi();
int currentRow() const; 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; QListWidget *playmatList;
QComboBox *modeCombo; QComboBox *modeCombo;
QLabel *modeLabel; 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 "../interface/widgets/general/background_sources.h"
#include "../playmat/playmat_collection_dialog.h" #include "../playmat/playmat_collection_dialog.h"
#include "../playmat/playmat_settings_dialog.h" #include "../playmat/playmat_settings_dialog.h"
#include "../playmat/playmat_settings_utils.h"
#include <QApplication> #include <QApplication>
#include <QColorDialog> #include <QColorDialog>
@ -342,22 +341,28 @@ AppearanceSettingsPage::AppearanceSettingsPage()
}); });
playmatVisibilityLabel.setBuddy(&playmatVisibilityCombo); playmatVisibilityLabel.setBuddy(&playmatVisibilityCombo);
// User-level playmat settings: override (force all decks) and fallback collection. // Playmat mode: Override / Fallback / Deck-only
connect(&playmatOverrideEditButton, &QPushButton::clicked, this, playmatModeCombo.addItem(tr("Override deck playmat"), 0);
&AppearanceSettingsPage::openOverridePlaymatSettings); playmatModeCombo.addItem(tr("Fallback if deck has none"), 1);
connect(&playmatOverrideClearButton, &QPushButton::clicked, this, &AppearanceSettingsPage::clearOverridePlaymat); 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, connect(&playmatDefaultEditButton, &QPushButton::clicked, this,
&AppearanceSettingsPage::openPlaymatCollectionDialog); &AppearanceSettingsPage::openPlaymatCollectionDialog);
auto *overrideButtons = new QHBoxLayout;
overrideButtons->addWidget(&playmatOverrideEditButton);
overrideButtons->addWidget(&playmatOverrideClearButton);
auto *playmatGrid = new QGridLayout; auto *playmatGrid = new QGridLayout;
playmatGrid->addWidget(&playmatVisibilityLabel, 0, 0, 1, 1); playmatGrid->addWidget(&playmatVisibilityLabel, 0, 0, 1, 1);
playmatGrid->addWidget(&playmatVisibilityCombo, 0, 1, 1, 1); playmatGrid->addWidget(&playmatVisibilityCombo, 0, 1, 1, 1);
playmatGrid->addWidget(&playmatOverrideLabel, 1, 0, 1, 1); playmatGrid->addWidget(&playmatModeLabel, 1, 0, 1, 1);
playmatGrid->addLayout(overrideButtons, 1, 1, 1, 1); playmatGrid->addWidget(&playmatModeCombo, 1, 1, 1, 1);
playmatGrid->addWidget(&playmatDefaultLabel, 2, 0, 1, 1); playmatGrid->addWidget(&playmatDefaultLabel, 2, 0, 1, 1);
playmatGrid->addWidget(&playmatDefaultEditButton, 2, 1, 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() void AppearanceSettingsPage::openPlaymatCollectionDialog()
{ {
PlaymatCollectionDialog dialog(this); PlaymatCollectionDialog dialog(this);
dialog.exec(); 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() void AppearanceSettingsPage::retranslateUi()
@ -567,9 +542,7 @@ void AppearanceSettingsPage::retranslateUi()
minPlayersForMultiColumnLayoutLabel.setText(tr("Minimum player count for multi-column layout:")); minPlayersForMultiColumnLayoutLabel.setText(tr("Minimum player count for multi-column layout:"));
playmatGroupBox->setTitle(tr("Playmat settings")); playmatGroupBox->setTitle(tr("Playmat settings"));
playmatVisibilityLabel.setText(tr("Playmat visibility:")); playmatVisibilityLabel.setText(tr("Playmat visibility:"));
playmatOverrideLabel.setText(tr("Force playmat for all decks:")); playmatModeLabel.setText(tr("Default collection behavior:"));
playmatOverrideClearButton.setText(tr("Clear")); playmatDefaultLabel.setText(tr("Default playmat collection:"));
playmatDefaultLabel.setText(tr("Default playmats (when a deck has none):"));
playmatDefaultEditButton.setText(tr("Edit...")); playmatDefaultEditButton.setText(tr("Edit..."));
updatePlaymatSummary();
} }

View file

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

View file

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

View file

@ -25,7 +25,7 @@ static const QString CURRENT_SIDEBOARD_PLAN_KEY = "";
bool DeckList::Metadata::isEmpty() const 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() DeckList::DeckList()
@ -82,28 +82,28 @@ bool DeckList::readElement(QXmlStreamReader *xml)
QString vOffStr = xml->attributes().value("verticalOffset").toString(); QString vOffStr = xml->attributes().value("verticalOffset").toString();
QString zoomStr = xml->attributes().value("zoom").toString(); QString zoomStr = xml->attributes().value("zoom").toString();
QString cardName = xml->readElementText(); QString cardName = xml->readElementText();
metadata.playmatCard = {cardName, providerId}; PlaymatResolution playmat;
PlaymatParams params; playmat.card = {cardName, providerId};
// Clamp to the same ranges as the settings dialog and the remote // Clamp to the same ranges as the settings dialog and the remote
// player-properties path so malformed deck files cannot produce // player-properties path so malformed deck files cannot produce
// degenerate art rectangles (e.g. a zoom of 0 dividing by zero). // 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) { 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) { 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) { 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) { if (!ok) {
params.zoom = 1.0; playmat.params.zoom = 1.0;
} }
metadata.playmatParams = params; metadata.playmat = playmat;
} else if (childName == "tags") { } else if (childName == "tags") {
metadata.tags.clear(); // Clear existing tags metadata.tags.clear(); // Clear existing tags
while (xml->readNextStartElement()) { while (xml->readNextStartElement()) {
@ -134,14 +134,14 @@ static void writeMetadata(QXmlStreamWriter *xml, const DeckList::Metadata &metad
xml->writeAttribute("providerId", metadata.bannerCard.providerId); xml->writeAttribute("providerId", metadata.bannerCard.providerId);
xml->writeCharacters(metadata.bannerCard.name); xml->writeCharacters(metadata.bannerCard.name);
xml->writeEndElement(); xml->writeEndElement();
if (!metadata.playmatCard.isEmpty()) { if (!metadata.playmat.card.isEmpty()) {
xml->writeStartElement("playmatCard"); xml->writeStartElement("playmatCard");
xml->writeAttribute("providerId", metadata.playmatCard.providerId); xml->writeAttribute("providerId", metadata.playmat.card.providerId);
xml->writeAttribute("marginPctL", QString::number(metadata.playmatParams.marginPctL, 'f', 4)); xml->writeAttribute("marginPctL", QString::number(metadata.playmat.params.marginPctL, 'f', 4));
xml->writeAttribute("marginPctR", QString::number(metadata.playmatParams.marginPctR, 'f', 4)); xml->writeAttribute("marginPctR", QString::number(metadata.playmat.params.marginPctR, 'f', 4));
xml->writeAttribute("verticalOffset", QString::number(metadata.playmatParams.verticalOffset, 'f', 4)); xml->writeAttribute("verticalOffset", QString::number(metadata.playmat.params.verticalOffset, 'f', 4));
xml->writeAttribute("zoom", QString::number(metadata.playmatParams.zoom, 'f', 4)); xml->writeAttribute("zoom", QString::number(metadata.playmat.params.zoom, 'f', 4));
xml->writeCharacters(metadata.playmatCard.name); xml->writeCharacters(metadata.playmat.card.name);
xml->writeEndElement(); xml->writeEndElement();
} }
xml->writeTextElement("comments", metadata.comments); xml->writeTextElement("comments", metadata.comments);

View file

@ -18,7 +18,7 @@
#include <QMap> #include <QMap>
#include <QVector> #include <QVector>
#include <QtCore/QXmlStreamReader> #include <QtCore/QXmlStreamReader>
#include <libcockatrice/utility/card_ref.h> #include <libcockatrice/utility/playmat_params.h>
class AbstractDecklistNode; class AbstractDecklistNode;
class DecklistCardNode; class DecklistCardNode;
@ -27,34 +27,6 @@ class QIODevice;
class QTextStream; class QTextStream;
class InnerDecklistNode; 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 * @class DeckList
* @ingroup Decks * @ingroup Decks
@ -98,8 +70,7 @@ public:
CardRef bannerCard; ///< Optional representative card for the deck. CardRef bannerCard; ///< Optional representative card for the deck.
QStringList tags; ///< User-defined tags for deck classification. QStringList tags; ///< User-defined tags for deck classification.
QString lastLoadedTimestamp; ///< Timestamp string of last load. QString lastLoadedTimestamp; ///< Timestamp string of last load.
CardRef playmatCard; ///< Optional card used as playmat background for table+stack zones. PlaymatResolution playmat; ///< Optional playmat background for table+stack zones.
PlaymatParams playmatParams; ///< Positioning parameters for the playmat card image.
/** /**
* @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.
@ -145,13 +116,9 @@ public:
{ {
metadata.bannerCard = _bannerCard; metadata.bannerCard = _bannerCard;
} }
void setPlaymatCard(const CardRef &_playmatCard = {}) void setPlaymat(const PlaymatResolution &_playmat = {})
{ {
metadata.playmatCard = _playmatCard; metadata.playmat = _playmat;
}
void setPlaymatParams(const PlaymatParams &_playmatParams = {})
{
metadata.playmatParams = _playmatParams;
} }
void setLastLoadedTimestamp(const QString &_lastLoadedTimestamp = QString()) void setLastLoadedTimestamp(const QString &_lastLoadedTimestamp = QString())
{ {
@ -208,13 +175,9 @@ public:
{ {
return metadata.bannerCard; return metadata.bannerCard;
} }
CardRef getPlaymatCard() const PlaymatResolution getPlaymat() const
{ {
return metadata.playmatCard; return metadata.playmat;
}
PlaymatParams getPlaymatParams() const
{
return metadata.playmatParams;
} }
QString getLastLoadedTimestamp() const QString getLastLoadedTimestamp() const
{ {

View file

@ -6,14 +6,15 @@ PlaymatResolution resolveEffectivePlaymat(const DeckList &deck,
const PlaymatResolution &force, const PlaymatResolution &force,
const QList<PlaymatResolution> &fallbackList, const QList<PlaymatResolution> &fallbackList,
PlaymatFallbackMode fallbackMode, PlaymatFallbackMode fallbackMode,
int &rotationIndex) int rotationIndex)
{ {
if (!force.card.isEmpty()) { if (!force.card.isEmpty()) {
return force; return force;
} }
if (!deck.getPlaymatCard().isEmpty()) { const PlaymatResolution &deckPlaymat = deck.getPlaymat();
return {deck.getPlaymatCard(), deck.getPlaymatParams()}; if (!deckPlaymat.card.isEmpty()) {
return deckPlaymat;
} }
if (fallbackList.isEmpty()) { if (fallbackList.isEmpty()) {
@ -24,7 +25,7 @@ PlaymatResolution resolveEffectivePlaymat(const DeckList &deck,
case PlaymatFallbackMode::Fixed: case PlaymatFallbackMode::Fixed:
return fallbackList.first(); return fallbackList.first();
case PlaymatFallbackMode::RoundRobin: case PlaymatFallbackMode::RoundRobin:
return fallbackList.at(rotationIndex++ % fallbackList.size()); return fallbackList.at(rotationIndex % fallbackList.size());
case PlaymatFallbackMode::Random: case PlaymatFallbackMode::Random:
return fallbackList.at(QRandomGenerator::global()->bounded(fallbackList.size())); 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. 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: * @brief Resolves the effective playmat for a deck per the resolution chain:
* force override > deck-configured playmat > fallback list > none. * force override > deck-configured playmat > fallback list > none.
@ -49,6 +33,6 @@ PlaymatResolution resolveEffectivePlaymat(const DeckList &deck,
const PlaymatResolution &force, const PlaymatResolution &force,
const QList<PlaymatResolution> &fallbackList, const QList<PlaymatResolution> &fallbackList,
PlaymatFallbackMode fallbackMode, PlaymatFallbackMode fallbackMode,
int &rotationIndex); int rotationIndex);
#endif // COCKATRICE_PLAYMAT_RESOLVER_H #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_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 <QList>
#include <QString> #include <QString>
#include <QStringList> #include <QStringList>
#include <libcockatrice/utility/playmat_params.h>
/**
* @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();
}
};
class IInterfaceSettingsProvider class IInterfaceSettingsProvider
{ {
@ -66,14 +47,16 @@ public:
[[nodiscard]] virtual QStringList getUserListExpandedSections() const = 0; [[nodiscard]] virtual QStringList getUserListExpandedSections() const = 0;
[[nodiscard]] virtual int getPlaymatVisibility() const = 0; [[nodiscard]] virtual int getPlaymatVisibility() const = 0;
/** @brief User-level fallback playmats, used when a deck has none configured. */ /** @brief User-level playmat collection. Used either as a forced playmat
[[nodiscard]] virtual QList<StoredPlaymat> getPlaymatFallbackList() const = 0; * (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. */ /** @brief How the fallback list is applied: Always (0), Fallback (1), Never (2). */
[[nodiscard]] virtual StoredPlaymat getPlaymatOverride() const = 0; [[nodiscard]] virtual int getPlaymatMode() const = 0;
/** @brief How the fallback list is consulted (0 Fixed, 1 Round-robin, 2 Random). */ /** @brief How the fallback list is picked from when mode is Fallback
[[nodiscard]] virtual int getPlaymatFallbackMode() const = 0; * (0 Fixed, 1 Round-robin, 2 Random). */
[[nodiscard]] virtual int getPlaymatFallbackBehavior() const = 0;
}; };
#endif // COCKATRICE_INTERFACE_INTERFACE_SETTINGS_PROVIDER_H #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_attr.pb.h>
#include <libcockatrice/protocol/pb/command_set_card_counter.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_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_lock.pb.h>
#include <libcockatrice/protocol/pb/command_set_sideboard_plan.pb.h> #include <libcockatrice/protocol/pb/command_set_sideboard_plan.pb.h>
#include <libcockatrice/protocol/pb/command_shuffle.pb.h> #include <libcockatrice/protocol/pb/command_shuffle.pb.h>
@ -144,6 +145,13 @@ Response::ResponseCode Server_AbstractParticipant::cmdSetSideboardLock(const Com
return Response::RespFunctionNotAllowed; 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*/, Response::ResponseCode Server_AbstractParticipant::cmdConcede(const Command_Concede & /*cmd*/,
ResponseContainer & /*rc*/, ResponseContainer & /*rc*/,
GameEventStorage & /*ges*/) GameEventStorage & /*ges*/)
@ -525,6 +533,9 @@ Server_AbstractParticipant::processGameCommand(const GameCommand &command, Respo
case GameCommand::REVERSE_TURN: case GameCommand::REVERSE_TURN:
return cmdReverseTurn(command.GetExtension(Command_ReverseTurn::ext), rc, ges); return cmdReverseTurn(command.GetExtension(Command_ReverseTurn::ext), rc, ges);
break; break;
case GameCommand::SET_PLAYMAT:
return cmdSetPlaymat(command.GetExtension(Command_SetPlaymat::ext), rc, ges);
break;
default: default:
return Response::RespInvalidCommand; return Response::RespInvalidCommand;
} }

View file

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

View file

@ -1653,13 +1653,13 @@ void Server_AbstractPlayer::getPlayerProperties(ServerInfo_PlayerProperties &res
result.set_ready_start(readyStart); result.set_ready_start(readyStart);
if (deck) { if (deck) {
result.set_deck_hash(deck->getDeckHash().toStdString()); result.set_deck_hash(deck->getDeckHash().toStdString());
const auto &playmatCard = deck->getPlaymatCard(); const auto &playmat = deck->getPlaymat();
const auto &params = deck->getPlaymatParams(); auto *playmatParams = result.mutable_playmat_params();
result.set_playmat_card_name(playmatCard.name.toStdString()); playmatParams->set_card_name(playmat.card.name.toStdString());
result.set_playmat_card_provider_id(playmatCard.providerId.toStdString()); playmatParams->set_card_provider_id(playmat.card.providerId.toStdString());
result.set_playmat_margin_pct_l(params.marginPctL); playmatParams->set_margin_pct_l(playmat.params.marginPctL);
result.set_playmat_margin_pct_r(params.marginPctR); playmatParams->set_margin_pct_r(playmat.params.marginPctR);
result.set_playmat_vertical_offset(params.verticalOffset); playmatParams->set_vertical_offset(playmat.params.verticalOffset);
result.set_playmat_zoom(params.zoom); 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_mulligan.pb.h>
#include <libcockatrice/protocol/pb/command_set_active_phase.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_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_lock.pb.h>
#include <libcockatrice/protocol/pb/command_set_sideboard_plan.pb.h> #include <libcockatrice/protocol/pb/command_set_sideboard_plan.pb.h>
#include <libcockatrice/protocol/pb/command_shuffle.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_PlayerPropertiesChanged event;
event.mutable_player_properties()->set_sideboard_locked(true); event.mutable_player_properties()->set_sideboard_locked(true);
event.mutable_player_properties()->set_deck_hash(deck->getDeckHash().toStdString()); event.mutable_player_properties()->set_deck_hash(deck->getDeckHash().toStdString());
const auto &playmatCard = deck->getPlaymatCard(); const auto &playmat = deck->getPlaymat();
const auto &params = deck->getPlaymatParams(); auto *playmatParams = event.mutable_player_properties()->mutable_playmat_params();
event.mutable_player_properties()->set_playmat_card_name(playmatCard.name.toStdString()); playmatParams->set_card_name(playmat.card.name.toStdString());
event.mutable_player_properties()->set_playmat_card_provider_id(playmatCard.providerId.toStdString()); playmatParams->set_card_provider_id(playmat.card.providerId.toStdString());
event.mutable_player_properties()->set_playmat_margin_pct_l(params.marginPctL); playmatParams->set_margin_pct_l(playmat.params.marginPctL);
event.mutable_player_properties()->set_playmat_margin_pct_r(params.marginPctR); playmatParams->set_margin_pct_r(playmat.params.marginPctR);
event.mutable_player_properties()->set_playmat_vertical_offset(params.verticalOffset); playmatParams->set_vertical_offset(playmat.params.verticalOffset);
event.mutable_player_properties()->set_playmat_zoom(params.zoom); playmatParams->set_zoom(playmat.params.zoom);
ges.enqueueGameEvent(event, playerId); ges.enqueueGameEvent(event, playerId);
Context_DeckSelect context; Context_DeckSelect context;
@ -602,6 +603,41 @@ Server_Player::cmdReverseTurn(const Command_ReverseTurn &cmd, ResponseContainer
return Server_AbstractParticipant::cmdReverseTurn(cmd, rc, ges); 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, void Server_Player::getInfo(ServerInfo_Player *info,
Server_AbstractParticipant *recipient, Server_AbstractParticipant *recipient,
bool omniscient, bool omniscient,

View file

@ -62,6 +62,8 @@ public:
cmdSetActivePhase(const Command_SetActivePhase &cmd, ResponseContainer &rc, GameEventStorage &ges) override; cmdSetActivePhase(const Command_SetActivePhase &cmd, ResponseContainer &rc, GameEventStorage &ges) override;
Response::ResponseCode Response::ResponseCode
cmdReverseTurn(const Command_ReverseTurn & /*cmd*/, ResponseContainer & /*rc*/, GameEventStorage &ges) override; 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, Response::ResponseCode cmdChangeZoneProperties(const Command_ChangeZoneProperties &cmd,
ResponseContainer &rc, ResponseContainer &rc,
GameEventStorage &ges) override; GameEventStorage &ges) override;

View file

@ -46,6 +46,7 @@ set(PROTO_FILES
command_set_card_attr.proto command_set_card_attr.proto
command_set_card_counter.proto command_set_card_counter.proto
command_set_counter.proto command_set_counter.proto
command_set_playmat.proto
command_set_sideboard_lock.proto command_set_sideboard_lock.proto
command_set_sideboard_plan.proto command_set_sideboard_plan.proto
command_shuffle.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 /// Server: Server_Player::cmdReverseTurn
/// Client: reflected via subsequent turn events /// Client: reflected via subsequent turn events
REVERSE_TURN = 1034; 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; extensions 100 to max;

View file

@ -2,6 +2,15 @@ syntax = "proto2";
import "serverinfo_user.proto"; import "serverinfo_user.proto";
message ServerInfo_PlayerProperties { 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 sint32 player_id = 1;
optional ServerInfo_User user_info = 2; optional ServerInfo_User user_info = 2;
optional bool spectator = 3; optional bool spectator = 3;
@ -11,10 +20,5 @@ message ServerInfo_PlayerProperties {
optional sint32 ping_seconds = 7; optional sint32 ping_seconds = 7;
optional bool sideboard_locked = 8; optional bool sideboard_locked = 8;
optional bool judge = 9; optional bool judge = 9;
optional string playmat_card_name = 10; optional PlaymatParams playmat_params = 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];
} }

View file

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

View file

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

View file

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