diff --git a/.ci/Fedora40/Dockerfile b/.ci/Fedora42/Dockerfile
similarity index 95%
rename from .ci/Fedora40/Dockerfile
rename to .ci/Fedora42/Dockerfile
index 598bf7562..cf56ff604 100644
--- a/.ci/Fedora40/Dockerfile
+++ b/.ci/Fedora42/Dockerfile
@@ -1,4 +1,4 @@
-FROM fedora:40
+FROM fedora:42
RUN dnf install -y \
ccache \
diff --git a/.ci/release_template.md b/.ci/release_template.md
index c23e285c3..a7a2d31cb 100644
--- a/.ci/release_template.md
+++ b/.ci/release_template.md
@@ -23,8 +23,8 @@ Available pre-compiled binaries for installation:
• Ubuntu 20.04 LTS Focal Fossa
• Debian 12 Bookworm
• Debian 11 Bullseye
+ • Fedora 42
• Fedora 41
- • Fedora 40
We are also packaged in Arch Linux's official extra repository, courtesy of @FFY00.
General Linux support is available via a flatpak package at Flathub!
diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml
index fbb882aa2..bd7fd37e2 100644
--- a/.github/workflows/desktop-build.yml
+++ b/.github/workflows/desktop-build.yml
@@ -9,6 +9,7 @@ on:
- 'webclient/**'
- '.github/workflows/web-*.yml'
- '.github/workflows/translations-*.yml'
+ - '.github/workflows/docker-release.yml'
tags:
- '*'
pull_request:
@@ -101,12 +102,12 @@ jobs:
package: DEB
- distro: Fedora
- version: 40
+ version: 41
package: RPM
test: skip # Running tests on all distros is superfluous
- distro: Fedora
- version: 41
+ version: 42
package: RPM
- distro: Ubuntu
@@ -380,13 +381,15 @@ jobs:
uses: jurplel/install-qt-action@v4
with:
cache: true
- setup-python: false
+ setup-python: true
version: ${{matrix.qt_version}}
arch: win64_${{matrix.qt_arch}}
tools: ${{matrix.qt_tools}}
modules: ${{matrix.qt_modules}}
- - name: Run vcpkg
+ # TODO: re-enable when https://github.com/lukka/run-vcpkg/issues/243 is fixed
+ - if: false
+ name: Run vcpkg (disabled)
uses: lukka/run-vcpkg@v11
with:
runVcpkgInstall: true
diff --git a/.github/workflows/desktop-lint.yml b/.github/workflows/desktop-lint.yml
index 4ebfd9b79..aee6e81d5 100644
--- a/.github/workflows/desktop-lint.yml
+++ b/.github/workflows/desktop-lint.yml
@@ -7,6 +7,7 @@ on:
- 'webclient/**'
- '.github/workflows/web-*.yml'
- '.github/workflows/translations-*.yml'
+ - '.github/workflows/docker-release.yml'
jobs:
format:
diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml
new file mode 100644
index 000000000..439ea24cd
--- /dev/null
+++ b/.github/workflows/docker-release.yml
@@ -0,0 +1,71 @@
+name: Build Docker Container
+
+on:
+ push:
+ tags:
+ - '*Release*'
+ branches:
+ - master
+ pull_request:
+ branches:
+ - master
+ paths:
+ - '.github/workflows/docker-release.yml'
+ - 'CMakeLists.txt'
+ - 'Dockerfile'
+ - 'servatrice/**'
+ - 'common/**'
+ - 'cmake/**'
+ - '!**.md'
+
+jobs:
+ docker:
+ name: amd64 & arm64
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Docker metadata
+ id: metadata
+ uses: docker/metadata-action@v5
+ with:
+ images: |
+ ghcr.io/cockatrice/servatrice
+ labels: |
+ org.opencontainers.image.title=Servatrice
+ org.opencontainers.image.url=https://cockatrice.github.io/
+ org.opencontainers.image.description=Server for Cockatrice, a cross-platform virtual tabletop for multiplayer card games
+ annotations: |
+ org.opencontainers.image.title=Servatrice
+ org.opencontainers.image.url=https://cockatrice.github.io/
+ org.opencontainers.image.description=Server for Cockatrice, a cross-platform virtual tabletop for multiplayer card games
+
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Login to GitHub Container Registry
+ if: github.ref_type == 'tag'
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ github.token }}
+
+ - name: Build and push Docker Image
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ platforms: linux/amd64,linux/arm64
+ push: ${{ github.ref_type == 'tag' }}
+ tags: ${{ steps.metadata.outputs.tags }}
+ labels: ${{ steps.metadata.outputs.labels }}
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
diff --git a/Dockerfile b/Dockerfile
index 330c51b4a..2a0d10eba 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -16,7 +16,11 @@ RUN apt-get update && apt-get install -y\
qt6-tools-dev \
qt6-tools-dev-tools
-COPY . /home/servatrice/code/
+COPY ./CMakeLists.txt ./LICENSE ./README.md /home/servatrice/code/
+COPY ./cmake /home/servatrice/code/cmake
+COPY ./common /home/servatrice/code/common
+COPY ./servatrice /home/servatrice/code/servatrice
+
WORKDIR /home/servatrice/code
WORKDIR build
diff --git a/README.md b/README.md
index 0a8058bba..48cacff78 100644
--- a/README.md
+++ b/README.md
@@ -143,7 +143,6 @@ The following flags (with their non-default values) can be passed to `cmake`:
# Run
-
Cockatrice is the game client
Oracle fetches card data
Servatrice is the server
diff --git a/cockatrice/src/client/tabs/api/edhrec/tab_edhrec_main.cpp b/cockatrice/src/client/tabs/api/edhrec/tab_edhrec_main.cpp
index 0bd4e843a..2eed749af 100644
--- a/cockatrice/src/client/tabs/api/edhrec/tab_edhrec_main.cpp
+++ b/cockatrice/src/client/tabs/api/edhrec/tab_edhrec_main.cpp
@@ -356,7 +356,7 @@ void TabEdhRecMain::processAverageDeckResponse(QJsonObject reply)
{
EdhrecAverageDeckApiResponse deckData;
deckData.fromJson(reply);
- tabSupervisor->addVisualDeckEditorTab(deckData.deck.deckLoader);
+ tabSupervisor->openDeckInNewTab(deckData.deck.deckLoader);
}
void TabEdhRecMain::prettyPrintJson(const QJsonValue &value, int indentLevel)
diff --git a/cockatrice/src/client/tabs/tab_supervisor.cpp b/cockatrice/src/client/tabs/tab_supervisor.cpp
index 5d9600d11..4ed0e5a6b 100644
--- a/cockatrice/src/client/tabs/tab_supervisor.cpp
+++ b/cockatrice/src/client/tabs/tab_supervisor.cpp
@@ -590,7 +590,7 @@ void TabSupervisor::actTabDeckStorage(bool checked)
void TabSupervisor::openTabDeckStorage()
{
tabDeckStorage = new TabDeckStorage(this, client, userInfo);
- connect(tabDeckStorage, &TabDeckStorage::openDeckEditor, this, &TabSupervisor::addDeckEditorTab);
+ connect(tabDeckStorage, &TabDeckStorage::openDeckEditor, this, &TabSupervisor::openDeckInNewTab);
myAddTab(tabDeckStorage, aTabDeckStorage);
connect(tabDeckStorage, &Tab::closed, this, [this] {
tabDeckStorage = nullptr;
@@ -691,7 +691,7 @@ void TabSupervisor::gameJoined(const Event_GameJoined &event)
auto *tab = new TabGame(this, QList() << client, event, roomGameTypes);
connect(tab, &TabGame::gameClosing, this, &TabSupervisor::gameLeft);
connect(tab, &TabGame::openMessageDialog, this, &TabSupervisor::addMessageTab);
- connect(tab, &TabGame::openDeckEditor, this, &TabSupervisor::addDeckEditorTab);
+ connect(tab, &TabGame::openDeckEditor, this, &TabSupervisor::openDeckInNewTab);
myAddTab(tab);
gameTabs.insert(event.game_info().game_id(), tab);
setCurrentWidget(tab);
@@ -701,7 +701,7 @@ void TabSupervisor::localGameJoined(const Event_GameJoined &event)
{
auto *tab = new TabGame(this, localClients, event, QMap());
connect(tab, &TabGame::gameClosing, this, &TabSupervisor::gameLeft);
- connect(tab, &TabGame::openDeckEditor, this, &TabSupervisor::addDeckEditorTab);
+ connect(tab, &TabGame::openDeckEditor, this, &TabSupervisor::openDeckInNewTab);
myAddTab(tab);
gameTabs.insert(event.game_info().game_id(), tab);
setCurrentWidget(tab);
@@ -807,6 +807,29 @@ void TabSupervisor::talkLeft(TabMessage *tab)
removeTab(indexOf(tab));
}
+/**
+ * Creates a new deck editor tab and loads the deck into it.
+ * Creates either a classic or visual deck editor tab depending on settings
+ * @param deckToOpen The deck to open in the tab. Creates a copy of the DeckLoader instance.
+ */
+void TabSupervisor::openDeckInNewTab(const DeckLoader *deckToOpen)
+{
+ int type = SettingsCache::instance().getDefaultDeckEditorType();
+ switch (type) {
+ case ClassicDeckEditor:
+ addDeckEditorTab(deckToOpen);
+ break;
+ case VisualDeckEditor:
+ addVisualDeckEditorTab(deckToOpen);
+ break;
+ default:
+ qCWarning(TabSupervisorLog) << "Unknown DeckEditorType [" << type
+ << "]; opening ClassicDeckEditor as fallback";
+ addDeckEditorTab(deckToOpen);
+ break;
+ }
+}
+
/**
* Creates a new deck editor tab
* @param deckToOpen The deck to open in the tab. Creates a copy of the DeckLoader instance.
diff --git a/cockatrice/src/client/tabs/tab_supervisor.h b/cockatrice/src/client/tabs/tab_supervisor.h
index 0ca13ac41..1c367043e 100644
--- a/cockatrice/src/client/tabs/tab_supervisor.h
+++ b/cockatrice/src/client/tabs/tab_supervisor.h
@@ -75,6 +75,14 @@ protected:
class TabSupervisor : public QTabWidget
{
Q_OBJECT
+
+public:
+ enum DeckEditorType
+ {
+ ClassicDeckEditor,
+ VisualDeckEditor
+ };
+
private:
ServerInfo_User *userInfo;
AbstractClient *client;
@@ -152,6 +160,7 @@ signals:
void showWindowIfHidden();
public slots:
+ void openDeckInNewTab(const DeckLoader *deckToOpen);
TabDeckEditor *addDeckEditorTab(const DeckLoader *deckToOpen);
TabDeckEditorVisual *addVisualDeckEditorTab(const DeckLoader *deckToOpen);
TabVisualDatabaseDisplay *addVisualDatabaseDisplayTab();
diff --git a/cockatrice/src/client/tabs/visual_deck_storage/tab_deck_storage_visual.cpp b/cockatrice/src/client/tabs/visual_deck_storage/tab_deck_storage_visual.cpp
index 38af03c59..65b4fcab0 100644
--- a/cockatrice/src/client/tabs/visual_deck_storage/tab_deck_storage_visual.cpp
+++ b/cockatrice/src/client/tabs/visual_deck_storage/tab_deck_storage_visual.cpp
@@ -12,7 +12,7 @@
TabDeckStorageVisual::TabDeckStorageVisual(TabSupervisor *_tabSupervisor)
: Tab(_tabSupervisor), visualDeckStorageWidget(new VisualDeckStorageWidget(this))
{
- connect(this, &TabDeckStorageVisual::openDeckEditor, tabSupervisor, &TabSupervisor::addVisualDeckEditorTab);
+ connect(this, &TabDeckStorageVisual::openDeckEditor, tabSupervisor, &TabSupervisor::openDeckInNewTab);
connect(visualDeckStorageWidget, &VisualDeckStorageWidget::deckLoadRequested, this,
&TabDeckStorageVisual::actOpenLocalDeck);
connect(visualDeckStorageWidget, &VisualDeckStorageWidget::openDeckEditor, this,
diff --git a/cockatrice/src/client/ui/widgets/cards/additional_info/mana_symbol_widget.cpp b/cockatrice/src/client/ui/widgets/cards/additional_info/mana_symbol_widget.cpp
index e42e93444..b17b0ff47 100644
--- a/cockatrice/src/client/ui/widgets/cards/additional_info/mana_symbol_widget.cpp
+++ b/cockatrice/src/client/ui/widgets/cards/additional_info/mana_symbol_widget.cpp
@@ -66,7 +66,7 @@ void ManaSymbolWidget::loadManaIcon()
QString filename = "theme:icons/mana/";
if (symbol == "W" || symbol == "U" || symbol == "B" || symbol == "R" || symbol == "G") {
- filename += symbol + ".svg";
+ filename += symbol;
}
manaIcon = QPixmap(filename);
diff --git a/cockatrice/src/client/ui/widgets/cards/card_info_picture_widget.cpp b/cockatrice/src/client/ui/widgets/cards/card_info_picture_widget.cpp
index 4ee3de5eb..5e275f8e9 100644
--- a/cockatrice/src/client/ui/widgets/cards/card_info_picture_widget.cpp
+++ b/cockatrice/src/client/ui/widgets/cards/card_info_picture_widget.cpp
@@ -64,6 +64,12 @@ CardInfoPictureWidget::CardInfoPictureWidget(QWidget *parent, const bool _hoverT
});
}
+CardInfoPictureWidget::~CardInfoPictureWidget()
+{
+ enlargedPixmapWidget->hide();
+ enlargedPixmapWidget->deleteLater();
+}
+
/**
* @brief Sets the card to be displayed and updates the pixmap.
* @param card A shared pointer to the card information (CardInfoPtr).
@@ -341,6 +347,12 @@ void CardInfoPictureWidget::mousePressEvent(QMouseEvent *event)
emit cardClicked();
}
+void CardInfoPictureWidget::hideEvent(QHideEvent *event)
+{
+ enlargedPixmapWidget->hide();
+ QWidget::hideEvent(event);
+}
+
QMenu *CardInfoPictureWidget::createRightClickMenu()
{
auto *cardMenu = new QMenu(this);
diff --git a/cockatrice/src/client/ui/widgets/cards/card_info_picture_widget.h b/cockatrice/src/client/ui/widgets/cards/card_info_picture_widget.h
index 10cc422aa..79de74526 100644
--- a/cockatrice/src/client/ui/widgets/cards/card_info_picture_widget.h
+++ b/cockatrice/src/client/ui/widgets/cards/card_info_picture_widget.h
@@ -21,6 +21,7 @@ public:
explicit CardInfoPictureWidget(QWidget *parent = nullptr,
bool hoverToZoomEnabled = false,
bool raiseOnEnter = false);
+ ~CardInfoPictureWidget();
CardInfoPtr getInfo()
{
return info;
@@ -52,6 +53,7 @@ protected:
void moveEvent(QMoveEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
+ void hideEvent(QHideEvent *event) override;
void loadPixmap();
[[nodiscard]] const QPixmap &getResizedPixmap() const
{
diff --git a/cockatrice/src/dialogs/dlg_create_token.cpp b/cockatrice/src/dialogs/dlg_create_token.cpp
index 7b3db2773..14adaa48a 100644
--- a/cockatrice/src/dialogs/dlg_create_token.cpp
+++ b/cockatrice/src/dialogs/dlg_create_token.cpp
@@ -63,6 +63,9 @@ DlgCreateToken::DlgCreateToken(const QStringList &_predefinedTokens, QWidget *pa
destroyCheckBox = new QCheckBox(tr("&Destroy token when it leaves the table"));
destroyCheckBox->setChecked(true);
+ faceDownCheckBox = new QCheckBox(tr("Create face-down (Only hides name)"));
+ connect(faceDownCheckBox, &QCheckBox::toggled, this, &DlgCreateToken::faceDownCheckBoxToggled);
+
QGridLayout *grid = new QGridLayout;
grid->addWidget(nameLabel, 0, 0);
grid->addWidget(nameEdit, 0, 1);
@@ -73,6 +76,7 @@ DlgCreateToken::DlgCreateToken(const QStringList &_predefinedTokens, QWidget *pa
grid->addWidget(annotationLabel, 3, 0);
grid->addWidget(annotationEdit, 3, 1);
grid->addWidget(destroyCheckBox, 4, 0, 1, 2);
+ grid->addWidget(faceDownCheckBox, 5, 0, 1, 2);
QGroupBox *tokenDataGroupBox = new QGroupBox(tr("Token data"));
tokenDataGroupBox->setLayout(grid);
@@ -155,6 +159,21 @@ void DlgCreateToken::closeEvent(QCloseEvent *event)
SettingsCache::instance().setTokenDialogGeometry(saveGeometry());
}
+void DlgCreateToken::faceDownCheckBoxToggled(bool checked)
+{
+ if (checked) {
+ colorEdit->setCurrentIndex(6);
+ colorEdit->setEnabled(false);
+ ptEdit->clear();
+ ptEdit->clearFocus();
+ ptEdit->setEnabled(false);
+ } else {
+ colorEdit->setEnabled(true);
+ ptEdit->setEnabled(true);
+ annotationEdit->setEnabled(true);
+ }
+}
+
void DlgCreateToken::tokenSelectionChanged(const QModelIndex ¤t, const QModelIndex & /*previous*/)
{
const QModelIndex realIndex = cardDatabaseDisplayModel->mapToSource(current);
@@ -230,5 +249,6 @@ TokenInfo DlgCreateToken::getTokenInfo() const
.color = colorEdit->itemData(colorEdit->currentIndex()).toString(),
.pt = ptEdit->text(),
.annotation = annotationEdit->text(),
- .destroy = destroyCheckBox->isChecked()};
+ .destroy = destroyCheckBox->isChecked(),
+ .faceDown = faceDownCheckBox->isChecked()};
}
diff --git a/cockatrice/src/dialogs/dlg_create_token.h b/cockatrice/src/dialogs/dlg_create_token.h
index 373e094f1..cd1b48c81 100644
--- a/cockatrice/src/dialogs/dlg_create_token.h
+++ b/cockatrice/src/dialogs/dlg_create_token.h
@@ -24,6 +24,7 @@ struct TokenInfo
QString pt;
QString annotation;
bool destroy = true;
+ bool faceDown = false;
};
class DlgCreateToken : public QDialog
@@ -36,6 +37,7 @@ public:
protected:
void closeEvent(QCloseEvent *event) override;
private slots:
+ void faceDownCheckBoxToggled(bool checked);
void tokenSelectionChanged(const QModelIndex ¤t, const QModelIndex &previous);
void updateSearch(const QString &search);
void actChooseTokenFromAll(bool checked);
@@ -51,6 +53,7 @@ private:
QComboBox *colorEdit;
QLineEdit *nameEdit, *ptEdit, *annotationEdit;
QCheckBox *destroyCheckBox;
+ QCheckBox *faceDownCheckBox;
QRadioButton *chooseTokenFromAllRadioButton, *chooseTokenFromDeckRadioButton;
CardInfoPictureWidget *pic;
QTreeView *chooseTokenView;
diff --git a/cockatrice/src/dialogs/dlg_settings.cpp b/cockatrice/src/dialogs/dlg_settings.cpp
index 502662c14..63901325f 100644
--- a/cockatrice/src/dialogs/dlg_settings.cpp
+++ b/cockatrice/src/dialogs/dlg_settings.cpp
@@ -4,6 +4,7 @@
#include "../client/network/release_channel.h"
#include "../client/network/spoiler_background_updater.h"
#include "../client/sound_engine.h"
+#include "../client/tabs/tab_supervisor.h"
#include "../client/ui/picture_loader/picture_loader.h"
#include "../client/ui/theme_manager.h"
#include "../deck/custom_line_edit.h"
@@ -629,6 +630,10 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage()
connect(&closeEmptyCardViewCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(),
&SettingsCache::setCloseEmptyCardView);
+ focusCardViewSearchBarCheckBox.setChecked(SettingsCache::instance().getFocusCardViewSearchBar());
+ connect(&focusCardViewSearchBarCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(),
+ &SettingsCache::setFocusCardViewSearchBar);
+
annotateTokensCheckBox.setChecked(SettingsCache::instance().getAnnotateTokens());
connect(&annotateTokensCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(),
&SettingsCache::setAnnotateTokens);
@@ -642,8 +647,9 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage()
generalGrid->addWidget(&clickPlaysAllSelectedCheckBox, 1, 0);
generalGrid->addWidget(&playToStackCheckBox, 2, 0);
generalGrid->addWidget(&closeEmptyCardViewCheckBox, 3, 0);
- generalGrid->addWidget(&annotateTokensCheckBox, 4, 0);
- generalGrid->addWidget(&useTearOffMenusCheckBox, 5, 0);
+ generalGrid->addWidget(&focusCardViewSearchBarCheckBox, 4, 0);
+ generalGrid->addWidget(&annotateTokensCheckBox, 5, 0);
+ generalGrid->addWidget(&useTearOffMenusCheckBox, 6, 0);
generalGroupBox = new QGroupBox;
generalGroupBox->setLayout(generalGrid);
@@ -699,12 +705,20 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage()
index == visualDeckStoragePromptForConversionIndexAlways);
});
+ defaultDeckEditorTypeSelector.addItem(""); // these will be set in retranslateUI
+ defaultDeckEditorTypeSelector.addItem("");
+ defaultDeckEditorTypeSelector.setCurrentIndex(SettingsCache::instance().getDefaultDeckEditorType());
+ connect(&defaultDeckEditorTypeSelector, QOverload::of(&QComboBox::currentIndexChanged),
+ &SettingsCache::instance(), &SettingsCache::setDefaultDeckEditorType);
+
auto *deckEditorGrid = new QGridLayout;
deckEditorGrid->addWidget(&openDeckInNewTabCheckBox, 0, 0);
deckEditorGrid->addWidget(&visualDeckStorageInGameCheckBox, 1, 0);
deckEditorGrid->addWidget(&visualDeckStorageSelectionAnimationCheckBox, 2, 0);
deckEditorGrid->addWidget(&visualDeckStoragePromptForConversionLabel, 3, 0);
deckEditorGrid->addWidget(&visualDeckStoragePromptForConversionSelector, 3, 1);
+ deckEditorGrid->addWidget(&defaultDeckEditorTypeLabel, 4, 0);
+ deckEditorGrid->addWidget(&defaultDeckEditorTypeSelector, 4, 1);
deckEditorGroupBox = new QGroupBox;
deckEditorGroupBox->setLayout(deckEditorGrid);
@@ -754,6 +768,7 @@ void UserInterfaceSettingsPage::retranslateUi()
clickPlaysAllSelectedCheckBox.setText(tr("&Clicking plays all selected cards (instead of just the clicked card)"));
playToStackCheckBox.setText(tr("&Play all nonlands onto the stack (not the battlefield) by default"));
closeEmptyCardViewCheckBox.setText(tr("Close card view window when last card is removed"));
+ focusCardViewSearchBarCheckBox.setText(tr("Auto focus search bar when card view window is opened"));
annotateTokensCheckBox.setText(tr("Annotate card text on tokens"));
useTearOffMenusCheckBox.setText(tr("Use tear-off menus, allowing right click menus to persist on screen"));
notificationsGroupBox->setTitle(tr("Notifications settings"));
@@ -774,6 +789,9 @@ void UserInterfaceSettingsPage::retranslateUi()
tr("ask to convert to .cod"));
visualDeckStoragePromptForConversionSelector.setItemText(visualDeckStoragePromptForConversionIndexAlways,
tr("always convert to .cod"));
+ defaultDeckEditorTypeLabel.setText(tr("Default deck editor type"));
+ defaultDeckEditorTypeSelector.setItemText(TabSupervisor::ClassicDeckEditor, tr("Classic Deck Editor"));
+ defaultDeckEditorTypeSelector.setItemText(TabSupervisor::VisualDeckEditor, tr("Visual Deck Editor"));
replayGroupBox->setTitle(tr("Replay settings"));
rewindBufferingMsLabel.setText(tr("Buffer time for backwards skip via shortcut:"));
rewindBufferingMsBox.setSuffix(" ms");
diff --git a/cockatrice/src/dialogs/dlg_settings.h b/cockatrice/src/dialogs/dlg_settings.h
index 6c9b8b062..54b2cbcd8 100644
--- a/cockatrice/src/dialogs/dlg_settings.h
+++ b/cockatrice/src/dialogs/dlg_settings.h
@@ -147,6 +147,7 @@ private:
QCheckBox clickPlaysAllSelectedCheckBox;
QCheckBox playToStackCheckBox;
QCheckBox closeEmptyCardViewCheckBox;
+ QCheckBox focusCardViewSearchBarCheckBox;
QCheckBox annotateTokensCheckBox;
QCheckBox useTearOffMenusCheckBox;
QCheckBox tapAnimationCheckBox;
@@ -155,6 +156,8 @@ private:
QComboBox visualDeckStoragePromptForConversionSelector;
QCheckBox visualDeckStorageInGameCheckBox;
QCheckBox visualDeckStorageSelectionAnimationCheckBox;
+ QLabel defaultDeckEditorTypeLabel;
+ QComboBox defaultDeckEditorTypeSelector;
QLabel rewindBufferingMsLabel;
QSpinBox rewindBufferingMsBox;
QGroupBox *generalGroupBox;
diff --git a/cockatrice/src/dialogs/dlg_view_log.cpp b/cockatrice/src/dialogs/dlg_view_log.cpp
index dc569c92c..b60a9767b 100644
--- a/cockatrice/src/dialogs/dlg_view_log.cpp
+++ b/cockatrice/src/dialogs/dlg_view_log.cpp
@@ -3,23 +3,40 @@
#include "../settings/cache_settings.h"
#include "../utility/logger.h"
+#include
#include
+#include
+#include
#include
DlgViewLog::DlgViewLog(QWidget *parent) : QDialog(parent)
{
-
logArea = new QPlainTextEdit;
logArea->setReadOnly(true);
auto *mainLayout = new QVBoxLayout;
+ mainLayout->setSpacing(3);
+ mainLayout->setContentsMargins(20, 20, 20, 6);
+
mainLayout->addWidget(logArea);
+ auto *bottomLayout = new QHBoxLayout;
+
coClearLog = new QCheckBox;
coClearLog->setText(tr("Clear log when closing"));
coClearLog->setChecked(SettingsCache::instance().servers().getClearDebugLogStatus(false));
connect(coClearLog, &QCheckBox::toggled, this, &DlgViewLog::actCheckBoxChanged);
- mainLayout->addWidget(coClearLog);
+
+ copyToClipboardButton = new QPushButton;
+ copyToClipboardButton->setText(tr("Copy to clipboard"));
+ copyToClipboardButton->setAutoDefault(false);
+ connect(copyToClipboardButton, &QPushButton::clicked, this, &DlgViewLog::actCopyToClipboard);
+
+ bottomLayout->addWidget(coClearLog);
+ bottomLayout->addStretch();
+ bottomLayout->addWidget(copyToClipboardButton);
+
+ mainLayout->addLayout(bottomLayout);
setLayout(mainLayout);
@@ -27,7 +44,7 @@ DlgViewLog::DlgViewLog(QWidget *parent) : QDialog(parent)
resize(800, 500);
loadInitialLogBuffer();
- connect(&Logger::getInstance(), &Logger::logEntryAdded, this, &DlgViewLog::logEntryAdded);
+ connect(&Logger::getInstance(), &Logger::logEntryAdded, this, &DlgViewLog::appendLogEntry);
}
void DlgViewLog::actCheckBoxChanged(bool abNewValue)
@@ -35,16 +52,26 @@ void DlgViewLog::actCheckBoxChanged(bool abNewValue)
SettingsCache::instance().servers().setClearDebugLogStatus(abNewValue);
}
+void DlgViewLog::actCopyToClipboard()
+{
+ QApplication::clipboard()->setText(logArea->toPlainText());
+}
+
void DlgViewLog::loadInitialLogBuffer()
{
QList logBuffer = Logger::getInstance().getLogBuffer();
for (const QString &message : logBuffer)
- logEntryAdded(message);
+ appendLogEntry(message);
}
-void DlgViewLog::logEntryAdded(QString message)
+void DlgViewLog::appendLogEntry(const QString &message)
{
- logArea->appendPlainText(message);
+ static auto colorEscapeCodePattern = QRegularExpression("\033\\[\\d+m");
+
+ QString sanitizedMessage = message;
+ sanitizedMessage.replace(colorEscapeCodePattern, "");
+
+ logArea->appendPlainText(sanitizedMessage);
}
void DlgViewLog::closeEvent(QCloseEvent * /* event */)
diff --git a/cockatrice/src/dialogs/dlg_view_log.h b/cockatrice/src/dialogs/dlg_view_log.h
index 3845f0062..26d6ad5e1 100644
--- a/cockatrice/src/dialogs/dlg_view_log.h
+++ b/cockatrice/src/dialogs/dlg_view_log.h
@@ -19,11 +19,13 @@ protected:
private:
QPlainTextEdit *logArea;
QCheckBox *coClearLog;
+ QPushButton *copyToClipboardButton;
void loadInitialLogBuffer();
private slots:
- void logEntryAdded(QString message);
+ void appendLogEntry(const QString &message);
void actCheckBoxChanged(bool abNewValue);
+ void actCopyToClipboard();
};
#endif
\ No newline at end of file
diff --git a/cockatrice/src/game/board/abstract_card_drag_item.cpp b/cockatrice/src/game/board/abstract_card_drag_item.cpp
index 0c7acd917..a7234c3d4 100644
--- a/cockatrice/src/game/board/abstract_card_drag_item.cpp
+++ b/cockatrice/src/game/board/abstract_card_drag_item.cpp
@@ -39,6 +39,8 @@ AbstractCardDragItem::AbstractCardDragItem(AbstractCardItem *_item,
prepareGeometryChange();
update();
});
+
+ connect(item, &QObject::destroyed, this, &AbstractCardDragItem::deleteLater);
}
AbstractCardDragItem::~AbstractCardDragItem()
diff --git a/cockatrice/src/game/board/card_drag_item.cpp b/cockatrice/src/game/board/card_drag_item.cpp
index d21e9168b..ab4431958 100644
--- a/cockatrice/src/game/board/card_drag_item.cpp
+++ b/cockatrice/src/game/board/card_drag_item.cpp
@@ -47,10 +47,25 @@ void CardDragItem::updatePosition(const QPointF &cursorScenePos)
cursorZone = zoneViewZone;
else if (cardZone)
cursorZone = cardZone;
- if (!cursorZone)
- return;
+
+ // Always update the current zone, even if its null, to cancel the drag
+ // instead of dropping cards into an non-intuitive location.
currentZone = cursorZone;
+ if (!cursorZone) {
+ // Avoid the cards getting stuck visually when not over
+ // any zone.
+ QPointF newPos = cursorScenePos - hotSpot;
+
+ if (newPos != pos()) {
+ for (int i = 0; i < childDrags.size(); i++)
+ childDrags[i]->setPos(newPos + childDrags[i]->getHotSpot());
+ setPos(newPos);
+ }
+
+ return;
+ }
+
QPointF zonePos = currentZone->scenePos();
QPointF cursorPosInZone = cursorScenePos - zonePos;
diff --git a/cockatrice/src/game/player/player.cpp b/cockatrice/src/game/player/player.cpp
index 235c4a734..9fff6b3df 100644
--- a/cockatrice/src/game/player/player.cpp
+++ b/cockatrice/src/game/player/player.cpp
@@ -1864,6 +1864,7 @@ void Player::actCreateAnotherToken()
cmd.set_pt(lastTokenInfo.pt.toStdString());
cmd.set_annotation(lastTokenInfo.annotation.toStdString());
cmd.set_destroy_on_zone_change(lastTokenInfo.destroy);
+ cmd.set_face_down(lastTokenInfo.faceDown);
cmd.set_x(-1);
cmd.set_y(lastTokenTableRow);
@@ -2075,6 +2076,8 @@ void Player::createCard(const CardItem *sourceCard,
break;
case CardRelation::TransformInto:
+ // allow cards to directly transform on stack
+ cmd.set_zone(sourceCard->getZone()->getName() == "stack" ? "stack" : "table");
// Transform card zone changes are handled server-side
cmd.set_target_zone(sourceCard->getZone()->getName().toStdString());
cmd.set_target_card_id(sourceCard->getId());
@@ -2240,10 +2243,10 @@ void Player::eventCreateToken(const Event_CreateToken &event)
CardItem *card = new CardItem(this, nullptr, QString::fromStdString(event.card_name()),
QString::fromStdString(event.card_provider_id()), event.card_id());
- // use db PT if not provided in event
+ // use db PT if not provided in event and not face-down
if (!QString::fromStdString(event.pt()).isEmpty()) {
card->setPT(QString::fromStdString(event.pt()));
- } else {
+ } else if (!event.face_down()) {
CardInfoPtr dbCard = card->getInfo();
if (dbCard) {
card->setPT(dbCard->getPowTough());
@@ -2252,8 +2255,9 @@ void Player::eventCreateToken(const Event_CreateToken &event)
card->setColor(QString::fromStdString(event.color()));
card->setAnnotation(QString::fromStdString(event.annotation()));
card->setDestroyOnZoneChange(event.destroy_on_zone_change());
+ card->setFaceDown(event.face_down());
- emit logCreateToken(this, card->getName(), card->getPT());
+ emit logCreateToken(this, card->getName(), card->getPT(), card->getFaceDown());
zone->addCard(card, true, event.x(), event.y());
}
diff --git a/cockatrice/src/game/player/player.h b/cockatrice/src/game/player/player.h
index 9bd59fca0..d802ef796 100644
--- a/cockatrice/src/game/player/player.h
+++ b/cockatrice/src/game/player/player.h
@@ -128,7 +128,7 @@ signals:
Player *targetPlayer,
QString targetCard,
bool _playerTarget);
- void logCreateToken(Player *player, QString cardName, QString pt);
+ void logCreateToken(Player *player, QString cardName, QString pt, bool faceDown);
void logDrawCards(Player *player, int number, bool deckIsEmpty);
void logUndoDraw(Player *player, QString cardName);
void logMoveCard(Player *player, CardItem *card, CardZone *startZone, int oldX, CardZone *targetZone, int newX);
diff --git a/cockatrice/src/game/zones/view_zone_widget.cpp b/cockatrice/src/game/zones/view_zone_widget.cpp
index 1721831ac..f0430f549 100644
--- a/cockatrice/src/game/zones/view_zone_widget.cpp
+++ b/cockatrice/src/game/zones/view_zone_widget.cpp
@@ -56,6 +56,11 @@ ZoneViewWidget::ZoneViewWidget(Player *_player,
connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(&searchEdit); });
+ if (SettingsCache::instance().getFocusCardViewSearchBar()) {
+ this->setActive(true);
+ searchEdit.setFocus();
+ }
+
QGraphicsProxyWidget *searchEditProxy = new QGraphicsProxyWidget;
searchEditProxy->setWidget(&searchEdit);
searchEditProxy->setZValue(2000000007);
diff --git a/cockatrice/src/server/message_log_widget.cpp b/cockatrice/src/server/message_log_widget.cpp
index cabc86733..a61a7ba2c 100644
--- a/cockatrice/src/server/message_log_widget.cpp
+++ b/cockatrice/src/server/message_log_widget.cpp
@@ -216,12 +216,16 @@ void MessageLogWidget::logCreateArrow(Player *player,
}
}
-void MessageLogWidget::logCreateToken(Player *player, QString cardName, QString pt)
+void MessageLogWidget::logCreateToken(Player *player, QString cardName, QString pt, bool faceDown)
{
- appendHtmlServerMessage(tr("%1 creates token: %2%3.")
- .arg(sanitizeHtml(player->getName()))
- .arg(cardLink(std::move(cardName)))
- .arg(pt.isEmpty() ? QString() : QString(" (%1)").arg(sanitizeHtml(pt))));
+ if (faceDown) {
+ appendHtmlServerMessage(tr("%1 creates a face down token.").arg(sanitizeHtml(player->getName())));
+ } else {
+ appendHtmlServerMessage(tr("%1 creates token: %2%3.")
+ .arg(sanitizeHtml(player->getName()))
+ .arg(cardLink(std::move(cardName)))
+ .arg(pt.isEmpty() ? QString() : QString(" (%1)").arg(sanitizeHtml(pt))));
+ }
}
void MessageLogWidget::logDeckSelect(Player *player, QString deckHash, int sideboardSize)
diff --git a/cockatrice/src/server/message_log_widget.h b/cockatrice/src/server/message_log_widget.h
index f6780b729..21b03dee9 100644
--- a/cockatrice/src/server/message_log_widget.h
+++ b/cockatrice/src/server/message_log_widget.h
@@ -41,7 +41,7 @@ public slots:
Player *targetPlayer,
QString targetCard,
bool playerTarget);
- void logCreateToken(Player *player, QString cardName, QString pt);
+ void logCreateToken(Player *player, QString cardName, QString pt, bool faceDown);
void logDeckSelect(Player *player, QString deckHash, int sideboardSize);
void logDestroyCard(Player *player, QString cardName);
void logDrawCards(Player *player, int number, bool deckIsEmpty);
diff --git a/cockatrice/src/settings/cache_settings.cpp b/cockatrice/src/settings/cache_settings.cpp
index ba25590f3..6507780fe 100644
--- a/cockatrice/src/settings/cache_settings.cpp
+++ b/cockatrice/src/settings/cache_settings.cpp
@@ -251,6 +251,7 @@ SettingsCache::SettingsCache()
cardViewInitialRowsMax = settings->value("interface/cardViewInitialRowsMax", 14).toInt();
cardViewExpandedRowsMax = settings->value("interface/cardViewExpandedRowsMax", 20).toInt();
closeEmptyCardView = settings->value("interface/closeEmptyCardView", true).toBool();
+ focusCardViewSearchBar = settings->value("interface/focusCardViewSearchBar", true).toBool();
showShortcuts = settings->value("menu/showshortcuts", true).toBool();
displayCardNames = settings->value("cards/displaycardnames", true).toBool();
@@ -285,6 +286,7 @@ SettingsCache::SettingsCache()
visualDeckStorageInGame = settings->value("interface/visualdeckstorageingame", true).toBool();
visualDeckStorageSelectionAnimation =
settings->value("interface/visualdeckstorageselectionanimation", true).toBool();
+ defaultDeckEditorType = settings->value("interface/defaultDeckEditorType", 1).toInt();
visualDatabaseDisplayFilterToMostRecentSetsEnabled =
settings->value("interface/visualdatabasedisplayfiltertomostrecentsetsenabled", true).toBool();
visualDatabaseDisplayFilterToMostRecentSetsAmount =
@@ -369,6 +371,12 @@ void SettingsCache::setCloseEmptyCardView(QT_STATE_CHANGED_T value)
settings->setValue("interface/closeEmptyCardView", closeEmptyCardView);
}
+void SettingsCache::setFocusCardViewSearchBar(QT_STATE_CHANGED_T value)
+{
+ focusCardViewSearchBar = value;
+ settings->setValue("interface/focusCardViewSearchBar", focusCardViewSearchBar);
+}
+
void SettingsCache::setKnownMissingFeatures(const QString &_knownMissingFeatures)
{
knownMissingFeatures = _knownMissingFeatures;
@@ -791,6 +799,12 @@ void SettingsCache::setVisualDeckStorageSelectionAnimation(QT_STATE_CHANGED_T va
emit visualDeckStorageSelectionAnimationChanged(visualDeckStorageSelectionAnimation);
}
+void SettingsCache::setDefaultDeckEditorType(int value)
+{
+ defaultDeckEditorType = value;
+ settings->setValue("interface/defaultDeckEditorType", defaultDeckEditorType);
+}
+
void SettingsCache::setVisualDatabaseDisplayFilterToMostRecentSetsEnabled(QT_STATE_CHANGED_T _enabled)
{
visualDatabaseDisplayFilterToMostRecentSetsEnabled = _enabled;
diff --git a/cockatrice/src/settings/cache_settings.h b/cockatrice/src/settings/cache_settings.h
index 78993ce4a..bb4bed95f 100644
--- a/cockatrice/src/settings/cache_settings.h
+++ b/cockatrice/src/settings/cache_settings.h
@@ -153,6 +153,7 @@ private:
bool visualDeckStorageAlwaysConvert;
bool visualDeckStorageInGame;
bool visualDeckStorageSelectionAnimation;
+ int defaultDeckEditorType;
bool visualDatabaseDisplayFilterToMostRecentSetsEnabled;
int visualDatabaseDisplayFilterToMostRecentSetsAmount;
bool horizontalHand;
@@ -183,6 +184,7 @@ private:
int cardViewInitialRowsMax;
int cardViewExpandedRowsMax;
bool closeEmptyCardView;
+ bool focusCardViewSearchBar;
int pixmapCacheSize;
int networkCacheSize;
int redirectCacheTtl;
@@ -488,6 +490,10 @@ public:
{
return visualDeckStorageSelectionAnimation;
}
+ int getDefaultDeckEditorType() const
+ {
+ return defaultDeckEditorType;
+ }
bool getVisualDatabaseDisplayFilterToMostRecentSetsEnabled() const
{
return visualDatabaseDisplayFilterToMostRecentSetsEnabled;
@@ -694,6 +700,7 @@ public:
void setCardViewInitialRowsMax(int _cardViewInitialRowsMax);
void setCardViewExpandedRowsMax(int value);
void setCloseEmptyCardView(QT_STATE_CHANGED_T value);
+ void setFocusCardViewSearchBar(QT_STATE_CHANGED_T value);
QString getClientID()
{
return clientID;
@@ -722,6 +729,10 @@ public:
{
return closeEmptyCardView;
}
+ bool getFocusCardViewSearchBar() const
+ {
+ return focusCardViewSearchBar;
+ }
ShortcutsSettings &shortcuts() const
{
return *shortcutsSettings;
@@ -839,6 +850,7 @@ public slots:
void setVisualDeckStorageAlwaysConvert(bool _visualDeckStorageAlwaysConvert);
void setVisualDeckStorageInGame(QT_STATE_CHANGED_T value);
void setVisualDeckStorageSelectionAnimation(QT_STATE_CHANGED_T value);
+ void setDefaultDeckEditorType(int value);
void setVisualDatabaseDisplayFilterToMostRecentSetsEnabled(QT_STATE_CHANGED_T _enabled);
void setVisualDatabaseDisplayFilterToMostRecentSetsAmount(int _amount);
void setHorizontalHand(QT_STATE_CHANGED_T _horizontalHand);
diff --git a/cockatrice/src/utility/logger.cpp b/cockatrice/src/utility/logger.cpp
index 74ed90597..0184146ae 100644
--- a/cockatrice/src/utility/logger.cpp
+++ b/cockatrice/src/utility/logger.cpp
@@ -81,12 +81,12 @@ void Logger::closeLogfileSession()
fileHandle.close();
}
-void Logger::log(QtMsgType /* type */, const QMessageLogContext & /* ctx */, const QString message)
+void Logger::log(QtMsgType /* type */, const QMessageLogContext & /* ctx */, const QString &message)
{
QMetaObject::invokeMethod(this, "internalLog", Qt::QueuedConnection, Q_ARG(const QString &, message));
}
-void Logger::internalLog(QString message)
+void Logger::internalLog(const QString &message)
{
QMutexLocker locker(&mutex);
diff --git a/cockatrice/src/utility/logger.h b/cockatrice/src/utility/logger.h
index 246e9204a..b90644b36 100644
--- a/cockatrice/src/utility/logger.h
+++ b/cockatrice/src/utility/logger.h
@@ -28,7 +28,7 @@ public:
}
void logToFile(bool enabled);
- void log(QtMsgType type, const QMessageLogContext &ctx, QString message);
+ void log(QtMsgType type, const QMessageLogContext &ctx, const QString &message);
QString getClientVersion();
QString getClientOperatingSystem();
QString getSystemArchitecture();
@@ -57,10 +57,10 @@ protected:
void closeLogfileSession();
protected slots:
- void internalLog(QString message);
+ void internalLog(const QString &message);
signals:
- void logEntryAdded(QString message);
+ void logEntryAdded(const QString &message);
};
#endif
diff --git a/cockatrice/translations/cockatrice_it.ts b/cockatrice/translations/cockatrice_it.ts
index f310b5842..7cdce9896 100644
--- a/cockatrice/translations/cockatrice_it.ts
+++ b/cockatrice/translations/cockatrice_it.ts
@@ -203,7 +203,7 @@ Controlla se la cartella è valida e prova ancora.
Minimum overlap percentage of cards on the stack and in vertical hand
- Sovrapposizione % minima delle carte in pila e nella mano verticale
+ Sovrapposizione % minima delle carte in pila e nella mano verticale:
@@ -2770,13 +2770,13 @@ Per favore, visitate la pagina di download per aggiornare manualmente.
Released
- Rilasciato
+ Data di rilascio
Changelog
- Cambiamenti della versione
+ Changelog
@@ -2789,13 +2789,14 @@ Per favore, visitate la pagina di download per aggiornare manualmente.
Unfortunately there are no download packages available for your operating system.
You may have to build from source yourself.
-
+ Purtroppo l'aggiornamento automatico non è riuscito a trovare una versione compatibile.
+Dovrai scaricare la nuova versione manualmente.
Please check the <a href="%1">releases page</a> on our Github and download the build for your system.
Please check the download page manually and visit the wiki for instructions on compiling.
-
+ Per favore, controlla la <a href="%1">pagina delle release</a> sul nostro Github e scarica la versione giusta per il tuo sistema.
@@ -3668,7 +3669,7 @@ La tua versione è la %1, la versione online è la %2.
Start &local game...
- Inizia &partita in locale...
+ Inizia partita in &locale...
@@ -3688,7 +3689,7 @@ La tua versione è la %1, la versione online è la %2.
&Restore password...
- &Recupera password...
+ Recupera &password...
@@ -3713,7 +3714,7 @@ La tua versione è la %1, la versione online è la %2.
C&ard Database
- &Database delle carte
+ &Database carte
@@ -3916,7 +3917,7 @@ Scopri metodi alternativi per visualizzare i set o disabilitare set ed effetti n
An error occurred when attempting to write to the process. For example, the process may not be running, or it may have closed its input channel.
-
+ Si è verificato un errore nel cercare di scrivere al processo. Ad esempio, il processo potrebbe non essere in esecuzione, o potrebbe aver chiuso il suo canale di input.
@@ -5850,7 +5851,7 @@ Il database delle carte verrà ricaricato.
Time started
- Inizio tempo
+ Inizio
@@ -7122,7 +7123,7 @@ Più informazioni inserisci, più specifici saranno i risultati.
Deck Editor
- Editor dei mazzi
+ &Editor dei mazzi
@@ -7142,12 +7143,12 @@ Più informazioni inserisci, più specifici saranno i risultati.
Deck Storage
- Archivio mazzi
+ &Archivio mazzi
Game Replays
- Replay partite
+ &Replay partite
@@ -8299,12 +8300,12 @@ Se pregato di evitare di continuare questa attività o potrebbero venire presi u
Analyze Deck (deckstats.net)
Analyze Deck
- Analizza Mazzo (deckstats.net)
+ Analizza mazzo (deckstats.net)
Analyze Deck (tappedout.net)
- Analizza Mazzo (tappedout.net)
+ Analizza mazzo (tappedout.net)
@@ -8496,7 +8497,7 @@ Se pregato di evitare di continuare questa attività o potrebbero venire presi u
Set Red Counters...
- Imposta Contatore Rosso...
+ Imposta Segnalino Rosso...
diff --git a/common/pb/command_create_token.proto b/common/pb/command_create_token.proto
index 671ce3fe3..4b7d11098 100644
--- a/common/pb/command_create_token.proto
+++ b/common/pb/command_create_token.proto
@@ -27,4 +27,5 @@ message Command_CreateToken {
optional TargetMode target_mode = 11;
optional string card_provider_id = 12;
+ optional bool face_down = 13;
}
diff --git a/common/pb/event_create_token.proto b/common/pb/event_create_token.proto
index bc88a744c..6947b6048 100644
--- a/common/pb/event_create_token.proto
+++ b/common/pb/event_create_token.proto
@@ -15,4 +15,5 @@ message Event_CreateToken {
optional sint32 x = 8;
optional sint32 y = 9;
optional string card_provider_id = 10;
+ optional bool face_down = 11;
}
diff --git a/common/server_player.cpp b/common/server_player.cpp
index 60ebfa3c0..01f6947fa 100644
--- a/common/server_player.cpp
+++ b/common/server_player.cpp
@@ -386,13 +386,23 @@ void Server_Player::revealTopCardIfNeeded(Server_CardZone *zone, GameEventStorag
}
}
-static Event_CreateToken makeCreateTokenEvent(Server_CardZone *zone, Server_Card *card, int xCoord, int yCoord)
+/**
+ * Creates the create token event.
+ * By default, will set event's name and color fields to empty if the token is face-down
+ */
+static Event_CreateToken
+makeCreateTokenEvent(Server_CardZone *zone, Server_Card *card, int xCoord, int yCoord, bool revealFacedownInfo = false)
{
Event_CreateToken event;
event.set_zone_name(zone->getName().toStdString());
event.set_card_id(card->getId());
- event.set_card_name(card->getName().toStdString());
- event.set_card_provider_id(card->getProviderId().toStdString());
+ event.set_face_down(card->getFaceDown());
+
+ if (!card->getFaceDown() || revealFacedownInfo) {
+ event.set_card_name(card->getName().toStdString());
+ event.set_card_provider_id(card->getProviderId().toStdString());
+ }
+
event.set_color(card->getColor().toStdString());
event.set_pt(card->getPT().toStdString());
event.set_annotation(card->getAnnotation().toStdString());
@@ -401,7 +411,6 @@ static Event_CreateToken makeCreateTokenEvent(Server_CardZone *zone, Server_Card
event.set_y(yCoord);
return event;
}
-
static Event_AttachCard makeAttachCardEvent(Server_Card *attachedCard, Server_Card *parentCard = nullptr)
{
Event_AttachCard event;
@@ -417,6 +426,29 @@ static Event_AttachCard makeAttachCardEvent(Server_Card *attachedCard, Server_Ca
return event;
}
+/**
+ * Determines whether moving the card from startZone to targetZone should cause the card to be destroyed.
+ */
+static bool
+shouldDestroyOnMove(const Server_Card *card, const Server_CardZone *startZone, const Server_CardZone *targetZone)
+{
+ if (!card->getDestroyOnZoneChange()) {
+ return false;
+ }
+
+ if (startZone->getName() == targetZone->getName()) {
+ return false;
+ }
+
+ // Allow tokens on the stack
+ if ((startZone->getName() == "table" || startZone->getName() == "stack") &&
+ (targetZone->getName() == "table" || targetZone->getName() == "stack")) {
+ return false;
+ }
+
+ return true;
+}
+
Response::ResponseCode Server_Player::moveCard(GameEventStorage &ges,
Server_CardZone *startzone,
const QList &_cards,
@@ -523,7 +555,7 @@ Response::ResponseCode Server_Player::moveCard(GameEventStorage &ges,
}
}
- if (card->getDestroyOnZoneChange() && (startzone->getName() != targetzone->getName())) {
+ if (shouldDestroyOnMove(card, startzone, targetzone)) {
Event_DestroyCard event;
event.set_zone_name(startzone->getName().toStdString());
event.set_card_id(static_cast(card->getId()));
@@ -1471,7 +1503,8 @@ Server_Player::cmdCreateToken(const Command_CreateToken &cmd, ResponseContainer
const QString cardName = nameFromStdString(cmd.card_name());
const QString cardProviderId = nameFromStdString(cmd.card_provider_id());
if (zone->hasCoords()) {
- xCoord = zone->getFreeGridColumn(xCoord, yCoord, cardName, false);
+ bool dontStackSameName = cmd.face_down();
+ xCoord = zone->getFreeGridColumn(xCoord, yCoord, cardName, dontStackSameName);
}
if (xCoord < 0) {
xCoord = 0;
@@ -1482,13 +1515,17 @@ Server_Player::cmdCreateToken(const Command_CreateToken &cmd, ResponseContainer
auto *card = new Server_Card(cardName, cardProviderId, newCardId(), xCoord, yCoord);
card->moveToThread(thread());
- card->setPT(nameFromStdString(cmd.pt()));
- card->setColor(nameFromStdString(cmd.color()));
+ // Client should already prevent face-down tokens from having attributes; this just an extra server-side check
+ if (!cmd.face_down()) {
+ card->setColor(nameFromStdString(cmd.color()));
+ card->setPT(nameFromStdString(cmd.pt()));
+ }
card->setAnnotation(nameFromStdString(cmd.annotation()));
card->setDestroyOnZoneChange(cmd.destroy_on_zone_change());
+ card->setFaceDown(cmd.face_down());
zone->insertCard(card, xCoord, yCoord);
- ges.enqueueGameEvent(makeCreateTokenEvent(zone, card, xCoord, yCoord), playerId);
+ sendCreateTokenEvents(zone, card, xCoord, yCoord, ges);
// check if the token is a replacement for an existing card
if (!targetCard) {
@@ -1619,6 +1656,38 @@ Server_Player::cmdCreateToken(const Command_CreateToken &cmd, ResponseContainer
return Response::RespOk;
}
+/**
+ * Creates and sends the events required to properly communicate the given token creation.
+ * Primarily written to handle creating face-down tokens.
+ */
+void Server_Player::sendCreateTokenEvents(Server_CardZone *zone,
+ Server_Card *card,
+ int xCoord,
+ int yCoord,
+ GameEventStorage &ges)
+{
+ // Token is not face-down; things are easy
+ if (!card->getFaceDown()) {
+ ges.enqueueGameEvent(makeCreateTokenEvent(zone, card, xCoord, yCoord), playerId);
+ return;
+ }
+
+ // Token is face-down. We have to send different info to each player
+ auto eventOthers = makeCreateTokenEvent(zone, card, xCoord, yCoord, false);
+ ges.enqueueGameEvent(eventOthers, playerId, GameEventStorageItem::SendToOthers);
+
+ auto eventPrivate = makeCreateTokenEvent(zone, card, xCoord, yCoord, true);
+ ges.enqueueGameEvent(eventPrivate, playerId, GameEventStorageItem::SendToPrivate, playerId);
+
+ // Event_CreateToken didn't use to have face_down field; send attribute event afterward for backwards compatibility
+ Event_SetCardAttr event;
+ event.set_zone_name(zone->getName().toStdString());
+ event.set_card_id(card->getId());
+ event.set_attribute(AttrFaceDown);
+ event.set_attr_value("1");
+ ges.enqueueGameEvent(event, playerId);
+}
+
Response::ResponseCode
Server_Player::cmdCreateArrow(const Command_CreateArrow &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges)
{
diff --git a/common/server_player.h b/common/server_player.h
index ffa12f8b0..b9d414482 100644
--- a/common/server_player.h
+++ b/common/server_player.h
@@ -84,6 +84,7 @@ private:
bool conceded;
bool sideboardLocked;
void revealTopCardIfNeeded(Server_CardZone *zone, GameEventStorage &ges);
+ void sendCreateTokenEvents(Server_CardZone *zone, Server_Card *card, int xCoord, int yCoord, GameEventStorage &ges);
public:
mutable QMutex playerMutex;
diff --git a/dbconverter/src/mocks.cpp b/dbconverter/src/mocks.cpp
index 93048d3cf..890294870 100644
--- a/dbconverter/src/mocks.cpp
+++ b/dbconverter/src/mocks.cpp
@@ -64,6 +64,9 @@ void SettingsCache::setCardViewExpandedRowsMax(int /* value */)
void SettingsCache::setCloseEmptyCardView(const QT_STATE_CHANGED_T /* value */)
{
}
+void SettingsCache::setFocusCardViewSearchBar(QT_STATE_CHANGED_T /* value */)
+{
+}
void SettingsCache::setKnownMissingFeatures(const QString & /* _knownMissingFeatures */)
{
}
@@ -256,6 +259,9 @@ void SettingsCache::setVisualDeckStorageInGame(QT_STATE_CHANGED_T /* value */)
void SettingsCache::setVisualDeckStorageSelectionAnimation(QT_STATE_CHANGED_T /* value */)
{
}
+void SettingsCache::setDefaultDeckEditorType(int /* value */)
+{
+}
void SettingsCache::setVisualDatabaseDisplayFilterToMostRecentSetsEnabled(QT_STATE_CHANGED_T /* _enabled */)
{
}
diff --git a/tests/carddatabase/mocks.cpp b/tests/carddatabase/mocks.cpp
index a2ca73fb1..b34c28727 100644
--- a/tests/carddatabase/mocks.cpp
+++ b/tests/carddatabase/mocks.cpp
@@ -68,6 +68,9 @@ void SettingsCache::setCardViewExpandedRowsMax(int /* value */)
void SettingsCache::setCloseEmptyCardView(QT_STATE_CHANGED_T /* value */)
{
}
+void SettingsCache::setFocusCardViewSearchBar(QT_STATE_CHANGED_T /* value */)
+{
+}
void SettingsCache::setKnownMissingFeatures(const QString & /* _knownMissingFeatures */)
{
}
@@ -260,6 +263,9 @@ void SettingsCache::setVisualDeckStorageInGame(QT_STATE_CHANGED_T /* value */)
void SettingsCache::setVisualDeckStorageSelectionAnimation(QT_STATE_CHANGED_T /* value */)
{
}
+void SettingsCache::setDefaultDeckEditorType(int /* value */)
+{
+}
void SettingsCache::setVisualDatabaseDisplayFilterToMostRecentSetsEnabled(QT_STATE_CHANGED_T /* _enabled */)
{
}
diff --git a/webclient/package-lock.json b/webclient/package-lock.json
index f5c2ea22e..fa93ceb06 100644
--- a/webclient/package-lock.json
+++ b/webclient/package-lock.json
@@ -17962,9 +17962,10 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/serialize-javascript": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
- "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==",
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
+ "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
+ "license": "BSD-3-Clause",
"dependencies": {
"randombytes": "^2.1.0"
}
@@ -32825,9 +32826,9 @@
}
},
"serialize-javascript": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
- "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==",
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
+ "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
"requires": {
"randombytes": "^2.1.0"
}