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()); + } +};