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 FossaDebian 12 BookwormDebian 11 Bullseye + • Fedora 42Fedora 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" }