From 77978c7178023db33820240f1522c6353e69bf4b Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Sun, 19 Apr 2026 01:03:49 +0200 Subject: [PATCH 01/13] stop prepending the adventure side (#6819) the adventure side of adventure cards used to be the first entry in mtgjson, at some point this changed to the second entry, better reflecting its secondary nature, however cockatrice has hardcoded the treatment of the card to assume the second part was the "main" part, when implementing the treatment of prepare layout cards (#6792) I copied the behavior over which was then already incorrect, this pr removes the special treatment of this card layout bringing the result back in line with expectation: the main part of the card provides the main type used in cockatrice for sorting and behavior --- oracle/src/oracleimporter.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/oracle/src/oracleimporter.cpp b/oracle/src/oracleimporter.cpp index b5d7b9856..578afd98d 100644 --- a/oracle/src/oracleimporter.cpp +++ b/oracle/src/oracleimporter.cpp @@ -366,8 +366,6 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList auto found_iter = splitCards.find(name + numProperty); if (found_iter == splitCards.end()) { splitCards.insert(name + numProperty, {{split}, name}); - } else if (layout == "adventure" || layout == "prepare") { - found_iter->first.insert(0, split); } else { found_iter->first.append(split); } From 682ac4ed0cf8ccf7ab6c124417fbde181c52a543 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Sun, 19 Apr 2026 01:07:38 +0200 Subject: [PATCH 02/13] Add -R option in Windows NSIS script for silent upgrade (#6818) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add -R option in Windows NSIS script Took 23 minutes * Small fix. Took 3 minutes --------- Co-authored-by: Lukas Brübach --- cmake/NSIS.template.in | 103 +++++++++++++++++- .../interface/widgets/dialogs/dlg_update.cpp | 4 +- 2 files changed, 102 insertions(+), 5 deletions(-) diff --git a/cmake/NSIS.template.in b/cmake/NSIS.template.in index 2fdc61fb9..7b52b7bcc 100644 --- a/cmake/NSIS.template.in +++ b/cmake/NSIS.template.in @@ -11,6 +11,7 @@ SetCompressor LZMA Var NormalDestDir Var PortableDestDir Var PortableMode +Var ReinstallMode !include LogicLib.nsh !include FileFunc.nsh @@ -28,13 +29,23 @@ Var PortableMode !define MUI_FINISHPAGE_RUN_TEXT "Run 'Cockatrice' now" !define MUI_ICON "${NSIS_SOURCE_PATH}\cockatrice\resources\appicon.ico" +!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfReinstall !insertmacro MUI_PAGE_WELCOME + +!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfReinstall !insertmacro MUI_PAGE_LICENSE "${NSIS_SOURCE_PATH}\LICENSE" + Page Custom PortableModePageCreate PortableModePageLeave !define MUI_PAGE_CUSTOMFUNCTION_PRE componentsPagePre !insertmacro MUI_PAGE_COMPONENTS + +!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfReinstall !insertmacro MUI_PAGE_DIRECTORY + +!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfReinstall !insertmacro MUI_PAGE_INSTFILES + +!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfReinstall !insertmacro MUI_PAGE_FINISH !insertmacro MUI_UNPAGE_CONFIRM @@ -73,6 +84,7 @@ ${IfNot} ${Errors} MessageBox MB_ICONINFORMATION|MB_SETFOREGROUND "\ /PORTABLE : Install in portable mode$\n\ /S : Silent install$\n\ + /R : Silent upgrade$\n\ /D=%directory% : Specify destination directory$\n" Quit ${EndIf} @@ -90,6 +102,16 @@ ${Else} ${EndIf} ${EndIf} +ClearErrors +${GetOptions} $9 "/R" $8 +${IfNot} ${Errors} + StrCpy $ReinstallMode 1 + SetSilent silent + SetAutoClose true +${Else} + StrCpy $ReinstallMode 0 +${EndIf} + ${If} $InstDir == "" ; User did not use /D to specify a directory, ; we need to set a default based on the install mode @@ -97,6 +119,22 @@ ${If} $InstDir == "" ${EndIf} Call SetModeDestinationFromInstdir +; --- Detect portable install when using /R --- +${If} $ReinstallMode = 1 + IfFileExists "$InstDir\portable.dat" 0 not_portable + StrCpy $PortableMode 1 + Goto portable_done + + not_portable: + StrCpy $PortableMode 0 + + portable_done: +${EndIf} + +${If} $ReinstallMode = 1 + Call AutoUninstallIfNeeded +${EndIf} + FunctionEnd Function un.onInit @@ -126,8 +164,46 @@ ${Else} ${EndIf} FunctionEnd +Function SkipIfReinstall +${If} $ReinstallMode = 1 + Abort +${EndIf} +FunctionEnd + +Function AutoUninstallIfNeeded + +SetShellVarContext all + +; --- 32-bit uninstall --- +SetRegView 32 +ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "QuietUninstallString" + +StrCmp $R0 "" done32 +DetailPrint "Removing previous version (32-bit)..." +ExecWait '$R0' + +done32: + +; --- 64-bit uninstall --- +${If} ${RunningX64} + SetRegView 64 + ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "QuietUninstallString" + + StrCmp $R0 "" done64 + DetailPrint "Removing previous version (64-bit)..." + ExecWait '$R0' + + done64: +${EndIf} + +FunctionEnd Function PortableModePageCreate + +${If} $ReinstallMode = 1 + Abort +${EndIf} + Call SetModeDestinationFromInstdir ; If the user clicks BACK on the directory page we will remember their mode specific directory !insertmacro MUI_HEADER_TEXT "Install Mode" "Choose how you want to install Cockatrice." nsDialogs::Create 1018 @@ -159,6 +235,11 @@ ${EndIf} FunctionEnd Function componentsPagePre + +${If} $ReinstallMode = 1 + Return +${EndIf} + ${If} $PortableMode = 0 SetShellVarContext all @@ -168,8 +249,12 @@ ${If} $PortableMode = 0 ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "UninstallString" StrCmp $R0 "" done32 - MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "A previous version of Cockatrice must be uninstalled before installing the new one." IDOK uninst32 - Abort + ${If} $ReinstallMode = 0 + MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "A previous version of Cockatrice must be uninstalled before installing the new one." IDOK uninst32 + Abort + ${Else} + Goto uninst32 + ${EndIf} uninst32: ClearErrors @@ -184,8 +269,12 @@ ${If} $PortableMode = 0 ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "UninstallString" StrCmp $R0 "" done64 - MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "A previous version of Cockatrice must be uninstalled before installing the new one." IDOK uninst64 - Abort + ${If} $ReinstallMode = 0 + MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "A previous version of Cockatrice must be uninstalled before installing the new one." IDOK uninst64 + Abort + ${Else} + Goto uninst64 + ${EndIf} uninst64: ClearErrors @@ -277,6 +366,12 @@ ${Else} FileWrite $0 "PORTABLE" FileClose $0 ${EndIf} + +${If} $ReinstallMode = 1 + IfFileExists "$INSTDIR\cockatrice.exe" 0 +2 + Exec '"$INSTDIR\cockatrice.exe"' +${EndIf} + SectionEnd Section "Start menu item" SecStartMenu diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp index 0a9244dec..c9adeb270 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp @@ -219,7 +219,9 @@ void DlgUpdate::downloadSuccessful(const QUrl &filepath) { setLabel(tr("Installing...")); // Try to open the installer. If it opens, quit Cockatrice - if (QDesktopServices::openUrl(filepath)) { + if (QProcess::startDetached(filepath.toLocalFile(), + QStringList() + << "/R" << QString("/D=%1").arg(QCoreApplication::applicationDirPath()))) { QMetaObject::invokeMethod(static_cast(parent()), "close", Qt::QueuedConnection); qCInfo(DlgUpdateLog) << "Opened downloaded update file successfully - closing Cockatrice"; close(); From db235ecae80a0a6dbf0cfb7289d63bd9602a5352 Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Sun, 19 Apr 2026 01:11:00 +0200 Subject: [PATCH 03/13] update version number to 3.0.0 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fe808a652..e4d6ee11e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -74,7 +74,7 @@ endif() # A project name is needed for CPack # Version can be overriden by git tags, see cmake/getversion.cmake -project("Cockatrice" VERSION 2.11.0) +project("Cockatrice" VERSION 3.0.0) # Set release name if not provided via env/cmake var if(NOT DEFINED GIT_TAG_RELEASENAME) From 655a8e52a156da9115a34b5584cf9ce011580d00 Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Sun, 19 Apr 2026 01:15:58 +0200 Subject: [PATCH 04/13] update release name --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e4d6ee11e..4a5e944c4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -78,7 +78,7 @@ project("Cockatrice" VERSION 3.0.0) # Set release name if not provided via env/cmake var if(NOT DEFINED GIT_TAG_RELEASENAME) - set(GIT_TAG_RELEASENAME "Omenpath") + set(GIT_TAG_RELEASENAME "Graduation Day") endif() # Use c++20 for all targets From 58a8c7d3df6902f0b32d403bddb9a9adabf2237a Mon Sep 17 00:00:00 2001 From: tooomm Date: Sun, 19 Apr 2026 02:47:14 +0200 Subject: [PATCH 05/13] CI: Use `ubuntu-slim` for simple jobs (#6798) --- .github/workflows/desktop-build.yml | 2 +- .github/workflows/desktop-lint.yml | 4 ++-- .github/workflows/translations-pull.yml | 2 +- .github/workflows/translations-push.yml | 4 ++-- .github/workflows/web-lint.yml | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index aeed5da81..3c8e915a8 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -46,7 +46,7 @@ concurrency: jobs: configure: name: Configure - runs-on: ubuntu-latest + runs-on: ubuntu-slim outputs: tag: ${{steps.configure.outputs.tag}} sha: ${{steps.configure.outputs.sha}} diff --git a/.github/workflows/desktop-lint.yml b/.github/workflows/desktop-lint.yml index 433f302a5..fe7be0287 100644 --- a/.github/workflows/desktop-lint.yml +++ b/.github/workflows/desktop-lint.yml @@ -20,13 +20,13 @@ on: jobs: format: - runs-on: ubuntu-22.04 + runs-on: ubuntu-slim steps: - name: Checkout uses: actions/checkout@v6 with: - fetch-depth: 20 # should be enough to find merge base + fetch-depth: 20 # should be enough to find merge base - name: Install dependencies shell: bash diff --git a/.github/workflows/translations-pull.yml b/.github/workflows/translations-pull.yml index ed61e3b19..ca9069192 100644 --- a/.github/workflows/translations-pull.yml +++ b/.github/workflows/translations-pull.yml @@ -16,7 +16,7 @@ jobs: if: github.event_name != 'schedule' || github.repository_owner == 'Cockatrice' name: Pull languages - runs-on: ubuntu-latest + runs-on: ubuntu-slim steps: - name: Checkout repo diff --git a/.github/workflows/translations-push.yml b/.github/workflows/translations-push.yml index 777e9e6ac..e926a58ed 100644 --- a/.github/workflows/translations-push.yml +++ b/.github/workflows/translations-push.yml @@ -16,7 +16,7 @@ jobs: if: github.event_name != 'schedule' || github.repository_owner == 'Cockatrice' name: Push strings - runs-on: ubuntu-latest + runs-on: ubuntu-slim steps: - name: Checkout repo @@ -46,7 +46,7 @@ jobs: - name: Render template id: template - uses: chuhlomin/render-template@v1 + uses: chuhlomin/render-template/binary@v1 with: template: .ci/update_translation_source_strings_template.md vars: | diff --git a/.github/workflows/web-lint.yml b/.github/workflows/web-lint.yml index 8a90325e7..ecc6d14d1 100644 --- a/.github/workflows/web-lint.yml +++ b/.github/workflows/web-lint.yml @@ -10,7 +10,7 @@ on: jobs: ESLint: - runs-on: ubuntu-latest + runs-on: ubuntu-slim defaults: run: From ccb9901b285dde9b4eb16c0e3dd5c59224345ed1 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Sun, 19 Apr 2026 23:08:56 +0200 Subject: [PATCH 06/13] [DeckAnalytics] Add a checkbox to include/exclude sideboard for calculation (#6823) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [DeckAnalytics] Add a checkbox to include/exclude sideboard for calculation Took 15 minutes * default to false, actually Took 2 minutes --------- Co-authored-by: Lukas Brübach --- .../widgets/deck_analytics/deck_analytics_widget.cpp | 12 ++++++++++++ .../widgets/deck_analytics/deck_analytics_widget.h | 4 ++++ .../deck_analytics/deck_list_statistics_analyzer.cpp | 8 +++++++- .../deck_analytics/deck_list_statistics_analyzer.h | 6 ++++++ 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.cpp index ea61302f0..147675e21 100644 --- a/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.cpp @@ -29,10 +29,14 @@ DeckAnalyticsWidget::DeckAnalyticsWidget(QWidget *parent, DeckListStatisticsAnal removeButton = new QPushButton(this); saveButton = new QPushButton(this); loadButton = new QPushButton(this); + includeSideboardCheckBox = new QCheckBox(this); + includeSideboardCheckBox->setChecked(false); + controlLayout->addWidget(addButton); controlLayout->addWidget(removeButton); controlLayout->addWidget(saveButton); controlLayout->addWidget(loadButton); + controlLayout->addWidget(includeSideboardCheckBox); layout->addWidget(controlContainer); @@ -40,6 +44,7 @@ DeckAnalyticsWidget::DeckAnalyticsWidget(QWidget *parent, DeckListStatisticsAnal connect(removeButton, &QPushButton::clicked, this, &DeckAnalyticsWidget::onRemoveSelected); connect(saveButton, &QPushButton::clicked, this, &DeckAnalyticsWidget::saveLayout); connect(loadButton, &QPushButton::clicked, this, &DeckAnalyticsWidget::loadLayout); + connect(includeSideboardCheckBox, &QCheckBox::clicked, this, &DeckAnalyticsWidget::includeSideboardChanged); // Scroll area and container scrollArea = new QScrollArea(this); @@ -66,6 +71,13 @@ void DeckAnalyticsWidget::retranslateUi() removeButton->setText(tr("Remove Panel")); saveButton->setText(tr("Save Layout")); loadButton->setText(tr("Load Layout")); + includeSideboardCheckBox->setText(tr("Include Sideboard")); +} + +void DeckAnalyticsWidget::includeSideboardChanged(bool checked) +{ + statsAnalyzer->getConfig().includeSideboard = checked; + updateDisplays(); } void DeckAnalyticsWidget::updateDisplays() diff --git a/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.h b/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.h index 31ee36fbb..09618c3f8 100644 --- a/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.h +++ b/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.h @@ -11,6 +11,7 @@ #include "deck_list_statistics_analyzer.h" #include "resizable_panel.h" +#include #include #include #include @@ -29,6 +30,7 @@ public slots: public: explicit DeckAnalyticsWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer); void retranslateUi(); + void includeSideboardChanged(bool checked); private slots: void onAddPanel(); @@ -57,6 +59,8 @@ private: QPushButton *saveButton; QPushButton *loadButton; + QCheckBox *includeSideboardCheckBox; + QScrollArea *scrollArea; QWidget *panelContainer; QVBoxLayout *panelLayout; diff --git a/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp b/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp index ad8afb766..073b6d25c 100644 --- a/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp +++ b/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp @@ -19,7 +19,13 @@ void DeckListStatisticsAnalyzer::analyze() { clearData(); - QList nodes = model->getCardNodes(); + QList nodes; + + if (config.includeSideboard) { + nodes = model->getCardNodes(); + } else { + nodes = model->getCardNodesForZone(DECK_ZONE_MAIN); + } for (auto node : nodes) { CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(node->getName()); diff --git a/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.h b/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.h index 946bb0117..52ae751bf 100644 --- a/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.h +++ b/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.h @@ -17,6 +17,7 @@ struct DeckListStatisticsAnalyzerConfig bool computeCategories = true; bool computeCurveBreakdowns = true; bool computeProbabilities = true; + bool includeSideboard = false; }; class DeckListStatisticsAnalyzer : public QObject @@ -133,6 +134,11 @@ public: return model; } + DeckListStatisticsAnalyzerConfig &getConfig() + { + return config; + } + signals: void statsUpdated(); From 98c4e829f8fb6cfcdb81ab5e30d3f8e90a4fd15e Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Mon, 20 Apr 2026 13:19:29 +0200 Subject: [PATCH 07/13] [GameSelector/Filters] Properly sync toolbar and dialog. (#6822) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Took 59 minutes Took 2 minutes Co-authored-by: Lukas Brübach --- .../widgets/server/game_selector.cpp | 10 +- .../interface/widgets/server/game_selector.h | 1 + .../game_selector_quick_filter_toolbar.cpp | 138 +++++++++++++++--- .../game_selector_quick_filter_toolbar.h | 18 +++ .../interface/widgets/server/games_model.cpp | 1 + .../interface/widgets/server/games_model.h | 3 + 6 files changed, 145 insertions(+), 26 deletions(-) diff --git a/cockatrice/src/interface/widgets/server/game_selector.cpp b/cockatrice/src/interface/widgets/server/game_selector.cpp index f14cc6d82..0ff2a5542 100644 --- a/cockatrice/src/interface/widgets/server/game_selector.cpp +++ b/cockatrice/src/interface/widgets/server/game_selector.cpp @@ -91,6 +91,7 @@ GameSelector::GameSelector(AbstractClient *_client, bool filtersSetToDefault = showFilters && gameListProxyModel->areFilterParametersSetToDefaults(); clearFilterButton->setEnabled(!filtersSetToDefault); connect(clearFilterButton, &QPushButton::clicked, this, &GameSelector::actClearFilter); + connect(gameListProxyModel, &GamesProxyModel::filtersChanged, this, &GameSelector::checkClearFilterButtonState); if (room) { createButton = new QPushButton; @@ -188,15 +189,16 @@ void GameSelector::actSetFilter() dlg.getShowOnlyIfSpectatorsCanChat(), dlg.getShowOnlyIfSpectatorsCanSeeHands()); gameListProxyModel->saveFilterParameters(gameTypeMap); - clearFilterButton->setEnabled(!gameListProxyModel->areFilterParametersSetToDefaults()); - updateTitle(); } +void GameSelector::checkClearFilterButtonState() +{ + clearFilterButton->setEnabled(!gameListProxyModel->areFilterParametersSetToDefaults()); +} + void GameSelector::actClearFilter() { - clearFilterButton->setEnabled(false); - gameListProxyModel->resetFilterParameters(); gameListProxyModel->saveFilterParameters(gameTypeMap); diff --git a/cockatrice/src/interface/widgets/server/game_selector.h b/cockatrice/src/interface/widgets/server/game_selector.h index ea0a4feb0..fa91e5f96 100644 --- a/cockatrice/src/interface/widgets/server/game_selector.h +++ b/cockatrice/src/interface/widgets/server/game_selector.h @@ -40,6 +40,7 @@ private slots: * Updates the proxy model with selected filter parameters and refreshes the displayed game list. */ void actSetFilter(); + void checkClearFilterButtonState(); /** * @brief Clears all filters applied to the game list. diff --git a/cockatrice/src/interface/widgets/server/game_selector_quick_filter_toolbar.cpp b/cockatrice/src/interface/widgets/server/game_selector_quick_filter_toolbar.cpp index daab4d6eb..f7eacd636 100644 --- a/cockatrice/src/interface/widgets/server/game_selector_quick_filter_toolbar.cpp +++ b/cockatrice/src/interface/widgets/server/game_selector_quick_filter_toolbar.cpp @@ -19,32 +19,46 @@ GameSelectorQuickFilterToolBar::GameSelectorQuickFilterToolBar(QWidget *parent, mainLayout->setSpacing(5); searchBar = new QLineEdit(this); - searchBar->setText(model->getCreatorNameFilters().join(", ")); - connect(searchBar, &QLineEdit::textChanged, this, [this](const QString &text) { model->setGameNameFilter(text); }); + searchBar->setText(model->getGameNameFilter()); + connect(searchBar, &QLineEdit::textChanged, this, [this](const QString &text) { + applyFilters([&](auto &, auto &, auto &, auto &, auto &, auto &, auto &, QString &gameNameFilter, auto &, + auto &, auto &, auto &, auto &, auto &, auto &, auto &, auto &) { gameNameFilter = text; }); + }); hideGamesNotCreatedByBuddiesCheckBox = new QCheckBox(this); - hideGamesNotCreatedByBuddiesCheckBox->setChecked(model->getHideBuddiesOnlyGames()); + hideGamesNotCreatedByBuddiesCheckBox->setChecked(model->getHideNotBuddyCreatedGames()); connect(hideGamesNotCreatedByBuddiesCheckBox, &QCheckBox::toggled, this, [this](bool checked) { - if (checked) { - QStringList buddyNames; - for (auto buddy : tabSupervisor->getUserListManager()->getBuddyList().values()) { - buddyNames << QString::fromStdString(buddy.name()); + applyFilters([&](auto &, auto &, auto &, auto &, auto &, bool &hideNotBuddyCreatedGames, auto &, auto &, + QStringList &creatorNameFilters, auto &, auto &, auto &, auto &, auto &, auto &, auto &, + auto &) { + hideNotBuddyCreatedGames = checked; + + if (checked) { + QStringList buddyNames; + for (auto buddy : tabSupervisor->getUserListManager()->getBuddyList().values()) { + buddyNames << QString::fromStdString(buddy.name()); + } + creatorNameFilters = buddyNames; + } else { + creatorNameFilters.clear(); } - model->setCreatorNameFilters(buddyNames); - } else { - model->setCreatorNameFilters({}); - } + }); }); hideFullGamesCheckBox = new QCheckBox(this); hideFullGamesCheckBox->setChecked(model->getHideFullGames()); - connect(hideFullGamesCheckBox, &QCheckBox::toggled, this, - [this](bool checked) { model->setHideFullGames(checked); }); + connect(hideFullGamesCheckBox, &QCheckBox::toggled, this, [this](bool checked) { + applyFilters([&](auto &, auto &, bool &hideFullGames, auto &, auto &, auto &, auto &, auto &, auto &, auto &, + auto &, auto &, auto &, auto &, auto &, auto &, auto &) { hideFullGames = checked; }); + }); hideStartedGamesCheckBox = new QCheckBox(this); hideStartedGamesCheckBox->setChecked(model->getHideGamesThatStarted()); - connect(hideStartedGamesCheckBox, &QCheckBox::toggled, this, - [this](bool checked) { model->setHideGamesThatStarted(checked); }); + connect(hideStartedGamesCheckBox, &QCheckBox::toggled, this, [this](bool checked) { + applyFilters([&](auto &, auto &, auto &, bool &hideGamesThatStarted, auto &, auto &, auto &, auto &, auto &, + auto &, auto &, auto &, auto &, auto &, auto &, auto &, + auto &) { hideGamesThatStarted = checked; }); + }); filterToFormatComboBox = new QComboBox(this); @@ -69,13 +83,15 @@ GameSelectorQuickFilterToolBar::GameSelectorQuickFilterToolBar(QWidget *parent, // Update proxy model on selection change connect(filterToFormatComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, [this](int index) { - QVariant data = filterToFormatComboBox->itemData(index); - if (!data.isValid()) { - model->setGameTypeFilter({}); // empty = no filter - } else { - int typeId = data.toInt(); - model->setGameTypeFilter({typeId}); - } + applyFilters([&](auto &, auto &, auto &, auto &, auto &, auto &, auto &, auto &, auto &, + QSet &gameTypeFilter, auto &, auto &, auto &, auto &, auto &, auto &, auto &) { + QVariant data = filterToFormatComboBox->itemData(index); + if (!data.isValid()) { + gameTypeFilter.clear(); + } else { + gameTypeFilter = {data.toInt()}; + } + }); }); hideGamesNotCreatedByBuddiesCheckBox->setMinimumSize(20, 20); @@ -96,9 +112,87 @@ GameSelectorQuickFilterToolBar::GameSelectorQuickFilterToolBar(QWidget *parent, setLayout(mainLayout); + syncFromModel(); + + connect(model, &GamesProxyModel::filtersChanged, this, &GameSelectorQuickFilterToolBar::syncFromModel); + retranslateUi(); } +void GameSelectorQuickFilterToolBar::syncFromModel() +{ + QSignalBlocker b1(searchBar); + QSignalBlocker b2(filterToFormatComboBox); + QSignalBlocker b3(hideGamesNotCreatedByBuddiesCheckBox); + QSignalBlocker b4(hideFullGamesCheckBox); + QSignalBlocker b5(hideStartedGamesCheckBox); + + searchBar->setText(model->getGameNameFilter()); + + hideGamesNotCreatedByBuddiesCheckBox->setChecked(model->getHideNotBuddyCreatedGames()); + hideFullGamesCheckBox->setChecked(model->getHideFullGames()); + hideStartedGamesCheckBox->setChecked(model->getHideGamesThatStarted()); + + QSet types = model->getGameTypeFilter(); + if (types.size() == 1) { + int idx = filterToFormatComboBox->findData(*types.begin()); + filterToFormatComboBox->setCurrentIndex(idx >= 0 ? idx : 0); + } else { + filterToFormatComboBox->setCurrentIndex(0); + } +} + +void GameSelectorQuickFilterToolBar::applyFilters(std::function &, + int &, + int &, + QTime &, + bool &, + bool &, + bool &, + bool &)> mutator) +{ + bool hideBuddiesOnlyGames = model->getHideBuddiesOnlyGames(); + bool hideIgnoredUserGames = model->getHideIgnoredUserGames(); + bool hideFullGames = model->getHideFullGames(); + bool hideGamesThatStarted = model->getHideGamesThatStarted(); + bool hidePasswordProtectedGames = model->getHidePasswordProtectedGames(); + bool hideNotBuddyCreatedGames = model->getHideNotBuddyCreatedGames(); + bool hideOpenDecklistGames = model->getHideOpenDecklistGames(); + + QString gameNameFilter = model->getGameNameFilter(); + QStringList creatorNameFilters = model->getCreatorNameFilters(); + QSet gameTypeFilter = model->getGameTypeFilter(); + + int minPlayers = model->getMaxPlayersFilterMin(); + int maxPlayers = model->getMaxPlayersFilterMax(); + QTime maxGameAge = model->getMaxGameAge(); + + bool showOnlyIfSpectatorsCanWatch = model->getShowOnlyIfSpectatorsCanWatch(); + bool showSpectatorPasswordProtected = model->getShowSpectatorPasswordProtected(); + bool showOnlyIfSpectatorsCanChat = model->getShowOnlyIfSpectatorsCanChat(); + bool showOnlyIfSpectatorsCanSeeHands = model->getShowOnlyIfSpectatorsCanSeeHands(); + + mutator(hideBuddiesOnlyGames, hideIgnoredUserGames, hideFullGames, hideGamesThatStarted, hidePasswordProtectedGames, + hideNotBuddyCreatedGames, hideOpenDecklistGames, gameNameFilter, creatorNameFilters, gameTypeFilter, + minPlayers, maxPlayers, maxGameAge, showOnlyIfSpectatorsCanWatch, showSpectatorPasswordProtected, + showOnlyIfSpectatorsCanChat, showOnlyIfSpectatorsCanSeeHands); + + model->setGameFilters(hideBuddiesOnlyGames, hideIgnoredUserGames, hideFullGames, hideGamesThatStarted, + hidePasswordProtectedGames, hideNotBuddyCreatedGames, hideOpenDecklistGames, gameNameFilter, + creatorNameFilters, gameTypeFilter, minPlayers, maxPlayers, maxGameAge, + showOnlyIfSpectatorsCanWatch, showSpectatorPasswordProtected, showOnlyIfSpectatorsCanChat, + showOnlyIfSpectatorsCanSeeHands); +} + void GameSelectorQuickFilterToolBar::retranslateUi() { searchBar->setPlaceholderText(tr("Filter by game name...")); diff --git a/cockatrice/src/interface/widgets/server/game_selector_quick_filter_toolbar.h b/cockatrice/src/interface/widgets/server/game_selector_quick_filter_toolbar.h index 642fdd1c4..c658418f9 100644 --- a/cockatrice/src/interface/widgets/server/game_selector_quick_filter_toolbar.h +++ b/cockatrice/src/interface/widgets/server/game_selector_quick_filter_toolbar.h @@ -18,6 +18,24 @@ public: TabSupervisor *tabSupervisor, GamesProxyModel *model, const QMap &allGameTypes); + void syncFromModel(); + void applyFilters(std::function &, + int &, + int &, + QTime &, + bool &, + bool &, + bool &, + bool &)> mutator); void retranslateUi(); private: diff --git a/cockatrice/src/interface/widgets/server/games_model.cpp b/cockatrice/src/interface/widgets/server/games_model.cpp index 05d363fee..1f05308b8 100644 --- a/cockatrice/src/interface/widgets/server/games_model.cpp +++ b/cockatrice/src/interface/widgets/server/games_model.cpp @@ -326,6 +326,7 @@ void GamesProxyModel::setGameFilters(bool _hideBuddiesOnlyGames, #else invalidateFilter(); #endif + emit filtersChanged(); } int GamesProxyModel::getNumFilteredGames() const diff --git a/cockatrice/src/interface/widgets/server/games_model.h b/cockatrice/src/interface/widgets/server/games_model.h index 56c806fb6..c6884093d 100644 --- a/cockatrice/src/interface/widgets/server/games_model.h +++ b/cockatrice/src/interface/widgets/server/games_model.h @@ -138,6 +138,9 @@ private: bool showOnlyIfSpectatorsCanChat; bool showOnlyIfSpectatorsCanSeeHands; +signals: + void filtersChanged(); + public: /** * @brief Constructs a GamesProxyModel. From 6765831b929381df4345c3eacb3dcd761771ecb6 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Mon, 20 Apr 2026 17:47:15 +0200 Subject: [PATCH 08/13] Change button colors to be palette aware. (#6821) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Change button colors to be palette aware. Took 13 minutes Took 41 seconds Took 15 seconds * Change button style. Took 24 minutes Took 4 seconds --------- Co-authored-by: Lukas Brübach --- .../visual_database_display_filter_button.h | 21 +++++++++++++++++++ ..._display_format_legality_filter_widget.cpp | 4 ++-- ...tabase_display_main_type_filter_widget.cpp | 5 +++-- ...al_database_display_name_filter_widget.cpp | 5 +++-- ...ual_database_display_set_filter_widget.cpp | 5 +++-- ...atabase_display_sub_type_filter_widget.cpp | 5 +++-- 6 files changed, 35 insertions(+), 10 deletions(-) create mode 100644 cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_button.h diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_button.h b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_button.h new file mode 100644 index 000000000..5d9f7f944 --- /dev/null +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_button.h @@ -0,0 +1,21 @@ +#ifndef COCKATRICE_VISUAL_DATABASE_DISPLAY_FILTER_BUTTON_H +#define COCKATRICE_VISUAL_DATABASE_DISPLAY_FILTER_BUTTON_H + +#include + +const QString visualDatabaseDisplayFilterButtonStyle = QString(R"( + QPushButton { + background-color: palette(button); + color: palette(button-text); + padding: 5px 10px; + border-radius: 4px; + border: 1px solid palette(dark); + } + QPushButton:checked { + background-color: palette(highlight); + color: palette(highlighted-text); + border: 1px solid palette(shadow); + } +)"); + +#endif // COCKATRICE_VISUAL_DATABASE_DISPLAY_FILTER_BUTTON_H diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_format_legality_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_format_legality_filter_widget.cpp index 0df948016..633f07af7 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_format_legality_filter_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_format_legality_filter_widget.cpp @@ -1,6 +1,7 @@ #include "visual_database_display_format_legality_filter_widget.h" #include "../../../filters/filter_tree_model.h" +#include "visual_database_display_filter_button.h" #include #include @@ -80,8 +81,7 @@ void VisualDatabaseDisplayFormatLegalityFilterWidget::createFormatButtons() for (auto it = allFormatsWithCount.begin(); it != allFormatsWithCount.end(); ++it) { auto *button = new QPushButton(it.key(), flowWidget); button->setCheckable(true); - button->setStyleSheet("QPushButton { background-color: lightgray; border: 1px solid gray; padding: 5px; }" - "QPushButton:checked { background-color: green; color: white; }"); + button->setStyleSheet(visualDatabaseDisplayFilterButtonStyle); flowWidget->addWidget(button); formatButtons[it.key()] = button; diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_main_type_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_main_type_filter_widget.cpp index bc8e914bd..c44489c1b 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_main_type_filter_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_main_type_filter_widget.cpp @@ -1,6 +1,7 @@ #include "visual_database_display_main_type_filter_widget.h" #include "../../../filters/filter_tree_model.h" +#include "visual_database_display_filter_button.h" #include #include @@ -75,8 +76,8 @@ void VisualDatabaseDisplayMainTypeFilterWidget::createMainTypeButtons() for (auto it = allMainCardTypesWithCount.begin(); it != allMainCardTypesWithCount.end(); ++it) { auto *button = new QPushButton(it.key(), flowWidget); button->setCheckable(true); - button->setStyleSheet("QPushButton { background-color: lightgray; border: 1px solid gray; padding: 5px; }" - "QPushButton:checked { background-color: green; color: white; }"); + + button->setStyleSheet(visualDatabaseDisplayFilterButtonStyle); flowWidget->addWidget(button); typeButtons[it.key()] = button; diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_name_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_name_filter_widget.cpp index 5098696dd..2751ee971 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_name_filter_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_name_filter_widget.cpp @@ -3,6 +3,7 @@ #include "../../../interface/widgets/dialogs/dlg_load_deck_from_clipboard.h" #include "../../../interface/widgets/tabs/abstract_tab_deck_editor.h" #include "../deck_editor/deck_state_manager.h" +#include "visual_database_display_filter_button.h" #include @@ -95,8 +96,8 @@ void VisualDatabaseDisplayNameFilterWidget::createNameFilter(const QString &name // Create a button for the filter auto *button = new QPushButton(name, flowWidget); - button->setStyleSheet("QPushButton { background-color: lightgray; border: 1px solid gray; padding: 5px; }" - "QPushButton:hover { background-color: red; color: white; }"); + + button->setStyleSheet(visualDatabaseDisplayFilterButtonStyle); connect(button, &QPushButton::clicked, this, [this, name]() { removeNameFilter(name); diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_set_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_set_filter_widget.cpp index 3339bc561..b72116461 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_set_filter_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_set_filter_widget.cpp @@ -2,6 +2,7 @@ #include "../../../client/settings/cache_settings.h" #include "../../../filters/filter_tree_model.h" +#include "visual_database_display_filter_button.h" #include #include @@ -101,8 +102,8 @@ void VisualDatabaseDisplaySetFilterWidget::createSetButtons() auto *button = new QPushButton(longName + " (" + shortName + ")", flowWidget); button->setCheckable(true); - button->setStyleSheet("QPushButton { background-color: lightgray; border: 1px solid gray; padding: 5px; }" - "QPushButton:checked { background-color: green; color: white; }"); + + button->setStyleSheet(visualDatabaseDisplayFilterButtonStyle); flowWidget->addWidget(button); setButtons[shortName] = button; diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_sub_type_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_sub_type_filter_widget.cpp index 57559d12c..6d4bcb58e 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_sub_type_filter_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_sub_type_filter_widget.cpp @@ -1,6 +1,7 @@ #include "visual_database_display_sub_type_filter_widget.h" #include "../../../filters/filter_tree_model.h" +#include "visual_database_display_filter_button.h" #include #include @@ -80,8 +81,8 @@ void VisualDatabaseDisplaySubTypeFilterWidget::createSubTypeButtons() for (auto it = allSubCardTypesWithCount.begin(); it != allSubCardTypesWithCount.end(); ++it) { auto *button = new QPushButton(it.key(), flowWidget); button->setCheckable(true); - button->setStyleSheet("QPushButton { background-color: lightgray; border: 1px solid gray; padding: 5px; }" - "QPushButton:checked { background-color: green; color: white; }"); + + button->setStyleSheet(visualDatabaseDisplayFilterButtonStyle); flowWidget->addWidget(button); typeButtons[it.key()] = button; From 6ab947418cb680f1e6336f092c9ae195b61d2ee8 Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Tue, 21 Apr 2026 01:26:08 +0200 Subject: [PATCH 09/13] add an isEmpty method to printing info (#6805) no reason, I just find it easier to understand --- .../interface/widgets/cards/card_info_text_widget.cpp | 2 +- .../card/database/card_database_querier.cpp | 4 ++-- .../libcockatrice/card/printing/printing_info.h | 10 ++++++++++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/cockatrice/src/interface/widgets/cards/card_info_text_widget.cpp b/cockatrice/src/interface/widgets/cards/card_info_text_widget.cpp index 456e1533a..f5f343807 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_text_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_info_text_widget.cpp @@ -62,7 +62,7 @@ void CardInfoTextWidget::setCard(const ExactCard &exactCard) text += QString("%1%2") .arg(tr("Name:"), card->getName().toHtmlEscaped()); - if (exactCard.getPrinting() != PrintingInfo()) { + if (!exactCard.getPrinting().isEmpty()) { QString setShort = exactCard.getPrinting().getSet()->getShortName().toHtmlEscaped(); QString cardNum = exactCard.getPrinting().getProperty("num").toHtmlEscaped(); diff --git a/libcockatrice_card/libcockatrice/card/database/card_database_querier.cpp b/libcockatrice_card/libcockatrice/card/database/card_database_querier.cpp index 26e515a2d..021e8d12d 100644 --- a/libcockatrice_card/libcockatrice/card/database/card_database_querier.cpp +++ b/libcockatrice_card/libcockatrice/card/database/card_database_querier.cpp @@ -133,7 +133,7 @@ ExactCard CardDatabaseQuerier::getRandomCard() const ExactCard CardDatabaseQuerier::getCardFromSameSet(const QString &cardName, const PrintingInfo &otherPrinting) const { // The source card does not have a printing defined, which means we can't get a card from the same set. - if (otherPrinting == PrintingInfo()) { + if (otherPrinting.isEmpty()) { return getCard({cardName}); } @@ -360,4 +360,4 @@ QMap CardDatabaseQuerier::getAllFormatsWithCount() const } return formatCounts; -} \ No newline at end of file +} diff --git a/libcockatrice_card/libcockatrice/card/printing/printing_info.h b/libcockatrice_card/libcockatrice/card/printing/printing_info.h index 43d82a9cb..ad7b33654 100644 --- a/libcockatrice_card/libcockatrice/card/printing/printing_info.h +++ b/libcockatrice_card/libcockatrice/card/printing/printing_info.h @@ -54,6 +54,16 @@ public: return this->set == other.set && this->properties == other.properties; } + /** + * @brief check if the info is empty, as if default constructed. + * + * @return True if both set and properties are empty, otherwise false. + */ + bool isEmpty() const + { + return set == nullptr && properties.isEmpty(); + } + private: CardSetPtr set; ///< The set this variation belongs to. QVariantHash properties; ///< Key-value store for variation-specific attributes. From 501c4b96d4bf77e07d283d34c7b78452482ba0a4 Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Tue, 21 Apr 2026 08:09:20 +0200 Subject: [PATCH 10/13] clear all players when closing game tab (#6828) this prevents issues caused by items in play when using the player destructor in the wrong order --- cockatrice/src/interface/widgets/tabs/tab_game.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.cpp b/cockatrice/src/interface/widgets/tabs/tab_game.cpp index cf8269069..161829b35 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_game.cpp @@ -259,6 +259,9 @@ TabGame::~TabGame() if (replayManager) { delete replayManager->replay; } + for (auto &player : game->getPlayerManager()->getPlayers()) { + player->clear(); + } } void TabGame::updatePlayerListDockTitle() From 9226bc9ddd99db017c8871a2ee14208e1f8e985d Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Tue, 21 Apr 2026 08:09:39 +0200 Subject: [PATCH 11/13] [TabArchidekt] Place sideboard categories into the sideboard. (#6824) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Took 35 minutes Took 3 seconds Co-authored-by: Lukas Brübach --- .../archidekt_api_response_card_entry.cpp | 31 +++++++++- .../card/archidekt_api_response_card_entry.h | 13 ++++- ...idekt_api_response_deck_display_widget.cpp | 56 +++++++++++++++++-- 3 files changed, 89 insertions(+), 11 deletions(-) diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/card/archidekt_api_response_card_entry.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/card/archidekt_api_response_card_entry.cpp index 7a424de8b..f51d0f3e7 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/card/archidekt_api_response_card_entry.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/card/archidekt_api_response_card_entry.cpp @@ -4,10 +4,29 @@ void ArchidektApiResponseCardEntry::fromJson(const QJsonObject &json) { id = json.value("id").toInt(); + categories.clear(); + auto categoriesJson = json.value("categories").toArray(); - for (auto category : categoriesJson) { - categories.append(category.toString()); + for (const auto &categoryValue : categoriesJson) { + Category cat; + + if (categoryValue.isObject()) { + QJsonObject obj = categoryValue.toObject(); + + cat.id = obj.value("id").toInt(); + cat.name = obj.value("name").toString(); + cat.isPremier = obj.value("isPremier").toBool(); + cat.includedInDeck = obj.value("includedInDeck").toBool(); + cat.includedInPrice = obj.value("includedInPrice").toBool(); + } else if (categoryValue.isString()) { + cat.name = categoryValue.toString(); + + // assume mainboard unless known otherwise + cat.includedInDeck = true; + } + + categories.append(cat); } companion = json.value("companion").toBool(); @@ -27,7 +46,13 @@ void ArchidektApiResponseCardEntry::fromJson(const QJsonObject &json) void ArchidektApiResponseCardEntry::debugPrint() const { qDebug() << "Id:" << id; - qDebug() << "Categories:" << categories; + for (auto category : categories) { + qDebug() << "Category ID:" << category.id; + qDebug() << "Category Name:" << category.name; + qDebug() << "Category Premier:" << category.isPremier; + qDebug() << "Category Included in Deck:" << category.includedInDeck; + qDebug() << "Category Included in Price:" << category.includedInPrice; + } qDebug() << "Companion:" << companion; qDebug() << "FlippedDefault:" << flippedDefault; qDebug() << "Label:" << label; diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/card/archidekt_api_response_card_entry.h b/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/card/archidekt_api_response_card_entry.h index f7f86e9ed..f3961dc6f 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/card/archidekt_api_response_card_entry.h +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/card/archidekt_api_response_card_entry.h @@ -9,6 +9,15 @@ #include #include +struct Category +{ + int id; + QString name; + bool isPremier; + bool includedInDeck; + bool includedInPrice; +}; + class ArchidektApiResponseCardEntry { public: @@ -26,7 +35,7 @@ public: return card; }; - QStringList getCategories() const + QList getCategories() const { return categories; } @@ -38,7 +47,7 @@ public: private: int id; - QStringList categories; + QList categories; bool companion; bool flippedDefault; QString label; diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp index 8b17cd49e..66b68d823 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp @@ -63,16 +63,60 @@ ArchidektApiResponseDeckDisplayWidget::ArchidektApiResponseDeckDisplayWidget(QWi QString tempDeck; QTextStream deckStream(&tempDeck); - for (auto card : response.getCards()) { + QString mainboardText; + QString sideboardText; + + QTextStream mainStream(&mainboardText); + QTextStream sideStream(&sideboardText); + + for (const auto &card : response.getCards()) { QString fullName = card.getCard().getOracleCard().value("name").toString(); // We don't really care about the second card, the card database already has it as a relation QString cleanName = fullName.split("//").first().trimmed(); - tempDeck += QString("%1 %2 (%3) %4\n") - .arg(card.getQuantity()) - .arg(cleanName) - .arg(card.getCard().getEdition().getEditionCode().toUpper()) - .arg(card.getCard().getCollectorNumber()); + QString line = QString("%1 %2 (%3) %4\n") + .arg(card.getQuantity()) + .arg(cleanName) + .arg(card.getCard().getEdition().getEditionCode().toUpper()) + .arg(card.getCard().getCollectorNumber()); + + bool isCommander = false; + bool isSideboardCategory = false; + bool includedInDeck = false; + + for (const auto &cat : card.getCategories()) { + + if (cat.name.compare("Commander", Qt::CaseInsensitive) == 0) { + isCommander = true; + } + + if (cat.name.compare("Sideboard", Qt::CaseInsensitive) == 0 || + cat.name.compare("Maybeboard", Qt::CaseInsensitive) == 0) { + isSideboardCategory = true; + } + + if (cat.includedInDeck) { + includedInDeck = true; + } + } + + QString target; + + if (isCommander || isSideboardCategory) { + sideStream << line; + } else if (includedInDeck) { + mainStream << line; + } else { + sideStream << line; + } + } + + // Combine with blank line separator + tempDeck = mainboardText; + + if (!sideboardText.isEmpty()) { + tempDeck += "\n"; + tempDeck += sideboardText; } model = new DeckListModel(this); From a20f3c0fb449bcc7d3ed6b9f49914eb1e83646fd Mon Sep 17 00:00:00 2001 From: Vorliz <39461522+Vorliz@users.noreply.github.com> Date: Tue, 21 Apr 2026 18:05:31 +0100 Subject: [PATCH 12/13] Fix #6659: Correct logging for bottom-of-library card moves (#6764) * Fix #6659: Correct logging for bottom-of-library card moves Cause: - This issue happens due to logic of moving the card from the top of the deck being reused when moving from the bottom of the deck, in a way that makes it impossible to check if the card came from the bottom. Resolution: - Updated the logging logic in the client for card moves. - Added a gRPC parameter ('is_from_bottom') for card moves. - Updates the server logic to reverse the order of the card move if the 'is_from_bottom' parameter is true. - Added a test to show the expected behaviour of the fix. NOTE: While the changes in this patch seem big, this is due to changing the loop in the moveCard function to a helper function, in order to make the bug fix change. The only change to the loop was to pass a variable attribution to the moveCard function because it was redundant to be in the loop. * chore: run format on test * refactor: new way to check if a move is from the bottom of the deck * refactor: change isFromBottom check to static function * update comments Co-authored-by: ebbit1q --------- Co-authored-by: ebbit1q --- .../src/game/log/message_log_widget.cpp | 2 +- .../remote/game/server_abstract_player.cpp | 372 ++++++++++-------- .../remote/game/server_abstract_player.h | 13 + tests/CMakeLists.txt | 1 + tests/movecard_tests/CMakeLists.txt | 16 + .../movecard_tests/reverse_card_move_test.cpp | 91 +++++ tests/movecard_tests/server_test_helpers.h | 42 ++ 7 files changed, 380 insertions(+), 157 deletions(-) create mode 100755 tests/movecard_tests/CMakeLists.txt create mode 100644 tests/movecard_tests/reverse_card_move_test.cpp create mode 100644 tests/movecard_tests/server_test_helpers.h diff --git a/cockatrice/src/game/log/message_log_widget.cpp b/cockatrice/src/game/log/message_log_widget.cpp index c38e433eb..09f6e656b 100644 --- a/cockatrice/src/game/log/message_log_widget.cpp +++ b/cockatrice/src/game/log/message_log_widget.cpp @@ -54,7 +54,7 @@ MessageLogWidget::getFromStr(CardZoneLogic *zone, QString cardName, int position fromStr = tr(" from the top of their library"); } } - } else if (position >= zone->getCards().size() - 1) { + } else if (position == zone->getCards().size()) { if (cardName.isEmpty()) { if (ownerChange) { cardName = tr("the bottom card of %1's library").arg(zone->getPlayer()->getPlayerInfo()->getName()); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp index f04bcc849..7c0437bf0 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp @@ -49,6 +49,7 @@ #include #include #include +#include Server_AbstractPlayer::Server_AbstractPlayer(Server_Game *_game, int _playerId, @@ -228,6 +229,37 @@ shouldBeFaceDown(const MoveCardStruct &cardStruct, const Server_CardZone *startZ return false; } +/** + * @brief Determines whether a set of moved cards is from the bottom of the deck + */ +static bool shouldBeFromTheBottom(const Server_CardZone *startZone, const std::set &cardsToMove) +{ + if (!startZone) { + return false; + } + + if (startZone->getName() != ZoneNames::DECK) { + return false; + } + + int movedCount = static_cast(cardsToMove.size()); + int tailStart = startZone->getCards().size() - movedCount; + if (tailStart <= 0) { // if the entire deck is moved it should not be considered from the bottom + return false; + } + + // check if the move is a contiguous block at the end of the deck, fail fast when not + int expectedPosition = tailStart; + for (const auto &card : cardsToMove) { + if (card.position != expectedPosition) { + return false; + } + ++expectedPosition; + } + + return true; +} + Response::ResponseCode Server_AbstractPlayer::moveCard(GameEventStorage &ges, Server_CardZone *startzone, const QList &_cards, @@ -244,8 +276,11 @@ Response::ResponseCode Server_AbstractPlayer::moveCard(GameEventStorage &ges, return Response::RespContextError; } - if (!targetzone->hasCoords() && (xCoord <= -1)) { - xCoord = targetzone->getCards().size(); + if (!targetzone->hasCoords()) { + yCoord = 0; + if (xCoord <= -1) { + xCoord = targetzone->getCards().size(); + } } std::set cardsToMove; @@ -285,164 +320,21 @@ Response::ResponseCode Server_AbstractPlayer::moveCard(GameEventStorage &ges, bool revealTopStart = false; bool revealTopTarget = false; - for (auto cardStruct : cardsToMove) { - Server_Card *card = cardStruct.card; - int originalPosition = cardStruct.position; + bool isFromBottom = shouldBeFromTheBottom(startzone, cardsToMove); - bool sourceBeingLookedAt; - int position = startzone->removeCard(card, sourceBeingLookedAt); - - // Attachment relationships can be retained when moving a card onto the opponent's table - if (startzone->getName() != targetzone->getName()) { - // Delete all attachment relationships - if (card->getParentCard()) { - card->setParentCard(nullptr); - } - - // Make a copy of the list because the original one gets modified during the loop - QList attachedCards = card->getAttachedCards(); - for (auto &attachedCard : attachedCards) { - attachedCard->getZone()->getPlayer()->unattachCard(ges, attachedCard); - } + if (isFromBottom) { + std::ranges::reverse_view reversedCardsToMove{cardsToMove}; + for (auto card : reversedCardsToMove) { + processMoveCard(ges, startzone, targetzone, card, xCoord, yCoord, xIndex, revealTopStart, revealTopTarget, + isReversed, undoingDraw); } - - if (startzone != targetzone) { - // Delete all arrows from and to the card - for (auto *player : game->getPlayers().values()) { - QList arrowsToDelete; - for (Server_Arrow *arrow : player->getArrows()) { - if ((arrow->getStartCard() == card) || (arrow->getTargetItem() == card)) - arrowsToDelete.append(arrow->getId()); - } - for (int j : arrowsToDelete) { - player->deleteArrow(j); - } - } - } - - if (shouldDestroyOnMove(card, startzone, targetzone)) { - Event_DestroyCard event; - event.set_zone_name(startzone->getName().toStdString()); - event.set_card_id(static_cast(card->getId())); - ges.enqueueGameEvent(event, playerId); - - if (Server_Card *stashedCard = card->takeStashedCard()) { - stashedCard->setId(newCardId()); - ges.enqueueGameEvent(makeCreateTokenEvent(startzone, stashedCard, card->getX(), card->getY()), - playerId); - card->deleteLater(); - card = stashedCard; - } else { - card->deleteLater(); - card = nullptr; - } - } - - if (card) { - ++xIndex; - int newX = isReversed ? targetzone->getCards().size() - xCoord + xIndex : xCoord + xIndex; - - bool faceDown = shouldBeFaceDown(cardStruct, startzone, targetzone); - - if (targetzone->hasCoords()) { - newX = targetzone->getFreeGridColumn(newX, yCoord, card->getName(), faceDown); - } else { - yCoord = 0; - card->resetState(targetzone->getName() == ZoneNames::STACK); - } - - targetzone->insertCard(card, newX, yCoord); - int targetLookedCards = targetzone->getCardsBeingLookedAt(); - bool sourceKnownToPlayer = isReversed || (sourceBeingLookedAt && !card->getFaceDown()); - if (targetzone->getType() == ServerInfo_Zone::HiddenZone && targetLookedCards >= newX) { - if (sourceKnownToPlayer) { - targetLookedCards += 1; - } else { - targetLookedCards = newX; - } - targetzone->setCardsBeingLookedAt(targetLookedCards); - } - - bool targetHiddenToOthers = faceDown || (targetzone->getType() != ServerInfo_Zone::PublicZone); - bool sourceHiddenToOthers = card->getFaceDown() || (startzone->getType() != ServerInfo_Zone::PublicZone); - - int oldCardId = card->getId(); - if ((faceDown && (startzone != targetzone)) || (targetzone->getPlayer() != startzone->getPlayer())) { - card->setId(targetzone->getPlayer()->newCardId()); - } - card->setFaceDown(faceDown); - - Event_MoveCard eventOthers; - eventOthers.set_start_player_id(startzone->getPlayer()->getPlayerId()); - eventOthers.set_start_zone(startzone->getName().toStdString()); - eventOthers.set_target_player_id(targetzone->getPlayer()->getPlayerId()); - if (startzone != targetzone) { - eventOthers.set_target_zone(targetzone->getName().toStdString()); - } - eventOthers.set_y(yCoord); - eventOthers.set_face_down(faceDown); - - Event_MoveCard eventPrivate(eventOthers); - if (sourceBeingLookedAt || targetzone->getType() != ServerInfo_Zone::HiddenZone || - startzone->getType() != ServerInfo_Zone::HiddenZone) { - eventPrivate.set_card_id(oldCardId); - eventPrivate.set_new_card_id(card->getId()); - } else { - eventPrivate.set_card_id(-1); - eventPrivate.set_new_card_id(-1); - } - if (sourceKnownToPlayer || !(faceDown || targetzone->getType() == ServerInfo_Zone::HiddenZone)) { - QString privateCardName = card->getName(); - eventPrivate.set_card_name(privateCardName.toStdString()); - eventPrivate.set_new_card_provider_id(card->getProviderId().toStdString()); - } - if (startzone->getType() == ServerInfo_Zone::HiddenZone) { - eventPrivate.set_position(position); - } else { - eventPrivate.set_position(-1); - } - - eventPrivate.set_x(newX); - - if ( - // cards from public zones have their id known, their previous position is already known, the event does - // not accomodate for previous locations in zones with coordinates (which are always public) - (startzone->getType() != ServerInfo_Zone::PublicZone) && - // other players are not allowed to be able to track which card is which in private zones like the hand - (startzone->getType() != ServerInfo_Zone::PrivateZone)) { - eventOthers.set_position(position); - } - if ( - // other players are not allowed to be able to track which card is which in private zones like the hand - (targetzone->getType() != ServerInfo_Zone::PrivateZone)) { - eventOthers.set_x(newX); - } - - if ((startzone->getType() == ServerInfo_Zone::PublicZone) || - (targetzone->getType() == ServerInfo_Zone::PublicZone)) { - eventOthers.set_card_id(oldCardId); - if (!(sourceHiddenToOthers && targetHiddenToOthers)) { - QString publicCardName = card->getName(); - eventOthers.set_card_name(publicCardName.toStdString()); - eventOthers.set_new_card_provider_id(card->getProviderId().toStdString()); - } - eventOthers.set_new_card_id(card->getId()); - } - - ges.enqueueGameEvent(eventPrivate, playerId, GameEventStorageItem::SendToPrivate, playerId); - ges.enqueueGameEvent(eventOthers, playerId, GameEventStorageItem::SendToOthers); - - if (originalPosition == 0) { - revealTopStart = true; - } - if (newX == 0) { - revealTopTarget = true; - } - - // handle side effects for this card - onCardBeingMoved(ges, cardStruct, startzone, targetzone, undoingDraw); + } else { + for (auto card : cardsToMove) { + processMoveCard(ges, startzone, targetzone, card, xCoord, yCoord, xIndex, revealTopStart, revealTopTarget, + isReversed, undoingDraw); } } + if (revealTopStart) { revealTopCardIfNeeded(startzone, ges); } @@ -462,6 +354,174 @@ Response::ResponseCode Server_AbstractPlayer::moveCard(GameEventStorage &ges, return Response::RespOk; } +void Server_AbstractPlayer::processMoveCard(GameEventStorage &ges, + Server_CardZone *startzone, + Server_CardZone *targetzone, + MoveCardStruct cardStruct, + int xCoord, + int yCoord, + int &xIndex, + bool &revealTopStart, + bool &revealTopTarget, + bool isReversed, + bool undoingDraw) +{ + Server_Card *card = cardStruct.card; + int originalPosition = cardStruct.position; + + bool sourceBeingLookedAt; + int position = startzone->removeCard(card, sourceBeingLookedAt); + + // Attachment relationships can be retained when moving a card onto the opponent's table + if (startzone->getName() != targetzone->getName()) { + // Delete all attachment relationships + if (card->getParentCard()) { + card->setParentCard(nullptr); + } + + // Make a copy of the list because the original one gets modified during the loop + QList attachedCards = card->getAttachedCards(); + for (auto &attachedCard : attachedCards) { + attachedCard->getZone()->getPlayer()->unattachCard(ges, attachedCard); + } + } + + if (startzone != targetzone) { + // Delete all arrows from and to the card + for (auto *player : game->getPlayers().values()) { + QList arrowsToDelete; + for (Server_Arrow *arrow : player->getArrows()) { + if ((arrow->getStartCard() == card) || (arrow->getTargetItem() == card)) + arrowsToDelete.append(arrow->getId()); + } + for (int j : arrowsToDelete) { + player->deleteArrow(j); + } + } + } + + if (shouldDestroyOnMove(card, startzone, targetzone)) { + Event_DestroyCard event; + event.set_zone_name(startzone->getName().toStdString()); + event.set_card_id(static_cast(card->getId())); + ges.enqueueGameEvent(event, playerId); + + if (Server_Card *stashedCard = card->takeStashedCard()) { + stashedCard->setId(newCardId()); + ges.enqueueGameEvent(makeCreateTokenEvent(startzone, stashedCard, card->getX(), card->getY()), playerId); + card->deleteLater(); + card = stashedCard; + } else { + card->deleteLater(); + card = nullptr; + } + } + + if (card) { + ++xIndex; + int newX = isReversed ? targetzone->getCards().size() - xCoord + xIndex : xCoord + xIndex; + + bool faceDown = shouldBeFaceDown(cardStruct, startzone, targetzone); + + if (targetzone->hasCoords()) { + newX = targetzone->getFreeGridColumn(newX, yCoord, card->getName(), faceDown); + } else { + card->resetState(targetzone->getName() == ZoneNames::STACK); + } + + targetzone->insertCard(card, newX, yCoord); + int targetLookedCards = targetzone->getCardsBeingLookedAt(); + bool sourceKnownToPlayer = isReversed || (sourceBeingLookedAt && !card->getFaceDown()); + if (targetzone->getType() == ServerInfo_Zone::HiddenZone && targetLookedCards >= newX) { + if (sourceKnownToPlayer) { + targetLookedCards += 1; + } else { + targetLookedCards = newX; + } + targetzone->setCardsBeingLookedAt(targetLookedCards); + } + + bool targetHiddenToOthers = faceDown || (targetzone->getType() != ServerInfo_Zone::PublicZone); + bool sourceHiddenToOthers = card->getFaceDown() || (startzone->getType() != ServerInfo_Zone::PublicZone); + + int oldCardId = card->getId(); + if ((faceDown && (startzone != targetzone)) || (targetzone->getPlayer() != startzone->getPlayer())) { + card->setId(targetzone->getPlayer()->newCardId()); + } + card->setFaceDown(faceDown); + + Event_MoveCard eventOthers; + eventOthers.set_start_player_id(startzone->getPlayer()->getPlayerId()); + eventOthers.set_start_zone(startzone->getName().toStdString()); + eventOthers.set_target_player_id(targetzone->getPlayer()->getPlayerId()); + if (startzone != targetzone) { + eventOthers.set_target_zone(targetzone->getName().toStdString()); + } + eventOthers.set_y(yCoord); + eventOthers.set_face_down(faceDown); + + Event_MoveCard eventPrivate(eventOthers); + if (sourceBeingLookedAt || targetzone->getType() != ServerInfo_Zone::HiddenZone || + startzone->getType() != ServerInfo_Zone::HiddenZone) { + eventPrivate.set_card_id(oldCardId); + eventPrivate.set_new_card_id(card->getId()); + } else { + eventPrivate.set_card_id(-1); + eventPrivate.set_new_card_id(-1); + } + if (sourceKnownToPlayer || !(faceDown || targetzone->getType() == ServerInfo_Zone::HiddenZone)) { + QString privateCardName = card->getName(); + eventPrivate.set_card_name(privateCardName.toStdString()); + eventPrivate.set_new_card_provider_id(card->getProviderId().toStdString()); + } + if (startzone->getType() == ServerInfo_Zone::HiddenZone) { + eventPrivate.set_position(position); + } else { + eventPrivate.set_position(-1); + } + + eventPrivate.set_x(newX); + + if ( + // cards from public zones have their id known, their previous position is already known, the event does + // not accomodate for previous locations in zones with coordinates (which are always public) + (startzone->getType() != ServerInfo_Zone::PublicZone) && + // other players are not allowed to be able to track which card is which in private zones like the hand + (startzone->getType() != ServerInfo_Zone::PrivateZone)) { + eventOthers.set_position(position); + } + if ( + // other players are not allowed to be able to track which card is which in private zones like the hand + (targetzone->getType() != ServerInfo_Zone::PrivateZone)) { + eventOthers.set_x(newX); + } + + if ((startzone->getType() == ServerInfo_Zone::PublicZone) || + (targetzone->getType() == ServerInfo_Zone::PublicZone)) { + eventOthers.set_card_id(oldCardId); + if (!(sourceHiddenToOthers && targetHiddenToOthers)) { + QString publicCardName = card->getName(); + eventOthers.set_card_name(publicCardName.toStdString()); + eventOthers.set_new_card_provider_id(card->getProviderId().toStdString()); + } + eventOthers.set_new_card_id(card->getId()); + } + + ges.enqueueGameEvent(eventPrivate, playerId, GameEventStorageItem::SendToPrivate, playerId); + ges.enqueueGameEvent(eventOthers, playerId, GameEventStorageItem::SendToOthers); + + if (originalPosition == 0) { + revealTopStart = true; + } + if (newX == 0) { + revealTopTarget = true; + } + + // handle side effects for this card + onCardBeingMoved(ges, cardStruct, startzone, targetzone, undoingDraw); + } +} + void Server_AbstractPlayer::onCardBeingMoved(GameEventStorage &ges, const MoveCardStruct &cardStruct, Server_CardZone *startzone, diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.h index 40fe84aa1..9d9809298 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.h @@ -93,6 +93,19 @@ public: bool fixFreeSpaces = true, bool undoingDraw = false, bool isReversed = false); + + void processMoveCard(GameEventStorage &ges, + Server_CardZone *startzone, + Server_CardZone *targetzone, + MoveCardStruct cardStruct, + int xCoord, + int yCoord, + int &xIndex, + bool &revealTopStart, + bool &revealTopTarget, + bool isReversed, + bool undoingDraw); + virtual void onCardBeingMoved(GameEventStorage &ges, const MoveCardStruct &cardStruct, Server_CardZone *startzone, diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c5346e59f..fffaf1bda 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -65,4 +65,5 @@ target_link_libraries( add_subdirectory(card_zone_algorithms) add_subdirectory(carddatabase) add_subdirectory(loading_from_clipboard) +add_subdirectory(movecard_tests) add_subdirectory(oracle) diff --git a/tests/movecard_tests/CMakeLists.txt b/tests/movecard_tests/CMakeLists.txt new file mode 100755 index 000000000..769047148 --- /dev/null +++ b/tests/movecard_tests/CMakeLists.txt @@ -0,0 +1,16 @@ +add_executable(reverse_card_move_test reverse_card_move_test.cpp) + +if(NOT GTEST_FOUND) + add_dependencies(reverse_card_move_test gtest) +endif() + +target_link_libraries( + reverse_card_move_test + PRIVATE libcockatrice_network_server_remote + PRIVATE libcockatrice_rng + PRIVATE Threads::Threads + PRIVATE ${GTEST_BOTH_LIBRARIES} + PRIVATE ${TEST_QT_MODULES} +) + +add_test(NAME reverse_card_move_test COMMAND reverse_card_move_test) diff --git a/tests/movecard_tests/reverse_card_move_test.cpp b/tests/movecard_tests/reverse_card_move_test.cpp new file mode 100644 index 000000000..2231a7e3b --- /dev/null +++ b/tests/movecard_tests/reverse_card_move_test.cpp @@ -0,0 +1,91 @@ +#include "game/server_abstract_player.h" +#include "game/server_card.h" +#include "game/server_cardzone.h" +#include "game/server_game.h" +#include "server_response_containers.h" +#include "server_room.h" +#include "server_test_helpers.h" + +#include +#include +#include +#include +#include + +RNG_Abstract *rng = nullptr; // this needs to be defined due to other functions in server + +TEST(ReverseCardMoveTest, MoveCardFromBottomTest) +{ + ServerInfo_User user; + user.set_name("test-user"); + + // instantiate a fake server instance + FakeServer server; + Server_Room room(0, 0, "", "", "", "", false, "", {}, &server); + Server_Game game(user, 1, "", "", 2, QList(), false, false, false, false, false, false, 20, false, &room); + Server_AbstractPlayer player(&game, 1, user, false, nullptr); + Server_CardZone deckZone(&player, ZoneNames::DECK, true, ServerInfo_Zone::PublicZone); + Server_CardZone exileZone(&player, ZoneNames::EXILE, true, ServerInfo_Zone::PublicZone); + + // setup the deck with 20 useless cards + for (int i = 0; i < 20; i++) { + auto *cardUseless = new Server_Card({"Card Useless", "card-Useless"}, player.newCardId(), i, 0); + deckZone.insertCard(cardUseless, i, 0); + } + + // add 4 cards to the end of it + auto *cardA = new Server_Card({"Card A", "card-a"}, player.newCardId(), 20, 0); + auto *cardB = new Server_Card({"Card B", "card-b"}, player.newCardId(), 21, 0); + auto *cardC = new Server_Card({"Card C", "card-c"}, player.newCardId(), 22, 0); + auto *cardD = new Server_Card({"Card D", "card-d"}, player.newCardId(), 23, 0); + + deckZone.insertCard(cardA, 20, 0); + deckZone.insertCard(cardB, 21, 0); + deckZone.insertCard(cardC, 22, 0); + deckZone.insertCard(cardD, 23, 0); + + // try to move them, with the expected client given order (n-3, n-2, n-1, n) + CardToMove moveA; + moveA.set_card_id(cardA->getId()); + CardToMove moveB; + moveB.set_card_id(cardB->getId()); + CardToMove moveC; + moveC.set_card_id(cardC->getId()); + CardToMove moveD; + moveD.set_card_id(cardD->getId()); + + QList cardsToMove = {&moveA, &moveB, &moveC, &moveD}; + GameEventStorage ges; + + const auto response = player.moveCard(ges, &deckZone, cardsToMove, &exileZone, 0, 0, false, false, false); + + EXPECT_EQ(response, Response::RespOk); + + int positionA; + int positionB; + int positionC; + int positionD; + // find the cards in the destination zone and check they are the right card + EXPECT_EQ(exileZone.getCard(cardA->getId(), &positionA), cardA); + EXPECT_EQ(exileZone.getCard(cardB->getId(), &positionB), cardB); + EXPECT_EQ(exileZone.getCard(cardC->getId(), &positionC), cardC); + EXPECT_EQ(exileZone.getCard(cardD->getId(), &positionD), cardD); + + // check that they are at the expected index + EXPECT_EQ(cardA->getX(), 3); + EXPECT_EQ(cardB->getX(), 2); + EXPECT_EQ(cardC->getX(), 1); + EXPECT_EQ(cardD->getX(), 0); + + // also check if the given positions are correct + EXPECT_EQ(positionA, 3); + EXPECT_EQ(positionB, 2); + EXPECT_EQ(positionC, 1); + EXPECT_EQ(positionD, 0); +} + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tests/movecard_tests/server_test_helpers.h b/tests/movecard_tests/server_test_helpers.h new file mode 100644 index 000000000..fd2ed6c17 --- /dev/null +++ b/tests/movecard_tests/server_test_helpers.h @@ -0,0 +1,42 @@ +#include "server.h" +#include "server_database_interface.h" + +class MockDatabaseInterface : public Server_DatabaseInterface +{ +public: + AuthenticationResult checkUserPassword(Server_ProtocolHandler *, + const QString &, + const QString &, + const QString &, + QString &, + int &, + bool) override + { + return NotLoggedIn; + } + ServerInfo_User getUserData(const QString &, bool) override + { + return ServerInfo_User(); + } + int getNextGameId() override + { + return 1; + } + int getNextReplayId() override + { + return 1; + } + int getActiveUserCount(QString) override + { + return 1; + } +}; + +class FakeServer : public Server +{ +public: + FakeServer() + { + setDatabaseInterface(new MockDatabaseInterface()); + } +}; From b1fe4c85d38b4ee3335ecdecfab02c172a9dabb3 Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Thu, 23 Apr 2026 02:28:12 +0200 Subject: [PATCH 13/13] fix sending decks to tappedout (#6832) --- .../src/client/network/interfaces/tapped_out_interface.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp b/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp index a30a7f531..af377d176 100644 --- a/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp +++ b/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp @@ -89,6 +89,8 @@ void TappedOutInterface::analyzeDeck(const DeckList &deck) QNetworkRequest request(QUrl("https://tappedout.net/mtg-decks/paste/")); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING)); + // we interpret the redirect and open it in the browser instead, do not follow redirects + request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy); manager->post(request, data); }