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:
diff --git a/CMakeLists.txt b/CMakeLists.txt
index fe808a652..4a5e944c4 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -74,11 +74,11 @@ 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)
- set(GIT_TAG_RELEASENAME "Omenpath")
+ set(GIT_TAG_RELEASENAME "Graduation Day")
endif()
# Use c++20 for all targets
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/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);
}
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/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/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();
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();
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.
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);
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()
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;
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.
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/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);
}
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());
+ }
+};