diff --git a/.ci/compile.sh b/.ci/compile.sh index 9be8ba741..bf5a169af 100755 --- a/.ci/compile.sh +++ b/.ci/compile.sh @@ -311,24 +311,16 @@ if [[ $RUNNER_OS == macOS ]]; then if [[ $MAKE_PACKAGE ]]; then # Workaround https://github.com/actions/runner-images/issues/7522 - # have hdiutil repeat the command 10 times in hope of success. - # The wrapper has to exit non-zero once it gives up: CPack reads that status to - # decide whether the disk image was created, so a zero exit turns a failed hdiutil - # into a much later, misleading "Problem copying the package" error that hides - # hdiutil's own message. + # have hdiutil repeat the command 10 times in hope of success hdiutil_script="/tmp/hdiutil.sh" # shellcheck disable=SC2016 echo '#!/bin/bash - attempts=10 i=0 - while true; do - hdiutil "$@" && exit 0 - rc=$? - if (( ++i >= attempts )); then - echo "::error::hdiutil $1 failed $i times (last status $rc), giving up" >&2 - exit "$rc" + while ! hdiutil "$@"; do + if (( ++i >= 10 )); then + echo "Error: hdiutil failed $i times!" >&2 + break fi - echo "hdiutil $1 attempt $i/$attempts failed (status $rc), retrying" >&2 sleep 1 done' >"$hdiutil_script" chmod +x "$hdiutil_script" diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index df054ea26..54068f5a0 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -101,36 +101,6 @@ jobs: --title "$release_name" \ --notes-file "$body_path" - vcpkg-baseline: - name: vcpkg baseline - runs-on: ubuntu-slim - steps: - - name: "Checkout" - uses: actions/checkout@v7 - - # vcpkg-tool reads versions/baseline.json out of the commit named by - # `builtin-baseline`, through the `vcpkg` submodule's git history. A submodule - # pointer that lags the manifest - e.g. a stale `vcpkg/` worktree captured by - # `git add` in a PR - makes every vcpkg leg fail in configure with "failed to - # `git show` versions/baseline.json". That reads like a baseline or network - # problem, when the pointer is what has to move. Check the two agree here in - # seconds instead of on five build legs. - - name: "Check the submodule pointer against the manifest baseline" - shell: bash - run: | - pointer="$(git rev-parse HEAD:vcpkg 2>/dev/null || true)" - baseline="$(sed -nE 's/.*"builtin-baseline"[[:space:]]*:[[:space:]]*"([0-9a-f]{40})".*/\1/p' vcpkg.json)" - if [[ -z "$pointer" || -z "$baseline" ]]; then - echo "::error file=vcpkg.json::could not read the vcpkg submodule pointer or the builtin-baseline" - exit 1 - fi - if [[ "$pointer" != "$baseline" ]]; then - echo "::error file=vcpkg.json::vcpkg submodule is at $pointer but builtin-baseline is $baseline" - echo "Move them together: git -C vcpkg checkout $baseline && git add vcpkg" - exit 1 - fi - echo "vcpkg submodule and builtin-baseline agree on $baseline" - build-linux: strategy: fail-fast: false @@ -496,7 +466,7 @@ jobs: vcpkg_upload: 1 # only one leg per vcpkg_scope hands its archives over name: ${{ matrix.os }} ${{ matrix.target }}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }} - needs: [configure, vcpkg-baseline] + needs: configure runs-on: ${{ matrix.runner }} timeout-minutes: 100 env: diff --git a/CMakeLists.txt b/CMakeLists.txt index 8a25fe2c6..d9acabc5e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -90,7 +90,7 @@ endif() # A project name is needed for CPack # Version can be overriden by git tags, see cmake/getversion.cmake -project("Cockatrice" VERSION 3.2.0) +project("Cockatrice" VERSION 3.1.2) # Set release name if not provided via env/cmake var if(NOT DEFINED GIT_TAG_RELEASENAME) @@ -259,13 +259,7 @@ else() add_compile_definitions(QT_DISABLE_DEPRECATED_BEFORE=0x060400) endif() -# Set once here for every target: these variables are inherited by the -# subdirectories, so the per-directory copies were redundant. Only the -# variables are set; qt_standard_project_setup would also change the runtime -# output directories and the install RPATH. set(CMAKE_AUTOMOC TRUE) -set(CMAKE_AUTOUIC TRUE) -set(CMAKE_AUTORCC TRUE) # Find other needed libraries find_package(Protobuf CONFIG) diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index b3fbae6d5..50e79c7a2 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -171,7 +171,6 @@ set(cockatrice_SOURCES src/interface/widgets/cards/card_info_picture_widget.cpp src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.cpp src/interface/widgets/cards/card_info_text_widget.cpp - src/interface/widgets/cards/card_picture_delegate.cpp src/interface/widgets/cards/card_size_widget.cpp src/interface/widgets/cards/deck_card_zone_display_widget.cpp src/interface/widgets/cards/deck_preview_card_picture_widget.cpp @@ -292,7 +291,6 @@ set(cockatrice_SOURCES src/interface/widgets/utility/sequence_edit.cpp src/interface/widgets/utility/visibility_change_listener.cpp src/interface/widgets/utility/visibility_change_listener.h - src/interface/widgets/visual_database_display/card_database_visual_view.cpp src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp src/interface/widgets/visual_database_display/visual_database_display_filter_save_load_widget.cpp src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp diff --git a/cockatrice/src/client/network/connection_controller/remote_connection_controller.cpp b/cockatrice/src/client/network/connection_controller/remote_connection_controller.cpp index c702f91f1..020e53877 100644 --- a/cockatrice/src/client/network/connection_controller/remote_connection_controller.cpp +++ b/cockatrice/src/client/network/connection_controller/remote_connection_controller.cpp @@ -3,7 +3,6 @@ #include "../../../interface/pixel_map_generator.h" #include "../../settings/cache_settings.h" #include "../interface/widgets/dialogs/dlg_connect.h" -#include "../interface/widgets/dialogs/dlg_edit_password.h" #include "../interface/widgets/dialogs/dlg_forgot_password_challenge.h" #include "../interface/widgets/dialogs/dlg_forgot_password_request.h" #include "../interface/widgets/dialogs/dlg_forgot_password_reset.h" @@ -100,10 +99,6 @@ void ConnectionController::wireClientSignals() connect(remoteClient, &RemoteClient::sigPasswordVerifierReady, this, &ConnectionController::onPasswordVerifierReady); - - connect(remoteClient, &RemoteClient::sigPasswordChangeSuccess, this, - &ConnectionController::onPasswordChangeSuccess); - connect(remoteClient, &RemoteClient::sigPasswordChangeError, this, &ConnectionController::onPasswordChangeError); } void ConnectionController::connectToServer() @@ -356,16 +351,10 @@ void ConnectionController::onLoginError(int r, } case Response::RespPasswordChangeRequired: { - // The credentials were right, but the account may not be used until a new password - // is chosen. Let the user complete the change here: sending them off to "ask the - // administrator for your temporary password" sends them in circles, since they just - // logged in with it and the session ends without a way to comply. - DlgEditPassword dlg(dialogParent); - dlg.setWindowTitle(tr("Password Change Required")); - if (dlg.exec()) { - remoteClient->changePasswordToServer(dlg.getOldPassword(), dlg.getNewPassword()); - return; // onPasswordChangeSuccess/onPasswordChangeError decides the next step - } + QMessageBox::information( + dialogParent, tr("Password Change Required"), + tr("An administrator has reset your password. Please contact your server administrator to obtain " + "your temporary password, then log in and change it via Account -> Change Password.")); remoteClient->disconnectFromServer(); return; } @@ -528,43 +517,6 @@ void ConnectionController::onNotifyUserAboutUpdate() "To update your client, go to Help -> Check for Updates.")); } -void ConnectionController::onPasswordChangeSuccess() -{ - // The credential saved for this profile is stale now: drop it so the next login asks for - // the new password instead of replaying the old verifier. - if (!pendingSaveName.isEmpty()) { - SettingsCache::instance().servers().setServerPassword(pendingSaveName, ""); - } - - QMessageBox::information(dialogParent, tr("Password Changed"), - tr("Your password has been changed. Please log in again with your new password.")); - remoteClient->disconnectFromServer(); - connectToServer(); -} - -void ConnectionController::onPasswordChangeError(int responseCode) -{ - QString reason; - switch (static_cast(responseCode)) { - case Response::RespWrongPassword: - reason = tr("The current password is incorrect."); - break; - case Response::RespPasswordTooShort: - reason = tr("The new password is too short."); - break; - case Response::RespClientUpdateRequired: - reason = tr("The server does not accept the new password format."); - break; - default: - reason = tr("The password could not be changed (error %1).").arg(responseCode); - break; - } - - QMessageBox::critical(dialogParent, tr("Password Change Failed"), reason); - remoteClient->disconnectFromServer(); - connectToServer(); -} - void ConnectionController::onForgotPasswordSuccess() { QMessageBox::information( diff --git a/cockatrice/src/client/network/connection_controller/remote_connection_controller.h b/cockatrice/src/client/network/connection_controller/remote_connection_controller.h index 2987a8d99..c8f96a9a9 100644 --- a/cockatrice/src/client/network/connection_controller/remote_connection_controller.h +++ b/cockatrice/src/client/network/connection_controller/remote_connection_controller.h @@ -94,10 +94,6 @@ private slots: // Persists the derived scrypt verifier after a successful challenge-response login void onPasswordVerifierReady(const QString &verifier); - // Completes the forced password change the server demanded at login - void onPasswordChangeSuccess(); - void onPasswordChangeError(int responseCode); - private: void wireClientSignals(); void updateWindowTitle(); diff --git a/cockatrice/src/filters/filter_tree_model.cpp b/cockatrice/src/filters/filter_tree_model.cpp index 9b55ce2fd..b3bf307f1 100644 --- a/cockatrice/src/filters/filter_tree_model.cpp +++ b/cockatrice/src/filters/filter_tree_model.cpp @@ -11,27 +11,6 @@ class QObject; -namespace -{ -/** - * @brief Visits every filter item below @p node, depth first. - */ -template void forEachFilter(const FilterTreeNode *node, const Callback &callback) -{ - if (node == nullptr) { - return; - } - - if (const auto *filterItem = dynamic_cast(node)) { - callback(filterItem); - } - - for (int i = 0; i < node->childCount(); ++i) { - forEachFilter(node->nodeAt(i), callback); - } -} -} // namespace - FilterTreeModel::FilterTreeModel(QObject *parent) : QAbstractItemModel(parent) { fTree = new FilterTree; @@ -131,31 +110,46 @@ void FilterTreeModel::clearFiltersOfType(CardFilter::Attr filterType) QList FilterTreeModel::getFiltersOfType(CardFilter::Attr filterType) const { QList filters; - forEachFilter(fTree, [&](const FilterItem *filterItem) { - if (filterItem->attr() == filterType) { - filters.append(new CardFilter(filterItem->text(), filterItem->type(), filterItem->attr())); - } - }); - return filters; -} + if (!fTree) { + return filters; + } -QStringList FilterTreeModel::getFilterTermsOfType(CardFilter::Attr filterType) const -{ - QStringList terms; - forEachFilter(fTree, [&](const FilterItem *filterItem) { - if (filterItem->attr() == filterType) { - terms.append(filterItem->text()); + std::function traverse; + traverse = [&](const FilterTreeNode *node) { + if (const auto *filterItem = dynamic_cast(node)) { + if (filterItem->attr() == filterType) { + QString text = filterItem->text(); + filters.append(new CardFilter(text, filterItem->type(), filterItem->attr())); + } } - }); - return terms; + for (int i = 0; i < node->childCount(); ++i) { + traverse(node->nodeAt(i)); + } + }; + + traverse(fTree); + return filters; } QList FilterTreeModel::allFilters() const { QList filters; - forEachFilter(fTree, [&](const FilterItem *filterItem) { - filters.append(new CardFilter(filterItem->text(), filterItem->type(), filterItem->attr())); - }); + if (!fTree) { + return filters; + } + + std::function traverse; + traverse = [&](const FilterTreeNode *node) { + if (const auto *filterItem = dynamic_cast(node)) { + QString text = filterItem->text(); + filters.append(new CardFilter(text, filterItem->type(), filterItem->attr())); + } + for (int i = 0; i < node->childCount(); ++i) { + traverse(node->nodeAt(i)); + } + }; + + traverse(fTree); return filters; } diff --git a/cockatrice/src/filters/filter_tree_model.h b/cockatrice/src/filters/filter_tree_model.h index 05f34f508..826fcc941 100644 --- a/cockatrice/src/filters/filter_tree_model.h +++ b/cockatrice/src/filters/filter_tree_model.h @@ -28,14 +28,7 @@ public slots: void addFilter(const CardFilter *f); void removeFilter(const CardFilter *f); void clearFiltersOfType(CardFilter::Attr filterType); - /** - * @brief Copies every active filter of one attribute into new CardFilter objects. - * - * The caller owns the returned filters. Prefer getFilterTermsOfType() when only - * the terms are needed. - */ [[nodiscard]] QList getFiltersOfType(CardFilter::Attr filterType) const; - /// @brief Copies every active filter into new CardFilter objects. The caller owns them. [[nodiscard]] QList allFilters() const; private slots: @@ -55,13 +48,6 @@ public: { return fTree; } - - /** - * @brief The texts of every active filter of one attribute, without allocating filters. - * - * Prefer this over getFiltersOfType() when only the filter terms are needed. - */ - [[nodiscard]] QStringList getFilterTermsOfType(CardFilter::Attr filterType) const; [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; [[nodiscard]] int columnCount(const QModelIndex & /*parent*/ = QModelIndex()) const override; [[nodiscard]] QVariant data(const QModelIndex &index, int role) const override; diff --git a/cockatrice/src/game_graphics/board/abstract_card_drag_item.cpp b/cockatrice/src/game_graphics/board/abstract_card_drag_item.cpp index c707b4022..2e18fca2d 100644 --- a/cockatrice/src/game_graphics/board/abstract_card_drag_item.cpp +++ b/cockatrice/src/game_graphics/board/abstract_card_drag_item.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include #include #include @@ -52,20 +51,7 @@ AbstractCardDragItem::AbstractCardDragItem(AbstractCardItem *_item, update(); }); - // The source card can be destroyed mid-drag (e.g. moved by a server event). deleteLater() alone is not enough: - // a scene repaint can run before the deferred delete and paint the dead card. Detach from the scene immediately. - connect(_item, &QObject::destroyed, this, [this]() { - hide(); - if (QGraphicsScene *sc = scene()) { - sc->removeItem(this); - } - deleteLater(); - }); -} - -AbstractCardItem *AbstractCardDragItem::getItem() const -{ - return item.data(); + connect(item, &QObject::destroyed, this, &AbstractCardDragItem::deleteLater); } QPainterPath AbstractCardDragItem::shape() const @@ -79,10 +65,6 @@ QPainterPath AbstractCardDragItem::shape() const void AbstractCardDragItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { - if (item.isNull()) { - return; - } - item->paint(painter, option, widget); // adds a mask to the card so it looks like the card hasnt been placed yet diff --git a/cockatrice/src/game_graphics/board/abstract_card_drag_item.h b/cockatrice/src/game_graphics/board/abstract_card_drag_item.h index 80e40ff1d..18afdcbb8 100644 --- a/cockatrice/src/game_graphics/board/abstract_card_drag_item.h +++ b/cockatrice/src/game_graphics/board/abstract_card_drag_item.h @@ -14,7 +14,6 @@ #include #include #include -#include #include #include @@ -25,8 +24,7 @@ class AbstractCardDragItem : public QObject, public QGraphicsItem Q_OBJECT Q_INTERFACES(QGraphicsItem) protected: - /// Guarded pointer: the source card may be destroyed while this drag item still exists. - QPointer item; + AbstractCardItem *item; QPointF hotSpot; QList childDrags; @@ -46,8 +44,10 @@ public: } [[nodiscard]] QPainterPath shape() const override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; - /// @return the source card, or nullptr if it has already been destroyed. - [[nodiscard]] AbstractCardItem *getItem() const; + [[nodiscard]] AbstractCardItem *getItem() const + { + return item; + } [[nodiscard]] QPointF getHotSpot() const { return hotSpot; diff --git a/cockatrice/src/game_graphics/board/card_drag_item.cpp b/cockatrice/src/game_graphics/board/card_drag_item.cpp index b263e8c92..265ea759c 100644 --- a/cockatrice/src/game_graphics/board/card_drag_item.cpp +++ b/cockatrice/src/game_graphics/board/card_drag_item.cpp @@ -130,9 +130,8 @@ void CardDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) sc->removeItem(this); QList dragItemList; - CardZoneLogic *startZone = static_cast(item.data())->getZone(); - if (currentZone && - !(static_cast(item.data())->getAttachedTo() && (startZone == currentZone->getLogic()))) { + CardZoneLogic *startZone = static_cast(item)->getZone(); + if (currentZone && !(static_cast(item)->getAttachedTo() && (startZone == currentZone->getLogic()))) { if (!occupied) { dragItemList.append(this); } @@ -140,7 +139,7 @@ void CardDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) for (int i = 0; i < childDrags.size(); i++) { CardDragItem *c = static_cast(childDrags[i]); if (!occupied && - !(static_cast(c->item.data())->getAttachedTo() && (startZone == currentZone->getLogic())) && + !(static_cast(c->item)->getAttachedTo() && (startZone == currentZone->getLogic())) && !c->occupied) { dragItemList.append(c); } diff --git a/cockatrice/src/game_graphics/deckview/deck_view.cpp b/cockatrice/src/game_graphics/deckview/deck_view.cpp index 6e0b0eaaa..630530792 100644 --- a/cockatrice/src/game_graphics/deckview/deck_view.cpp +++ b/cockatrice/src/game_graphics/deckview/deck_view.cpp @@ -77,7 +77,7 @@ void DeckViewCardDragItem::updatePosition(const QPointF &cursorScenePos) void DeckViewCardDragItem::handleDrop(DeckViewCardContainer *target) { - auto *card = static_cast(item.data()); + auto *card = static_cast(item); auto *start = static_cast(item->parentItem()); start->removeCard(card); target->addCard(card); diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp index 2d35538d2..4f77c7096 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp @@ -238,7 +238,6 @@ void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image) }); card.emitPixmapUpdated(); - emit getInstance().pixmapLoaded(card); } void CardPictureLoader::deleteAllLocalOverrides(const ExactCard &card) diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader.h b/cockatrice/src/interface/card_picture_loader/card_picture_loader.h index 0c73e5d59..c6d91570b 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader.h +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader.h @@ -146,14 +146,6 @@ signals: /** @brief Emitted after the worker has finished clearing the network and redirect caches. */ void networkCacheCleared(); - /** - * @brief Emitted once an image has been loaded and put into the pixmap cache. - * - * Views that paint cards from the loader without owning a widget per card can - * listen to this to repaint the rows whose picture just became available. - */ - void pixmapLoaded(const ExactCard &card); - private slots: /** * @brief Triggered when the user changes the picture download settings. diff --git a/cockatrice/src/interface/intents/intent_login.cpp b/cockatrice/src/interface/intents/intent_login.cpp index 8cc574cb0..0d8390015 100644 --- a/cockatrice/src/interface/intents/intent_login.cpp +++ b/cockatrice/src/interface/intents/intent_login.cpp @@ -27,9 +27,9 @@ void IntentGetLoginCredentials::onPreconditionSatisfied() ServersSettings &servers = SettingsCache::instance().servers(); const int index = servers.findServerIndex(context->hostname, context->port); - if (const auto server = servers.getServer(index)) { - context->username = server->username; - context->password = server->password; + if (index >= 0) { + context->username = servers.getValue(QString("username%1").arg(index), "server", "server_details").toString(); + context->password = servers.getValue(QString("password%1").arg(index), "server", "server_details").toString(); emitFinished(); } else { emitFailed(tr("No saved credentials for this server")); @@ -69,8 +69,9 @@ void IntentGetLoginCredentials::onPreconditionNotSatisfied() // it with the raw hostname when addNewServer updates the entry in place. QString saveName = context->hostname; const int existingIndex = servers.findServerIndex(context->hostname, context->port); - if (const auto existing = servers.getServer(existingIndex)) { - saveName = existing->saveName; + if (existingIndex >= 0) { + saveName = + servers.getValue(QString("saveName%1").arg(existingIndex), "server", "server_details").toString(); if (saveName.isEmpty()) { saveName = context->hostname; } diff --git a/cockatrice/src/interface/intents/url_parser.cpp b/cockatrice/src/interface/intents/url_parser.cpp index e0b7bb2fb..1e482a4f1 100644 --- a/cockatrice/src/interface/intents/url_parser.cpp +++ b/cockatrice/src/interface/intents/url_parser.cpp @@ -418,10 +418,12 @@ void IntentUrlParser::restoreToPreviousServer(const PendingIntentChain &chain) ServersSettings &servers = SettingsCache::instance().servers(); const int index = servers.findServerIndex(chain.previousServerHost, chain.previousServerPort); - const auto server = servers.getServer(index); - if (server && servers.hasLoginData(chain.previousServerHost, chain.previousServerPort)) { - client->connectToServer(chain.previousServerHost, chain.previousServerPort.toUInt(), server->username, - server->password); + if (index >= 0 && servers.hasLoginData(chain.previousServerHost, chain.previousServerPort)) { + const QString username = + servers.getValue(QString("username%1").arg(index), "server", "server_details").toString(); + const QString password = + servers.getValue(QString("password%1").arg(index), "server", "server_details").toString(); + client->connectToServer(chain.previousServerHost, chain.previousServerPort.toUInt(), username, password); return; } client->disconnectFromServer(); @@ -436,9 +438,11 @@ void IntentUrlParser::restoreToPreviousServer(const PendingIntentChain &chain) // when credentials are saved, otherwise stay offline. ServersSettings &servers = SettingsCache::instance().servers(); const int index = servers.findServerIndex(chain.previousServerHost, chain.previousServerPort); - const auto server = servers.getServer(index); - if (server && servers.hasLoginData(chain.previousServerHost, chain.previousServerPort)) { - client->connectToServer(chain.previousServerHost, chain.previousServerPort.toUInt(), server->username, - server->password); + if (index >= 0 && servers.hasLoginData(chain.previousServerHost, chain.previousServerPort)) { + const QString username = + servers.getValue(QString("username%1").arg(index), "server", "server_details").toString(); + const QString password = + servers.getValue(QString("password%1").arg(index), "server", "server_details").toString(); + client->connectToServer(chain.previousServerHost, chain.previousServerPort.toUInt(), username, password); } } diff --git a/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp b/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp index 8f02ebcde..0d94e7209 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp @@ -43,6 +43,11 @@ #include #include +static constexpr qreal MTG_CARD_ASPECT_RATIO = 1.396; +// static constexpr qreal YUGIOH_CARD_ASPECT_RATIO = 1.457; +static constexpr qreal ASPECT_RATIO = MTG_CARD_ASPECT_RATIO; + +static constexpr int BASE_WIDTH = 200; static constexpr int BASE_HEIGHT = 200; static constexpr int ENLARGED_PIXMAP_OFFSET = 10; @@ -157,11 +162,12 @@ void CardInfoPictureWidget::resizeEvent(QResizeEvent *event) */ void CardInfoPictureWidget::setScaleFactor(const int scale) { - const QSize newSize = sizeForScaleFactor(scale); + const int newWidth = BASE_WIDTH * scale / 100; + const int newHeight = static_cast(newWidth * ASPECT_RATIO); scaleFactor = scale; - setFixedSize(newSize); + setFixedSize(newWidth, newHeight); updatePixmap(); emit cardScaleFactorChanged(scale); @@ -260,13 +266,8 @@ void CardInfoPictureWidget::paintEvent(QPaintEvent *event) */ QSize CardInfoPictureWidget::sizeHint() const { - return sizeForScaleFactor(scaleFactor); -} - -QSize CardInfoPictureWidget::sizeForScaleFactor(int scaleFactor) -{ - const int width = BASE_WIDTH * scaleFactor / 100; - return {width, static_cast(width * ASPECT_RATIO)}; + return {static_cast(BASE_WIDTH * scaleFactor / 100.0), + static_cast(BASE_WIDTH * scaleFactor / 100.0 * ASPECT_RATIO)}; } /** diff --git a/cockatrice/src/interface/widgets/cards/card_info_picture_widget.h b/cockatrice/src/interface/widgets/cards/card_info_picture_widget.h index 0eb731d92..8d2d63736 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_picture_widget.h +++ b/cockatrice/src/interface/widgets/cards/card_info_picture_widget.h @@ -28,11 +28,6 @@ class CardInfoPictureWidget : public QWidget Q_OBJECT public: - /// Width of a card picture at 100 % scale, in pixels. - static constexpr int BASE_WIDTH = 200; - /// Height / width of a card picture. - static constexpr qreal ASPECT_RATIO = 1.396; - explicit CardInfoPictureWidget(QWidget *parent = nullptr, bool hoverToZoomEnabled = false, bool raiseOnEnter = false); @@ -42,12 +37,6 @@ public: } [[nodiscard]] QSize sizeHint() const override; - /** - * @brief The size a card picture has at @p scaleFactor percent. - * @param scaleFactor Percentage of BASE_WIDTH, as set by the card size slider. - */ - [[nodiscard]] static QSize sizeForScaleFactor(int scaleFactor); - public slots: void setCard(const ExactCard &card); void setScaleFactor(int scale); // New slot for scaling diff --git a/cockatrice/src/interface/widgets/cards/card_picture_delegate.cpp b/cockatrice/src/interface/widgets/cards/card_picture_delegate.cpp deleted file mode 100644 index 781c767ef..000000000 --- a/cockatrice/src/interface/widgets/cards/card_picture_delegate.cpp +++ /dev/null @@ -1,83 +0,0 @@ -#include "card_picture_delegate.h" - -#include "../../../client/settings/cache_settings.h" -#include "../../card_picture_loader/card_picture_loader.h" -#include "card_art_utils.h" -#include "card_database_visual_display_model.h" -#include "libcockatrice/card/printing/exact_card.h" -#include "libcockatrice/settings/cards_display_settings.h" - -#include -#include -#include -#include -#include -#include - -CardPictureDelegate::CardPictureDelegate(CardDatabaseVisualDisplayModel *_model, QObject *parent) - : QStyledItemDelegate(parent), model(_model) -{ -} - -void CardPictureDelegate::setCardSize(const QSize &size) -{ - if (cardSize == size) { - return; - } - - cardSize = size; -} - -void CardPictureDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const -{ - if (model == nullptr) { - return; - } - - const ExactCard card = model->cardAt(index.row()); - if (!card) { - return; - } - - // The grid cell is the card plus SPACING; give the picture the rest. - const int inset = SPACING / 2; - const QRect cardArea = option.rect.adjusted(inset, inset, -inset, -inset); - - QPixmap pixmap; - CardPictureLoader::getCardBackLoadingInProgressPixmap(pixmap, cardArea.size()); - CardPictureLoader::getPixmap(pixmap, card, cardArea.size()); - - if (pixmap.isNull()) { - return; - } - - if (SettingsCache::instance().cardsDisplay().getAutoRotateSidewaysLayoutCards()) { - pixmap = CardArtUtils::rotateSidewaysLayoutArt(pixmap, card); - } - - // Fit the picture into the cell and center it, like CardInfoPictureWidget does. - const QSize pictureSize = pixmap.deviceIndependentSize().toSize().scaled(cardArea.size(), Qt::KeepAspectRatio); - const QRect pictureRect(cardArea.center().x() - pictureSize.width() / 2, - cardArea.center().y() - pictureSize.height() / 2, pictureSize.width(), - pictureSize.height()); - - const qreal radius = SettingsCache::instance().cardsDisplay().getRoundCardCorners() - ? 0.05 * static_cast(pictureRect.width()) - : 0.0; - - painter->save(); - painter->setRenderHint(QPainter::Antialiasing); - painter->setRenderHint(QPainter::SmoothPixmapTransform); - QPainterPath shape; - shape.addRoundedRect(pictureRect, radius, radius); - painter->setClipPath(shape); - // Source rect is in device pixels, target in logical ones: drawPixmap scales - // between them without the device pixel ratio being applied twice. - painter->drawPixmap(pictureRect, pixmap, QRectF(pixmap.rect())); - painter->restore(); -} - -QSize CardPictureDelegate::sizeHint(const QStyleOptionViewItem & /*option*/, const QModelIndex & /*index*/) const -{ - return cardSize + QSize(SPACING, SPACING); -} diff --git a/cockatrice/src/interface/widgets/cards/card_picture_delegate.h b/cockatrice/src/interface/widgets/cards/card_picture_delegate.h deleted file mode 100644 index 31e673624..000000000 --- a/cockatrice/src/interface/widgets/cards/card_picture_delegate.h +++ /dev/null @@ -1,45 +0,0 @@ -/** - * @file card_picture_delegate.h - * @ingroup CardWidgets - * @brief Paints a card picture for a visual card grid item. - */ - -#ifndef CARD_PICTURE_DELEGATE_H -#define CARD_PICTURE_DELEGATE_H - -#include -#include -#include - -class CardDatabaseVisualDisplayModel; - -/** - * @brief Draws the card of a CardDatabaseVisualDisplayModel row as a picture. - * - * The delegate only paints rows the view asks for, so a grid of the whole card - * database stays cheap: no widget per card and no pixmap loaded for items that - * are off screen. Card images come from CardPictureLoader, which caches them and - * loads missing ones asynchronously. - */ -class CardPictureDelegate : public QStyledItemDelegate -{ - Q_OBJECT - -public: - /// Gap left around a card picture inside its grid cell, in pixels. - static constexpr int SPACING = 4; - - explicit CardPictureDelegate(CardDatabaseVisualDisplayModel *_model, QObject *parent = nullptr); - - /// The picture size inside a grid cell. The cell is this size plus SPACING. - void setCardSize(const QSize &size); - - void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; - [[nodiscard]] QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; - -private: - CardDatabaseVisualDisplayModel *model; - QSize cardSize; -}; - -#endif // CARD_PICTURE_DELEGATE_H diff --git a/cockatrice/src/interface/widgets/cards/card_size_widget.cpp b/cockatrice/src/interface/widgets/cards/card_size_widget.cpp index ac1ab9cb1..ab1840591 100644 --- a/cockatrice/src/interface/widgets/cards/card_size_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_size_widget.cpp @@ -2,7 +2,6 @@ #include "../general/layout_containers/flow_widget.h" -#include #include #include #include @@ -34,8 +33,8 @@ CardSizeWidget::CardSizeWidget(QWidget *parent, FlowWidget *_flowWidget, int def cardSizeLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); cardSizeSlider = new QSlider(Qt::Horizontal, this); - cardSizeSlider->setRange(MIN_CARD_SIZE, MAX_CARD_SIZE); ///< Slider range for card size adjustment. - cardSizeSlider->setValue(defaultValue); ///< Initial slider value. + cardSizeSlider->setRange(50, 250); ///< Slider range for card size adjustment. + cardSizeSlider->setValue(defaultValue); ///< Initial slider value. cardSizeLayout->addWidget(cardSizeLabel); cardSizeLayout->addWidget(cardSizeSlider); @@ -74,26 +73,13 @@ QSlider *CardSizeWidget::getSlider() const void CardSizeWidget::enableCtrlScrollResize(QWidget *host) { - const auto watchScrollArea = [this](QAbstractScrollArea *scrollArea) { - if (scrollArea == nullptr) { - return; - } - if (QWidget *viewport = scrollArea->viewport()) { - viewport->installEventFilter(this); - } - if (auto *contentScrollArea = qobject_cast(scrollArea)) { - if (QWidget *content = contentScrollArea->widget()) { - content->installEventFilter(this); - } - } - }; - host->installEventFilter(this); - watchScrollArea(qobject_cast(host)); - const auto scrollAreas = host->findChildren(); - for (QAbstractScrollArea *scrollArea : scrollAreas) { - watchScrollArea(scrollArea); + const auto scrollAreas = host->findChildren(); + for (QScrollArea *scrollArea : scrollAreas) { + if (QWidget *content = scrollArea->widget()) { + content->installEventFilter(this); + } } } diff --git a/cockatrice/src/interface/widgets/cards/card_size_widget.h b/cockatrice/src/interface/widgets/cards/card_size_widget.h index fced90d33..7de97a5e9 100644 --- a/cockatrice/src/interface/widgets/cards/card_size_widget.h +++ b/cockatrice/src/interface/widgets/cards/card_size_widget.h @@ -24,10 +24,6 @@ class CardSizeWidget : public QWidget Q_OBJECT public: - /// Range of the card size slider, as a percentage of the base card width. - static constexpr int MIN_CARD_SIZE = 50; - static constexpr int MAX_CARD_SIZE = 250; - explicit CardSizeWidget(QWidget *parent, FlowWidget *flowWidget = nullptr, int defaultValue = 100); [[nodiscard]] QSlider *getSlider() const; @@ -35,9 +31,8 @@ public: * @brief Resizes the cards when the user Ctrl + scrolls over @p host or any of its descendants. * * Installs this widget as an event filter on the given host widget. If the host contains a - * scroll area the filter is also installed on the scroll area's content widget, and on the - * viewport of every scroll area base (QListView, QTableView, ...), so the resize intercepts - * the wheel event before the scroll area would scroll the view. + * scroll area the filter is also installed on the scroll area's content widget, so the resize + * intercepts the wheel event before the scroll area would scroll the view. */ void enableCtrlScrollResize(QWidget *host); diff --git a/cockatrice/src/interface/widgets/replay/replay_event_index.cpp b/cockatrice/src/interface/widgets/replay/replay_event_index.cpp index 89abd1e34..47eec7f59 100644 --- a/cockatrice/src/interface/widgets/replay/replay_event_index.cpp +++ b/cockatrice/src/interface/widgets/replay/replay_event_index.cpp @@ -1,6 +1,6 @@ #include "replay_event_index.h" -#include +#include "../../../game/phase.h" #include "libcockatrice/protocol/get_pb_extension.h" #include "libcockatrice/protocol/pb/card_attributes.pb.h" #include "libcockatrice/protocol/pb/context_concede.pb.h" diff --git a/cockatrice/src/interface/widgets/server/user/user_info_box.cpp b/cockatrice/src/interface/widgets/server/user/user_info_box.cpp index fa7698037..4feb73538 100644 --- a/cockatrice/src/interface/widgets/server/user/user_info_box.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_info_box.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -298,7 +299,17 @@ void UserInfoBox::actPassword() void UserInfoBox::changePassword(const QString &oldPassword, const QString &newPassword) { - const Command_AccountPassword cmd = client->makeAccountPasswordCommand(oldPassword, newPassword); + Command_AccountPassword cmd; + cmd.set_old_password(oldPassword.toStdString()); + if (client->getServerSupportsChallengeResponse()) { + cmd.set_hashed_new_password(PasswordHasher::generatePasswordVerifier(newPassword).toStdString()); + } else if (client->getServerSupportsPasswordHash()) { + auto passwordSalt = PasswordHasher::generateRandomSalt(); + QString hashedPassword = PasswordHasher::computeHash(newPassword, passwordSalt); + cmd.set_hashed_new_password(hashedPassword.toStdString()); + } else { + cmd.set_new_password(newPassword.toStdString()); + } PendingCommand *pend = client->prepareSessionCommand(cmd); connect(pend, &PendingCommand::finished, this, &UserInfoBox::processPasswordResponse); diff --git a/cockatrice/src/interface/widgets/server/user/user_info_connection.cpp b/cockatrice/src/interface/widgets/server/user/user_info_connection.cpp index 2e864824c..d23ac63ba 100644 --- a/cockatrice/src/interface/widgets/server/user/user_info_connection.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_info_connection.cpp @@ -29,10 +29,19 @@ QMap> UserConnection_Inf ServersSettings &servers = SettingsCache::instance().servers(); - for (const auto &entry : servers.getServers()) { - UserConnection_Information userInfo(entry.saveName, entry.host, entry.port, entry.username, entry.password, - entry.savePassword, entry.site); - serverList.insert(entry.saveName, std::make_pair(entry.host, userInfo)); + int size = servers.getValue("totalServers", "server", "server_details").toInt() + 1; + + for (int i = 0; i < size; i++) { + QString _saveName = servers.getValue(QString("saveName%1").arg(i), "server", "server_details").toString(); + QString serverName = servers.getValue(QString("server%1").arg(i), "server", "server_details").toString(); + QString portNum = servers.getValue(QString("port%1").arg(i), "server", "server_details").toString(); + QString userName = servers.getValue(QString("username%1").arg(i), "server", "server_details").toString(); + QString pass = servers.getValue(QString("password%1").arg(i), "server", "server_details").toString(); + bool savePass = servers.getValue(QString("savePassword%1").arg(i), "server", "server_details").toBool(); + QString _site = servers.getValue(QString("site%1").arg(i), "server", "server_details").toString(); + + UserConnection_Information userInfo(_saveName, serverName, portNum, userName, pass, savePass, _site); + serverList.insert(_saveName, std::make_pair(serverName, userInfo)); } return serverList; @@ -44,18 +53,28 @@ QStringList UserConnection_Information::getServerInfo(const QString &find) ServersSettings &servers = SettingsCache::instance().servers(); - for (const auto &entry : servers.getServers()) { - if (find != entry.saveName) { + int size = servers.getValue("totalServers", "server", "server_details").toInt() + 1; + for (int i = 0; i < size; i++) { + QString _saveName = servers.getValue(QString("saveName%1").arg(i), "server", "server_details").toString(); + + if (find != _saveName) { continue; } - _server.append(entry.saveName); - _server.append(entry.host); - _server.append(entry.port); - _server.append(entry.username); - _server.append(entry.password); - _server.append(entry.savePassword ? "1" : "0"); - _server.append(entry.site); + QString serverName = servers.getValue(QString("server%1").arg(i), "server", "server_details").toString(); + QString portNum = servers.getValue(QString("port%1").arg(i), "server", "server_details").toString(); + QString userName = servers.getValue(QString("username%1").arg(i), "server", "server_details").toString(); + QString pass = servers.getValue(QString("password%1").arg(i), "server", "server_details").toString(); + bool savePass = servers.getValue(QString("savePassword%1").arg(i), "server", "server_details").toBool(); + QString _site = servers.getValue(QString("site%1").arg(i), "server", "server_details").toString(); + + _server.append(_saveName); + _server.append(serverName); + _server.append(portNum); + _server.append(userName); + _server.append(pass); + _server.append(savePass ? "1" : "0"); + _server.append(_site); break; } diff --git a/cockatrice/src/interface/widgets/tabs/tab_logs.cpp b/cockatrice/src/interface/widgets/tabs/tab_logs.cpp index 9509f48ec..67e076983 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_logs.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_logs.cpp @@ -13,7 +13,6 @@ #include #include #include -#include #include #include #include @@ -22,7 +21,6 @@ #include #include #include -#include #include #include #include @@ -33,17 +31,6 @@ class TabSupervisor; -namespace -{ -/** @brief Identifies the newest row of a page well enough to recognise a repeated page. */ -QString pageStartKey(const Response_ViewLogHistory &response) -{ - const ServerInfo_ChatMessage &first = response.log_message(0); - return QString::fromStdString(first.time()) + QChar(0x1f) + QString::fromStdString(first.sender_name()) + - QChar(0x1f) + QString::fromStdString(first.message()); -} -} // namespace - TabLog::TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client, bool _canUseDeveloperCommands) : Tab(_tabSupervisor), client(_client), canUseDeveloperCommands(_canUseDeveloperCommands) { @@ -65,20 +52,12 @@ TabLog::TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client, bool _can chatTable->setHorizontalHeaderLabels( QString(tr("Time;SenderName;SenderIP;Message;TargetID;TargetName")).split(";")); - tabManager = new QTabWidget(); + QTabWidget *tabManager = new QTabWidget(); tabManager->addTab(roomTable, tr("Room Logs")); tabManager->addTab(gameTable, tr("Game Logs")); tabManager->addTab(chatTable, tr("Chat Logs")); setCentralWidget(tabManager); - connect(roomTable->verticalScrollBar(), &QScrollBar::valueChanged, this, &TabLog::checkFetchMore); - connect(gameTable->verticalScrollBar(), &QScrollBar::valueChanged, this, &TabLog::checkFetchMore); - connect(chatTable->verticalScrollBar(), &QScrollBar::valueChanged, this, &TabLog::checkFetchMore); - // A tab that gains only a few rows has nothing to scroll, so its scrollbar never fires: - // switching to it would leave its next page unfetched until the moderator scrolled - // another tab. Re-check on every tab switch so the visible table can continue the browse. - connect(tabManager, &QTabWidget::currentChanged, this, &TabLog::checkFetchMore); - createDock(); restartLayout(); clearClicked(); @@ -95,9 +74,12 @@ void TabLog::retranslateUi() void TabLog::getClicked() { - // Empty filters are a valid browse: room/game history going back from - // today, paginated. Only require a date range + location, both of which - // get sensible defaults below so a bare "Get" just works. + if (findUsername->text().isEmpty() && findIPAddress->text().isEmpty() && findGameName->text().isEmpty() && + findGameID->text().isEmpty() && findMessage->text().isEmpty()) { + QMessageBox::critical(this, tr("Error"), tr("You must select at least one filter.")); + return; + } + if (!lastHour->isChecked() && !today->isChecked() && !pastDays->isChecked()) { pastDays->setChecked(true); pastXDays->setValue(20); @@ -117,71 +99,39 @@ void TabLog::getClicked() } if (maximumResults->value() == 0) { - maximumResults->setValue(200); + maximumResults->setValue(1000); } - pageSize = qBound(1, maximumResults->value(), 1000); - // Build the command once, here, and let every page copy it: rebuilding it from the - // widgets per page would run page 2+ against filters edited after Get was clicked, - // appending rows selected by a different query under the first page's results. - currentQuery.Clear(); - currentQuery.set_user_name(findUsername->text().toStdString()); - currentQuery.set_ip_address(findIPAddress->text().toStdString()); - currentQuery.set_game_name(findGameName->text().toStdString()); - currentQuery.set_game_id(findGameID->text().toStdString()); - currentQuery.set_message(findMessage->text().toStdString()); - if (mainRoom->isChecked()) { - currentQuery.add_log_location("room"); - } - if (gameRoom->isChecked()) { - currentQuery.add_log_location("game"); - } - if (privateChat->isChecked()) { - currentQuery.add_log_location("chat"); - } - currentQuery.set_date_range(dateRangeForSelection()); - currentQuery.set_maximum_results(pageSize); - hasCurrentQuery = true; - snapshotBefore.clear(); - lastPageStartKey.clear(); - - // Fresh search: reset pagination state and clear previous pages. - currentOffset = 0; - roomCounter = 0; - gameCounter = 0; - chatCounter = 0; - roomTable->setRowCount(0); - gameTable->setRowCount(0); - chatTable->setRowCount(0); - - fetchPage(0); -} - -int TabLog::dateRangeForSelection() const -{ + int dateRange = 0; if (lastHour->isChecked()) { - return 1; + dateRange = 1; } + if (today->isChecked()) { - return 24; + dateRange = 24; } + if (pastDays->isChecked()) { - return pastXDays->value() * 24; - } - return 0; -} - -void TabLog::fetchPage(int offset) -{ - if (isFetching || !hasCurrentQuery) { - return; + dateRange = pastXDays->value() * 24; } - Command_ViewLogHistory cmd = currentQuery; - cmd.set_offset(offset); - if (!snapshotBefore.isEmpty()) { - cmd.set_before_time(snapshotBefore.toStdString()); - } + Command_ViewLogHistory cmd; + cmd.set_user_name(findUsername->text().toStdString()); + cmd.set_ip_address(findIPAddress->text().toStdString()); + cmd.set_game_name(findGameName->text().toStdString()); + cmd.set_game_id(findGameID->text().toStdString()); + cmd.set_message(findMessage->text().toStdString()); + if (mainRoom->isChecked()) { + cmd.add_log_location("room"); + }; + if (gameRoom->isChecked()) { + cmd.add_log_location("game"); + }; + if (privateChat->isChecked()) { + cmd.add_log_location("chat"); + }; + cmd.set_date_range(dateRange); + cmd.set_maximum_results(maximumResults->value()); PendingCommand *pend; if (canUseDeveloperCommands) { @@ -191,42 +141,10 @@ void TabLog::fetchPage(int offset) pend = client->prepareModeratorCommand(cmd); } - isFetching = true; - getButton->setEnabled(false); - connect(pend, &PendingCommand::finished, this, &TabLog::viewLogHistory_processResponse); client->sendCommand(pend); } -void TabLog::checkFetchMore() -{ - if (isFetching || !hasMoreResults) { - return; - } - - // Only the table the moderator is looking at may pull the next page. Checking all - // three would make an empty or hidden table nearBottom by definition (maximum() and - // value() are both zero), so a room-only browse chained page after page without any - // scrolling - pulling the whole range with a growing OFFSET scan per page, and - // looping forever against a server that ignores `offset`. - auto *table = qobject_cast(tabManager->currentWidget()); - if (table == nullptr || table->rowCount() == 0) { - return; - } - - // ScrollPerItem (the default) counts rows, not pixels: `value` is the top visible row - // and `maximum` the last one that can be scrolled to. Prefetch half a page ahead so - // scrolling never visibly stalls, without the fixed 300-row threshold firing the next - // page the moment a page loads. - const QScrollBar *bar = table->verticalScrollBar(); - const int prefetchRows = qMax(1, pageSize / 2); - if (bar->maximum() - bar->value() > prefetchRows) { - return; - } - - fetchPage(currentOffset); -} - void TabLog::clearClicked() { findUsername->clear(); @@ -248,10 +166,6 @@ void TabLog::clearClicked() pastDays->setAutoExclusive(true); today->setAutoExclusive(true); lastHour->setAutoExclusive(true); - currentOffset = 0; - hasMoreResults = false; - hasCurrentQuery = false; - snapshotBefore.clear(); } void TabLog::createDock() @@ -295,15 +209,14 @@ void TabLog::createDock() pastXDays = new QSpinBox; pastXDays->setMaximum(20); - labelMaximum = new QLabel(tr("Results per page: ")); + labelMaximum = new QLabel(tr("Maximum Results: ")); maximumResults = new QSpinBox; maximumResults->setMaximum(1000); - maximumResults->setToolTip(tr("Page size for smooth scrolling: more rows load automatically as you scroll.")); - labelDescription = new QLabel(tr("Leave filters empty to browse recent room and game messages.\n" - "More rows load automatically as you scroll.")); + labelDescription = new QLabel(tr( + "At least one filter is required.\nThe more information you put in, the more specific your results will be.")); - getButton = new QPushButton(tr("Get Logs")); + getButton = new QPushButton(tr("Get User Logs")); getButton->setAutoDefault(true); connect(getButton, &QPushButton::clicked, this, &TabLog::getClicked); @@ -349,7 +262,7 @@ void TabLog::createDock() maxResultsGrid->addWidget(labelMaximum, 0, 0); maxResultsGrid->addWidget(maximumResults, 0, 1); - maxResultsGroupBox = new QGroupBox(tr("Results per page")); + maxResultsGroupBox = new QGroupBox(tr("Maximum Results")); maxResultsGroupBox->setLayout(maxResultsGrid); descriptionGrid = new QGridLayout; @@ -382,134 +295,84 @@ void TabLog::createDock() searchDock->setWidget(searchDockContents); } -void TabLog::appendChatMessage(QTableWidget *table, - int &counter, - const QString &time, - const QString &senderName, - const QString &senderIp, - const QString &messageText, - const QString &targetId, - const QString &targetName, - const QString &channel) -{ - table->insertRow(counter); - table->setItem(counter, 0, new QTableWidgetItem(time)); - table->setItem(counter, 1, new QTableWidgetItem(senderName)); - table->setItem(counter, 2, new QTableWidgetItem(senderIp)); - table->setItem(counter, 3, new QTableWidgetItem(messageText)); - table->setItem(counter, 4, new QTableWidgetItem(targetId)); - table->setItem(counter, 5, new QTableWidgetItem(targetName)); - if (table->columnCount() > 6) { - table->setItem(counter, 6, new QTableWidgetItem(channel)); - } - ++counter; -} - void TabLog::viewLogHistory_processResponse(const Response &resp) { const Response_ViewLogHistory &response = resp.GetExtension(Response_ViewLogHistory::ext); - const bool isFirstPage = (currentOffset == 0); - isFetching = false; - getButton->setEnabled(true); - if (resp.response_code() != Response::RespOk) { QMessageBox::critical(static_cast(parent()), tr("Message History"), tr("Failed to collect message history information.")); return; } - if (isFirstPage && response.log_message_size() == 0) { + if (response.log_message_size() == 0) { QMessageBox::information(static_cast(parent()), tr("Message History"), tr("There are no messages for the selected filters.")); - hasMoreResults = false; return; } - const QString thisPageStart = response.log_message_size() > 0 ? pageStartKey(response) : QString(); - - if (isFirstPage) { - // Rows come newest first, so the first one fixes the snapshot every later page is - // bounded by. Without it, each message logged between two page fetches pushes the - // window down by one and repeats a row the moderator already has. - snapshotBefore = QString::fromStdString(response.log_message(0).time()); - } else if (!response.has_has_more() && !thisPageStart.isEmpty() && thisPageStart == lastPageStartKey) { - // A server that predates `offset` ignores it and returns the same page again. It - // also predates `has_more`, so the full-page heuristic below can never prove - // exhaustion and every scroll would append the same rows forever. The identical - // first row is the proof; stop instead of looping. - hasMoreResults = false; - return; - } - lastPageStartKey = thisPageStart; + int roomCounter = 0, gameCounter = 0, chatCounter = 0; + roomTable->setRowCount(roomCounter); + gameTable->setRowCount(gameCounter); + chatTable->setRowCount(chatCounter); for (int i = 0; i < response.log_message_size(); ++i) { ServerInfo_ChatMessage message = response.log_message(i); const QString targetType = QString::fromStdString(message.target_type()); if (targetType == "room" || targetType == "room_channel") { - appendChatMessage(roomTable, roomCounter, QString::fromStdString(message.time()), - QString::fromStdString(message.sender_name()), - QString::fromStdString(message.sender_ip()), QString::fromStdString(message.message()), - QString::fromStdString(message.target_id()), - QString::fromStdString(message.target_name()), QString::fromStdString(message.channel())); + roomTable->insertRow(roomCounter); + roomTable->setItem(roomCounter, 0, new QTableWidgetItem(QString::fromStdString(message.time()))); + roomTable->setItem(roomCounter, 1, new QTableWidgetItem(QString::fromStdString(message.sender_name()))); + roomTable->setItem(roomCounter, 2, new QTableWidgetItem(QString::fromStdString(message.sender_ip()))); + roomTable->setItem(roomCounter, 3, new QTableWidgetItem(QString::fromStdString(message.message()))); + roomTable->setItem(roomCounter, 4, new QTableWidgetItem(QString::fromStdString(message.target_id()))); + roomTable->setItem(roomCounter, 5, new QTableWidgetItem(QString::fromStdString(message.target_name()))); + roomTable->setItem(roomCounter, 6, new QTableWidgetItem(QString::fromStdString(message.channel()))); + ++roomCounter; } if (targetType == "game") { - appendChatMessage(gameTable, gameCounter, QString::fromStdString(message.time()), - QString::fromStdString(message.sender_name()), - QString::fromStdString(message.sender_ip()), QString::fromStdString(message.message()), - QString::fromStdString(message.target_id()), - QString::fromStdString(message.target_name())); + gameTable->insertRow(gameCounter); + gameTable->setItem(gameCounter, 0, new QTableWidgetItem(QString::fromStdString(message.time()))); + gameTable->setItem(gameCounter, 1, new QTableWidgetItem(QString::fromStdString(message.sender_name()))); + gameTable->setItem(gameCounter, 2, new QTableWidgetItem(QString::fromStdString(message.sender_ip()))); + gameTable->setItem(gameCounter, 3, new QTableWidgetItem(QString::fromStdString(message.message()))); + gameTable->setItem(gameCounter, 4, new QTableWidgetItem(QString::fromStdString(message.target_id()))); + gameTable->setItem(gameCounter, 5, new QTableWidgetItem(QString::fromStdString(message.target_name()))); + ++gameCounter; } if (targetType == "chat") { - appendChatMessage(chatTable, chatCounter, QString::fromStdString(message.time()), - QString::fromStdString(message.sender_name()), - QString::fromStdString(message.sender_ip()), QString::fromStdString(message.message()), - QString::fromStdString(message.target_id()), - QString::fromStdString(message.target_name())); + chatTable->insertRow(chatCounter); + chatTable->setItem(chatCounter, 0, new QTableWidgetItem(QString::fromStdString(message.time()))); + chatTable->setItem(chatCounter, 1, new QTableWidgetItem(QString::fromStdString(message.sender_name()))); + chatTable->setItem(chatCounter, 2, new QTableWidgetItem(QString::fromStdString(message.sender_ip()))); + chatTable->setItem(chatCounter, 3, new QTableWidgetItem(QString::fromStdString(message.message()))); + chatTable->setItem(chatCounter, 4, new QTableWidgetItem(QString::fromStdString(message.target_id()))); + chatTable->setItem(chatCounter, 5, new QTableWidgetItem(QString::fromStdString(message.target_name()))); + ++chatCounter; } } - currentOffset += response.log_message_size(); - if (response.has_has_more()) { - hasMoreResults = response.has_more(); - } else { - // Legacy server without has_more: a full page implies more may exist. - hasMoreResults = (response.log_message_size() >= pageSize); - } - if (roomCounter) { roomTable->show(); - if (isFirstPage) { - roomTable->resizeColumnsToContents(); - } + roomTable->resizeColumnsToContents(); } else { roomTable->hide(); } if (gameCounter) { - if (isFirstPage) { - gameTable->resizeColumnsToContents(); - } + gameTable->resizeColumnsToContents(); gameTable->show(); } else { gameTable->hide(); } if (chatCounter) { - if (isFirstPage) { - chatTable->resizeColumnsToContents(); - } + chatTable->resizeColumnsToContents(); chatTable->show(); } else { chatTable->hide(); } - - // The new rows may already fill less than a viewport (or the user is - // still parked at the bottom), so chain the next page without waiting - // for another scroll event. The prefetch stays invisible: rows simply - // keep appearing as you scroll. - checkFetchMore(); } void TabLog::restartLayout() diff --git a/cockatrice/src/interface/widgets/tabs/tab_logs.h b/cockatrice/src/interface/widgets/tabs/tab_logs.h index f2f5842c5..fc8ef1ea0 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_logs.h +++ b/cockatrice/src/interface/widgets/tabs/tab_logs.h @@ -10,7 +10,6 @@ #include "tab.h" #include -#include #include class AbstractClient; @@ -26,7 +25,6 @@ class QWidget; class QGridLayout; class QVBoxLayout; class QTableWidget; -class QTabWidget; class Response; class TabSupervisor; @@ -50,37 +48,8 @@ private: *buttonGroupBox; QVBoxLayout *mainLayout; QTableWidget *roomTable, *gameTable, *chatTable; - QTabWidget *tabManager = nullptr; - - //! The filters of the search in flight, captured when Get was clicked so that - //! later pages cannot pick up edits the moderator made after starting it. - Command_ViewLogHistory currentQuery; - bool hasCurrentQuery = false; - //! Newest `log_time` of the first page, sent back as the upper bound of every - //! later page so messages logged in the meantime do not shift the window. - QString snapshotBefore; - //! Identity of the previous page's first row, used to spot a server that ignores - //! `offset` and hands back the same page forever. - QString lastPageStartKey; - int currentOffset = 0; - int pageSize = 200; - int roomCounter = 0, gameCounter = 0, chatCounter = 0; - bool isFetching = false; - bool hasMoreResults = false; void createDock(); - [[nodiscard]] int dateRangeForSelection() const; - void fetchPage(int offset); - void appendChatMessage(QTableWidget *table, - int &counter, - const QString &time, - const QString &senderName, - const QString &senderIp, - const QString &messageText, - const QString &targetId, - const QString &targetName, - const QString &channel = QString()); - void checkFetchMore(); signals: private slots: diff --git a/cockatrice/src/interface/widgets/visual_database_display/card_database_visual_view.cpp b/cockatrice/src/interface/widgets/visual_database_display/card_database_visual_view.cpp deleted file mode 100644 index 7b0d526f9..000000000 --- a/cockatrice/src/interface/widgets/visual_database_display/card_database_visual_view.cpp +++ /dev/null @@ -1,203 +0,0 @@ -#include "card_database_visual_view.h" - -#include "../../card_picture_loader/card_picture_loader.h" -#include "../cards/card_info_picture_widget.h" -#include "../cards/card_picture_delegate.h" -#include "../cards/card_size_widget.h" -#include "card_database_visual_display_model.h" -#include "libcockatrice/card/printing/exact_card.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -CardDatabaseVisualView::CardDatabaseVisualView(CardDatabaseVisualDisplayModel *_model, QWidget *parent) - : QListView(parent), model(_model) -{ - auto *cardDelegate = new CardPictureDelegate(model, this); - setItemDelegate(cardDelegate); - setModel(model); - - setViewMode(QListView::IconMode); - setFlow(QListView::LeftToRight); - setWrapping(true); - setResizeMode(QListView::Adjust); - setMovement(QListView::Static); - setUniformItemSizes(true); - setLayoutMode(QListView::SinglePass); - setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); - setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - setFrameShape(QFrame::NoFrame); - setWordWrap(false); - - // QListView hands the delegate's item height to the scroll bar as its single - // step under ScrollPerPixel, so one wheel notch would jump wheelScrollLines() - // whole card rows. Give the bar a text line instead; the wheel handler below - // scales that line to the grid. A positive step also stops Qt from taking the - // item height back on the next layout. - verticalScrollBar()->setSingleStep(qMax(1, fontMetrics().height())); - - // The grid is a read-only surface: clicks are actions on a card, not a - // selection, and they must not steal focus from the search field. - setSelectionMode(QAbstractItemView::NoSelection); - setEditTriggers(QAbstractItemView::NoEditTriggers); - setFocusPolicy(Qt::NoFocus); - - // Hover drives the card preview. - setMouseTracking(true); - viewport()->setMouseTracking(true); - - connect(&CardPictureLoader::getInstance(), &CardPictureLoader::pixmapLoaded, this, - [this] { viewport()->update(); }); - - // A new search result starts at the top, like the widget-per-card grid did. - connect(model, &QAbstractItemModel::modelReset, this, [this] { - verticalScrollBar()->setValue(0); - // The rows under a stationary cursor changed with the reset: re-resolve the - // hover so the preview follows the grid, as enterEvent did per card widget. - hoveredRow = -1; - updateHoveredRow(viewport()->mapFromGlobal(QCursor::pos())); - }); - - setCardSizeFactor(cardSizeFactor); -} - -void CardDatabaseVisualView::setCardSizeFactor(int factor) -{ - cardSizeFactor = qBound(CardSizeWidget::MIN_CARD_SIZE, factor, CardSizeWidget::MAX_CARD_SIZE); - - const QSize cardSize = CardInfoPictureWidget::sizeForScaleFactor(cardSizeFactor); - if (auto *cardDelegate = qobject_cast(itemDelegate())) { - cardDelegate->setCardSize(cardSize); - } - // The delegate's sizeHint does not emit sizeHintChanged, so the grid size is what - // makes a resize take effect. Keep the two in step. - setGridSize(cardSize + QSize(CardPictureDelegate::SPACING, CardPictureDelegate::SPACING)); -} - -void CardDatabaseVisualView::mousePressEvent(QMouseEvent *event) -{ - const QModelIndex index = indexAt(event->pos()); - if (index.isValid() && model != nullptr) { - const ExactCard card = model->cardAt(index.row()); - if (card) { - emit cardClicked(event, card); - event->accept(); - return; - } - } - - QListView::mousePressEvent(event); -} - -void CardDatabaseVisualView::mouseMoveEvent(QMouseEvent *event) -{ - QListView::mouseMoveEvent(event); - updateHoveredRow(event->pos()); -} - -bool CardDatabaseVisualView::viewportEvent(QEvent *event) -{ - // A per-card widget reported its hover from enterEvent, which also fires when the - // grid changes under a stationary cursor. A QListView only reports moves, so - // re-resolve the hover on enter and clear it on leave. - if (event->type() == QEvent::Enter) { - updateHoveredRow(viewport()->mapFromGlobal(QCursor::pos())); - } else if (event->type() == QEvent::Leave) { - hoveredRow = -1; - } - - return QListView::viewportEvent(event); -} - -void CardDatabaseVisualView::wheelEvent(QWheelEvent *event) -{ - // Ctrl+wheel is the card-size gesture and is filtered out before it reaches - // the view; any other modifier keeps QAbstractSlider's own page scrolling. - if (event->modifiers() != Qt::NoModifier) { - QListView::wheelEvent(event); - return; - } - - QScrollBar *bar = verticalScrollBar(); - if (!bar->isVisible()) { - event->ignore(); - return; - } - - // Touchpads and high-resolution wheels already report pixels: follow them. - if (!event->pixelDelta().isNull()) { - bar->setValue(bar->value() - event->pixelDelta().y()); - event->accept(); - return; - } - - if (event->angleDelta().y() == 0) { - QListView::wheelEvent(event); - return; - } - - // Accumulate wheel deltas smaller than a notch, like QAbstractSlider does, so - // a fine-grained wheel still moves. - wheelRemainder += static_cast(event->angleDelta().y()) / 120.0; - const int notches = static_cast(wheelRemainder); - if (notches == 0) { - event->accept(); - return; - } - wheelRemainder -= notches; - - const int pixels = -notches * wheelStep(); - bar->setValue(qBound(bar->minimum(), bar->value() + pixels, bar->maximum())); - event->accept(); -} - -int CardDatabaseVisualView::wheelStep() const -{ - // A card row is far too coarse for one wheel line, and a text line is too - // fine, so a line is an eighth of the grid cell. A notch then moves about a - // third of a card row at any card size, and the OS line count still applies. - const int lineHeight = qMax(1, gridSize().height() / 8); - return qMax(1, QApplication::wheelScrollLines()) * lineHeight; -} - -void CardDatabaseVisualView::updateHoveredRow(const QPoint &position) -{ - // indexAt() also answers for points outside the viewport, so a cursor parked - // below the grid (on the size slider, say) would resolve to the row just under - // the visible area. Only a point inside the viewport can hover a card. - if (model == nullptr || !viewport()->rect().contains(position)) { - return; - } - - const QModelIndex index = indexAt(position); - const int row = index.isValid() ? index.row() : -1; - if (row == hoveredRow) { - return; - } - - hoveredRow = row; - if (row >= 0) { - const ExactCard card = model->cardAt(row); - if (card) { - emit cardHovered(card); - } - } -} - -void CardDatabaseVisualView::leaveEvent(QEvent *event) -{ - hoveredRow = -1; - QListView::leaveEvent(event); -} diff --git a/cockatrice/src/interface/widgets/visual_database_display/card_database_visual_view.h b/cockatrice/src/interface/widgets/visual_database_display/card_database_visual_view.h deleted file mode 100644 index 5f004484d..000000000 --- a/cockatrice/src/interface/widgets/visual_database_display/card_database_visual_view.h +++ /dev/null @@ -1,67 +0,0 @@ -/** - * @file card_database_visual_view.h - * @ingroup VisualCardDatabaseWidgets - * @brief Icon-mode view of the card database, one card picture per item. - */ - -#ifndef CARD_DATABASE_VISUAL_VIEW_H -#define CARD_DATABASE_VISUAL_VIEW_H - -#include -#include -#include - -class CardDatabaseVisualDisplayModel; -class ExactCard; -class QEvent; -class QMouseEvent; -class QPoint; -class QWheelEvent; - -/** - * @brief A wrapping grid of card pictures. - * - * The view virtualizes painting: it only asks the delegate for the items that are - * on screen, so the whole card database can sit in the model without a widget (or - * pixmap) per card. Clicks and hover are reported with the exact card under the - * cursor, which is what the deck editor needs to add, decrement or preview a card. - */ -class CardDatabaseVisualView : public QListView -{ - Q_OBJECT - -public: - explicit CardDatabaseVisualView(CardDatabaseVisualDisplayModel *_model, QWidget *parent = nullptr); - - /** - * @brief Sets the card size as a percentage of the base card width. - * @param factor Percentage in the range of the card size slider. - */ - void setCardSizeFactor(int factor); - -signals: - /** @brief A card was pressed. The event is valid for the duration of the emission. */ - void cardClicked(QMouseEvent *event, const ExactCard &card); - /** @brief The cursor moved onto a different card. */ - void cardHovered(const ExactCard &card); - -protected: - void mousePressEvent(QMouseEvent *event) override; - void mouseMoveEvent(QMouseEvent *event) override; - void leaveEvent(QEvent *event) override; - bool viewportEvent(QEvent *event) override; - void wheelEvent(QWheelEvent *event) override; - -private: - void updateHoveredRow(const QPoint &position); - /// Pixels one wheel notch moves, scaled to the grid and the OS line count. - [[nodiscard]] int wheelStep() const; - - CardDatabaseVisualDisplayModel *model; - /// Fraction of a line left over from a wheel event smaller than one notch. - qreal wheelRemainder = 0.0; - int cardSizeFactor = 100; - int hoveredRow = -1; -}; - -#endif // CARD_DATABASE_VISUAL_VIEW_H diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp index 6037418ad..5a1097b0d 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp @@ -5,29 +5,40 @@ #include "../../../filters/syntax_help.h" #include "../../key_signals.h" #include "../../pixel_map_generator.h" +#include "../cards/card_info_picture_widget.h" +#include "../cards/card_info_picture_with_text_overlay_widget.h" #include "../cards/card_size_widget.h" #include "../deck_editor/card_database_view.h" #include "../general/layout_containers/flow_widget.h" #include "../utility/custom_line_edit.h" #include "card_database_display_model.h" #include "card_database_model.h" -#include "card_database_visual_display_model.h" -#include "card_database_visual_view.h" #include "deck_list_model.h" #include "libcockatrice/card/card_localization.h" #include "libcockatrice/card/database/card_database_loader.h" #include "libcockatrice/card/database/card_database_querier.h" #include "libcockatrice/card/printing/exact_card.h" +#include "libcockatrice/card/printing/printing_info.h" #include "libcockatrice/filters/filter_card.h" #include "libcockatrice/filters/filter_tree.h" #include "visual_database_display_color_filter_widget.h" #include "visual_database_display_filter_toolbar_widget.h" +#include #include +#include #include #include #include +#include +#include +#include +#include +#include #include +#include +#include +#include #include #include #include @@ -37,11 +48,14 @@ #include #include #include +#include +#include #include #include #include #include #include +#include class QMouseEvent; @@ -50,6 +64,11 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent, DeckListModel *deckListModel) : QWidget(parent) { + debounceTimer = new QTimer(this); + debounceTimer->setSingleShot(true); // Ensure it only fires once after the timeout + + connect(debounceTimer, &QTimer::timeout, this, &VisualDatabaseDisplayWidget::onSearchModelChanged); + // Create display model databaseDisplayModel = new CardDatabaseDisplayModel(this); databaseDisplayModel->setObjectName("databaseDisplayModel"); @@ -67,12 +86,23 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent, connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardSearchLanguageChanged, this, applyCardSearchLanguage); + cards = new QList; + connect(databaseDisplayModel, &CardDatabaseDisplayModel::modelDirty, this, + &VisualDatabaseDisplayWidget::modelDirty); + // Set up main layout and widgets setMinimumSize(0, 0); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); mainLayout = QtUtils::makeLayout(this, 0, 1); setLayout(mainLayout); + flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarPolicy::ScrollBarAsNeeded); + cardSizeWidget = new CardSizeWidget(this, flowWidget, + SettingsCache::instance().cardsDisplay().getVisualDatabaseDisplayCardSize()); + connect(cardSizeWidget, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance().cardsDisplay(), + &CardsDisplaySettings::setVisualDatabaseDisplayCardSize); + cardSizeWidget->enableCtrlScrollResize(flowWidget); + searchContainer = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAlwaysOff); searchEdit = new SearchLineEdit(); @@ -129,25 +159,6 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent, connect(databaseView, &CardDatabaseView::relatedCardClicked, this, &VisualDatabaseDisplayWidget::onRelatedCardClicked); - // The visual grid is a real view on a flattened model: one row per displayed - // card picture, painted on demand. - visualDisplayModel = new CardDatabaseVisualDisplayModel(databaseDisplayModel, CardDatabaseManager::query(), this); - - cardView = new CardDatabaseVisualView(visualDisplayModel, this); - cardView->setObjectName("visualCardView"); - connect(cardView, &CardDatabaseVisualView::cardClicked, this, - &VisualDatabaseDisplayWidget::cardClickedDatabaseDisplay); - connect(cardView, &CardDatabaseVisualView::cardHovered, this, - &VisualDatabaseDisplayWidget::cardHoveredDatabaseDisplay); - - const int cardSize = SettingsCache::instance().cardsDisplay().getVisualDatabaseDisplayCardSize(); - cardSizeWidget = new CardSizeWidget(this, nullptr, cardSize); - connect(cardSizeWidget, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance().cardsDisplay(), - &CardsDisplaySettings::setVisualDatabaseDisplayCardSize); - connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, cardView, &CardDatabaseVisualView::setCardSizeFactor); - cardView->setCardSizeFactor(cardSize); - cardSizeWidget->enableCtrlScrollResize(cardView); - colorFilterWidget = new VisualDatabaseDisplayColorFilterWidget(this, filterModel); filterContainer = new VisualDatabaseDisplayFilterToolbarWidget(this, deckListModel); @@ -179,6 +190,8 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent, databaseLoadIndicator->setVisible(false); } + QScrollBar *scrollBar = flowWidget->scrollArea->verticalScrollBar(); + connect(scrollBar, &QScrollBar::valueChanged, [this](int /*value*/) { loadCurrentPage(); }); retranslateUi(); } @@ -197,7 +210,7 @@ void VisualDatabaseDisplayWidget::initialize() mainLayout->addWidget(databaseView); - mainLayout->addWidget(cardView); + mainLayout->addWidget(flowWidget); mainLayout->addWidget(cardSizeWidget); @@ -228,6 +241,8 @@ void VisualDatabaseDisplayWidget::initializeFilters() databaseDisplayModel->setFilterTree(filterModel->filterTree()); + QTimer::singleShot(5000, this, [this] { loadCurrentPage(); }); + retranslateUi(); }); } @@ -263,36 +278,71 @@ void VisualDatabaseDisplayWidget::updateDisplayModeButton() displayModeButton->setText(displayModeButton->isChecked() ? tr("Table") : tr("Visual")); } +void VisualDatabaseDisplayWidget::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); + loadCurrentPage(); +} + void VisualDatabaseDisplayWidget::onDisplayModeChanged(bool checked) { updateDisplayModeButton(); if (checked) { // Table mode - cardView->setVisible(false); + flowWidget->setVisible(false); cardSizeWidget->setVisible(false); databaseView->setItemDelegate(new QStyledItemDelegate(databaseView)); databaseView->setVisible(true); } else { // Visual mode - cardView->setVisible(true); + flowWidget->setVisible(true); cardSizeWidget->setVisible(true); databaseView->setVisible(false); + populateCards(); } } +void VisualDatabaseDisplayWidget::onClick(QMouseEvent *event, const ExactCard &card) +{ + emit cardClickedDatabaseDisplay(event, card); +} + +void VisualDatabaseDisplayWidget::onHover(const ExactCard &hoveredCard) +{ + emit cardHoveredDatabaseDisplay(hoveredCard); +} + +void VisualDatabaseDisplayWidget::addCardToDisplay(const ExactCard &cardToAdd) +{ + cards->append(cardToAdd); + auto *display = new CardInfoPictureWithTextOverlayWidget(flowWidget, false); + display->setScaleFactor(cardSizeWidget->getSlider()->value()); + display->setCard(cardToAdd); + flowWidget->addWidget(display); + connect(display, &CardInfoPictureWithTextOverlayWidget::cardClicked, this, &VisualDatabaseDisplayWidget::onClick); + connect(display, &CardInfoPictureWithTextOverlayWidget::hoveredOnCard, this, &VisualDatabaseDisplayWidget::onHover); + connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, display, &CardInfoPictureWidget::setScaleFactor); +} + bool VisualDatabaseDisplayWidget::isVisualDisplayMode() const { return !displayModeButton->isChecked(); } -QStringList VisualDatabaseDisplayWidget::collectSetFilterTerms() const -{ - return filterModel->getFilterTermsOfType(CardFilter::AttrSet); -} - void VisualDatabaseDisplayWidget::onSearchModelChanged() { - visualDisplayModel->setSetFilterTerms(collectSetFilterTerms()); + if (flowWidget->isVisible()) { + // Clear the current page and prepare for new data + flowWidget->clearLayout(); // Clear existing cards + cards->clear(); // Clear the card list + // Reset scrollbar position to the top after loading new cards + QScrollBar *scrollBar = flowWidget->scrollArea->verticalScrollBar(); + scrollBar->setValue(0); // Reset scrollbar to top + + currentPage = 0; + loadCurrentPage(); + qCDebug(VisualDatabaseDisplayLog) << "Search model changed"; + } } void VisualDatabaseDisplayWidget::onSelectedCardChanged(const QString &cardName) @@ -318,3 +368,113 @@ void VisualDatabaseDisplayWidget::onRelatedCardClicked(const QString &relatedCar ExactCard exactCard = CardDatabaseManager::query()->guessCard({relatedCard}); emit cardInfoRequested(exactCard); } + +bool VisualDatabaseDisplayWidget::nearEndOfPage() const +{ + if (!flowWidget->isVisible()) { + return false; + } + + QScrollBar *scrollBar = flowWidget->scrollArea->verticalScrollBar(); + if (scrollBar->value() + scrollBar->pageStep() * 2 < scrollBar->maximum()) { + return false; // there is at least two pages of space to scroll remaining + } + + return true; +} + +void VisualDatabaseDisplayWidget::loadCurrentPage() +{ + // Ensure only the initial page is loaded + if (currentPage == 0) { + if (!initialLoadScheduled) { + initialLoadScheduled = true; + qCDebug(VisualDatabaseDisplayLog) << "Loading the first page"; + // Defer the first page so the tab switch stays responsive. The card + // grid builds one event loop turn later. This also applies to + // search-driven reloads, which reset currentPage back to 0. + QTimer::singleShot(0, this, [this] { + initialLoadScheduled = false; + populateCards(); + }); + } + } else if (nearEndOfPage()) { + // If not the first page, just load the next page and append to the flow widget + loadNextPage(); + } +} + +void VisualDatabaseDisplayWidget::populateCards() +{ + cards->clear(); + loadNextPage(); +} + +void VisualDatabaseDisplayWidget::loadNextPage() +{ + // Calculate the start and end indices for the next page + int rowCount = databaseDisplayModel->rowCount(); + int start = currentPage * cardsPerPage; + int end = qMin(start + cardsPerPage, rowCount); + + // Load more cards if we are at the end of the current list and can fetch more + if (end >= rowCount && databaseDisplayModel->canFetchMore(QModelIndex())) { + databaseDisplayModel->fetchMore(QModelIndex()); + } + + // Load the next page of cards and add them to the flow widget + flowWidget->setUpdatesEnabled(false); + loadPage(start, end); + flowWidget->setUpdatesEnabled(true); +} + +void VisualDatabaseDisplayWidget::loadPage(int start, int end) +{ + QList setFilters = filterModel->getFiltersOfType(CardFilter::AttrSet); + for (int row = start; row < end; ++row) { + qCDebug(VisualDatabaseDisplayLog) << "Adding " << row; + QModelIndex index = databaseDisplayModel->index(row, CardDatabaseModel::NameColumn); + QVariant name = databaseDisplayModel->data(index, Qt::DisplayRole); + qCDebug(VisualDatabaseDisplayLog) << name.toString(); + + if (CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(name.toString())) { + if (!setFilters.empty()) { + const auto &setMap = info->getSets(); + for (const CardFilter *setFilter : setFilters) { + const auto it = setMap.constFind(setFilter->term()); + if (it != setMap.constEnd()) { + for (const PrintingInfo &printing : it.value()) { + addCardToDisplay(ExactCard(info, printing)); + } + } + } + } else { + addCardToDisplay(CardDatabaseManager::query()->getPreferredCard(info)); + } + } else { + qCDebug(VisualDatabaseDisplayLog) << "Card not found in database!"; + } + } + currentPage++; +} + +void VisualDatabaseDisplayWidget::modelDirty() const +{ + debounceTimer->start(debounceTime); +} + +void VisualDatabaseDisplayWidget::sortCardList(const QStringList &properties, + Qt::SortOrder order = Qt::AscendingOrder) const +{ + CardInfoComparator comparator(properties, order); + std::sort(cards->begin(), cards->end(), [comparator](const ExactCard &a, const ExactCard &b) { + return comparator(a.getCardPtr(), b.getCardPtr()); + }); +} + +void VisualDatabaseDisplayWidget::databaseDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + (void)topLeft; + (void)bottomRight; + qCDebug(VisualDatabaseDisplayLog) << "Database Data changed"; +} diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h index 6148670a3..15adca214 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h @@ -9,6 +9,8 @@ #include "libcockatrice/card/card_info.h" +#include +#include #include #include #include @@ -19,21 +21,26 @@ class CardDatabaseDisplayModel; class CardDatabaseModel; class CardDatabaseView; -class CardDatabaseVisualDisplayModel; -class CardDatabaseVisualView; class CardSizeWidget; +class DeckList; class DeckListModel; class ExactCard; class FilterTreeModel; class FlowWidget; +class OverlapControlWidget; class QLabel; -class QMouseEvent; +class QModelIndex; class QPushButton; +class QScrollArea; +class QTimer; class QToolButton; class QVBoxLayout; class SearchLineEdit; class VisualDatabaseDisplayColorFilterWidget; class VisualDatabaseDisplayFilterToolbarWidget; +template class QList; + +inline Q_LOGGING_CATEGORY(VisualDatabaseDisplayLog, "visual_database_display"); class VisualDatabaseDisplayWidget : public QWidget { @@ -45,6 +52,14 @@ public: DeckListModel *deckListModel = nullptr); void retranslateUi(); + void adjustCardsPerPage(); + void populateCards(); + void loadPage(int start, int end); + void loadNextPage(); + void loadCurrentPage(); + void sortCardList(const QStringList &properties, Qt::SortOrder order) const; + void setDeckList(const DeckList &new_deck_list_model); + /** * @brief Sets the callback used to create a custom zone from the add-to-zone menu. * The callback returns the name of the created zone, or an empty string if creation was cancelled. @@ -72,13 +87,6 @@ public: bool isVisualDisplayMode() const; public slots: - /** - * @brief Pushes the active set filters to the visual grid. - * - * The grid's rows follow the display model's filter and sort signals on their - * own; this only forwards the set filters, which decide which printings the - * grid shows. - */ void onSearchModelChanged(); signals: @@ -93,6 +101,11 @@ signals: protected slots: void initialize(); + void onClick(QMouseEvent *event, const ExactCard &card); + void onHover(const ExactCard &hoveredCard); + void addCardToDisplay(const ExactCard &cardToAdd); + void databaseDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); + void modelDirty() const; void onDisplayModeChanged(bool checked); void onSelectedCardChanged(const QString &cardName); @@ -112,21 +125,31 @@ private: QToolButton *clearFilterWidget; VisualDatabaseDisplayFilterToolbarWidget *filterContainer; CardDatabaseDisplayModel *databaseDisplayModel; - CardDatabaseVisualDisplayModel *visualDisplayModel; CardDatabaseView *databaseView; - CardDatabaseVisualView *cardView; std::function newZoneCreator; + QList *cards; QVBoxLayout *mainLayout; + QScrollArea *scrollArea; + FlowWidget *flowWidget; + QWidget *overlapCategories; + QVBoxLayout *overlapCategoriesLayout; + OverlapControlWidget *overlapControlWidget; CardSizeWidget *cardSizeWidget; + QTimer *debounceTimer; + int debounceTime = 300; // in Ms + int currentPage = 0; // Current page index + int cardsPerPage = 100; // Number of cards per page bool filtersInitialized = false; + bool initialLoadScheduled = false; void initializeFilters(); void highlightAllSearchEdit(); void updateDisplayModeButton(); - [[nodiscard]] QStringList collectSetFilterTerms() const; + bool nearEndOfPage() const; protected: + void resizeEvent(QResizeEvent *event) override; void showEvent(QShowEvent *event) override; }; diff --git a/cockatrice/src/interface/window_main.cpp b/cockatrice/src/interface/window_main.cpp index d546676bc..f8ab5d3f6 100644 --- a/cockatrice/src/interface/window_main.cpp +++ b/cockatrice/src/interface/window_main.cpp @@ -254,11 +254,7 @@ void MainWindow::actWatchReplay() void MainWindow::localGameEnded() { - // Defer deletion so the LocalClients (already scheduled with deleteLater() in TabSupervisor::stop) - // and any queued commands are processed before the server and its interfaces are destroyed. - if (localServer) { - localServer->deleteLater(); - } + delete localServer; localServer = nullptr; aConnect->setEnabled(true); diff --git a/format.sh b/format.sh index fba76e93a..b4c78543b 100755 --- a/format.sh +++ b/format.sh @@ -33,7 +33,6 @@ cmakefile="CMakeLists.txt" cmakedir="cmake/.*\\.cmake" cmakeinclude=("cmake/gtest-CMakeLists.txt.in") scripts="*.sh" -scriptsrx="[^/]*\\.sh$" color="--" verbosity=0 sep="----------" @@ -255,7 +254,7 @@ if [[ $branch ]]; then if [[ $do_shell ]]; then shell_names=() for name in "${basenames[@]}"; do - filerx="(^|/)$scriptsrx" + filerx="(^|/)$scripts$" if [[ $name =~ $filerx ]]; then shell_names+=("$name") fi diff --git a/libcockatrice_card/CMakeLists.txt b/libcockatrice_card/CMakeLists.txt index d2e6ee83a..e2d5029ed 100644 --- a/libcockatrice_card/CMakeLists.txt +++ b/libcockatrice_card/CMakeLists.txt @@ -1,3 +1,7 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + set(HEADERS libcockatrice/card/card_info.h libcockatrice/card/card_info_comparator.h diff --git a/libcockatrice_deck_list/CMakeLists.txt b/libcockatrice_deck_list/CMakeLists.txt index 3e7d522c2..ffd4c28a1 100644 --- a/libcockatrice_deck_list/CMakeLists.txt +++ b/libcockatrice_deck_list/CMakeLists.txt @@ -1,3 +1,7 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + set(HEADERS libcockatrice/deck_list/tree/abstract_deck_list_card_node.h libcockatrice/deck_list/tree/abstract_deck_list_node.h diff --git a/libcockatrice_filters/CMakeLists.txt b/libcockatrice_filters/CMakeLists.txt index 070c04637..df2ead83f 100644 --- a/libcockatrice_filters/CMakeLists.txt +++ b/libcockatrice_filters/CMakeLists.txt @@ -1,3 +1,7 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + set(HEADERS libcockatrice/filters/filter_card.h libcockatrice/filters/filter_string.h libcockatrice/filters/filter_tree.h ) diff --git a/libcockatrice_filters/libcockatrice/filters/filter_card.h b/libcockatrice_filters/libcockatrice/filters/filter_card.h index dbeffd18f..e6a37ee40 100644 --- a/libcockatrice_filters/libcockatrice/filters/filter_card.h +++ b/libcockatrice_filters/libcockatrice/filters/filter_card.h @@ -54,7 +54,7 @@ private: enum Attr a; public: - CardFilter(const QString &term, Type type, Attr attr) : trm(term), t(type), a(attr) + CardFilter(QString &term, Type type, Attr attr) : trm(term), t(type), a(attr) { } diff --git a/libcockatrice_game/CMakeLists.txt b/libcockatrice_game/CMakeLists.txt index ce68c5ea6..e7d362ca2 100644 --- a/libcockatrice_game/CMakeLists.txt +++ b/libcockatrice_game/CMakeLists.txt @@ -1,3 +1,7 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + set(HEADERS libcockatrice/game/abstract_game.h libcockatrice/game/board/arrow_data.h diff --git a/libcockatrice_interfaces/CMakeLists.txt b/libcockatrice_interfaces/CMakeLists.txt index 69ed13b0e..16ea686b5 100644 --- a/libcockatrice_interfaces/CMakeLists.txt +++ b/libcockatrice_interfaces/CMakeLists.txt @@ -1,3 +1,7 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + set(HEADERS libcockatrice/interfaces/interface_cache_storage_settings_provider.h libcockatrice/interfaces/interface_card_database_path_provider.h diff --git a/libcockatrice_models/CMakeLists.txt b/libcockatrice_models/CMakeLists.txt index 5186eff13..7af35b2d5 100644 --- a/libcockatrice_models/CMakeLists.txt +++ b/libcockatrice_models/CMakeLists.txt @@ -1,3 +1,7 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + add_subdirectory(libcockatrice/models/database) add_subdirectory(libcockatrice/models/deck_list) diff --git a/libcockatrice_models/libcockatrice/models/database/CMakeLists.txt b/libcockatrice_models/libcockatrice/models/database/CMakeLists.txt index 22a04cd32..2471adacf 100644 --- a/libcockatrice_models/libcockatrice/models/database/CMakeLists.txt +++ b/libcockatrice_models/libcockatrice/models/database/CMakeLists.txt @@ -1,7 +1,10 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + set(HEADERS card_database_model.h card_database_display_model.h - card_database_visual_display_model.h card/card_completer_proxy_model.h card/card_search_model.h card_set/card_sets_model.h @@ -14,7 +17,6 @@ add_library( ${HEADERS} card_database_model.cpp card_database_display_model.cpp - card_database_visual_display_model.cpp card/card_completer_proxy_model.cpp card/card_search_model.cpp card_set/card_sets_model.cpp diff --git a/libcockatrice_models/libcockatrice/models/database/card_database_display_model.cpp b/libcockatrice_models/libcockatrice/models/database/card_database_display_model.cpp index 9ec0afd9c..5573deb18 100644 --- a/libcockatrice_models/libcockatrice/models/database/card_database_display_model.cpp +++ b/libcockatrice_models/libcockatrice/models/database/card_database_display_model.cpp @@ -21,7 +21,7 @@ class QObject; CardDatabaseDisplayModel::CardDatabaseDisplayModel(QObject *parent) - : QSortFilterProxyModel(parent), isToken(ShowAll), filterString(nullptr), cardModel(nullptr), + : QSortFilterProxyModel(parent), isToken(ShowAll), filterString(nullptr), collator(NaturalSort::makeNaturalSortCollator()) { filterTree = nullptr; @@ -33,37 +33,22 @@ CardDatabaseDisplayModel::CardDatabaseDisplayModel(QObject *parent) invalidate(); emit modelDirty(); }); + + loadedRowCount = 0; } void CardDatabaseDisplayModel::setSourceModel(QAbstractItemModel *model) { - cardModel = qobject_cast(model); QSortFilterProxyModel::setSourceModel(model); connect(model, &QAbstractItemModel::rowsInserted, this, [this]() { dirty(); }); connect(model, &QAbstractItemModel::rowsRemoved, this, [this]() { dirty(); }); - connect(model, &QAbstractItemModel::modelReset, this, [this]() { dirty(); }); -} - -QVariant CardDatabaseDisplayModel::data(const QModelIndex &index, int role) const -{ - if (role == CardInfoRole) { - return QVariant::fromValue(cardInfoAt(index)); - } - - return QSortFilterProxyModel::data(index, role); -} - -CardInfoPtr CardDatabaseDisplayModel::cardInfoAt(const QModelIndex &index) const -{ - if (!index.isValid() || cardModel == nullptr) { - return {}; - } - - const QModelIndex sourceIndex = mapToSource(index); - return sourceIndex.isValid() ? cardModel->getCard(sourceIndex.row()) : CardInfoPtr(); + connect(model, &QAbstractItemModel::modelReset, this, [this]() { + loadedRowCount = 0; + dirty(); + }); } QMap CardDatabaseDisplayModel::characterTranslation = {{L'“', L'\"'}, @@ -71,6 +56,32 @@ QMap CardDatabaseDisplayModel::characterTranslation = {{L'“' {L'‘', L'\''}, {L'’', L'\''}}; +bool CardDatabaseDisplayModel::canFetchMore(const QModelIndex &index) const +{ + return loadedRowCount < sourceModel()->rowCount(index); +} + +void CardDatabaseDisplayModel::fetchMore(const QModelIndex &index) +{ + int remainder = sourceModel()->rowCount(index) - loadedRowCount; + int itemsToFetch = qMin(100, remainder); + + if (itemsToFetch == 0) { + return; + } + + const auto startIndex = qMin(rowCount(QModelIndex()), loadedRowCount); + beginInsertRows(QModelIndex(), startIndex, startIndex + itemsToFetch - 1); + + loadedRowCount += itemsToFetch; + endInsertRows(); +} + +int CardDatabaseDisplayModel::rowCount(const QModelIndex &parent) const +{ + return QSortFilterProxyModel::rowCount(parent); +} + bool CardDatabaseDisplayModel::lessThan(const QModelIndex &left, const QModelIndex &right) const { const QString leftString = sourceModel()->data(left, CardDatabaseModel::SortRole).toString(); diff --git a/libcockatrice_models/libcockatrice/models/database/card_database_display_model.h b/libcockatrice_models/libcockatrice/models/database/card_database_display_model.h index 3a9bbc6ab..343a7482e 100644 --- a/libcockatrice_models/libcockatrice/models/database/card_database_display_model.h +++ b/libcockatrice_models/libcockatrice/models/database/card_database_display_model.h @@ -21,7 +21,6 @@ #include #include -class CardDatabaseModel; class FilterTree; class QObject; template class QMap; @@ -37,25 +36,13 @@ public: ShowAll }; - enum Role - { - /** - * @brief The CardInfoPtr behind a proxy row. - * - * Saves views from looking the card up again through its name, and keeps - * them from depending on the source model's column layout. - */ - CardInfoRole = Qt::UserRole + 1 - }; - private: FilterBool isToken; QString cardName, cardText; QSet cardNameSet, cardTypes, cardColors; FilterTree *filterTree; FilterString *filterString; - /// The source model, kept typed so CardInfoRole does not cast per row. - CardDatabaseModel *cardModel; + int loadedRowCount; QTimer dirtyTimer; CardSearchLanguage searchLanguage; QString searchText; @@ -98,16 +85,9 @@ public: dirtyTimer.start(20); } void clearFilterAll(); - [[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - - /** - * @brief Returns the card behind a proxy row, without the QVariant wrapper of CardInfoRole. - * - * The visual grid calls this for every row on each rebuild; CardInfoRole stays the - * generic way in for consumers that only see the model through data(). - */ - [[nodiscard]] CardInfoPtr cardInfoAt(const QModelIndex &index) const; - + [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; + [[nodiscard]] bool canFetchMore(const QModelIndex &parent) const override; + void fetchMore(const QModelIndex &parent) override; signals: void modelDirty(); diff --git a/libcockatrice_models/libcockatrice/models/database/card_database_model.cpp b/libcockatrice_models/libcockatrice/models/database/card_database_model.cpp index a83b7c83d..1ede554f8 100644 --- a/libcockatrice_models/libcockatrice/models/database/card_database_model.cpp +++ b/libcockatrice_models/libcockatrice/models/database/card_database_model.cpp @@ -27,18 +27,14 @@ CardDatabaseModel::CardDatabaseModel(CardDatabase *_db, bool _showOnlyCardsFromE CardDatabaseModel::~CardDatabaseModel() = default; -int CardDatabaseModel::rowCount(const QModelIndex &parent) const +int CardDatabaseModel::rowCount(const QModelIndex & /*parent*/) const { - // A list model's card rows are leaves; returning the full card count for a - // valid parent makes every row claim all cards as children. - return parent.isValid() ? 0 : cardList.size(); + return cardList.size(); } -int CardDatabaseModel::columnCount(const QModelIndex &parent) const +int CardDatabaseModel::columnCount(const QModelIndex & /*parent*/) const { - // Card rows are leaves, so a valid parent holds no columns. Reporting the - // row width for a card index would claim child cells that do not exist. - return parent.isValid() ? 0 : CARDDBMODEL_COLUMNS; + return CARDDBMODEL_COLUMNS; } QVariant CardDatabaseModel::data(const QModelIndex &index, int role) const diff --git a/libcockatrice_models/libcockatrice/models/database/card_database_visual_display_model.cpp b/libcockatrice_models/libcockatrice/models/database/card_database_visual_display_model.cpp deleted file mode 100644 index f6a3a925c..000000000 --- a/libcockatrice_models/libcockatrice/models/database/card_database_visual_display_model.cpp +++ /dev/null @@ -1,123 +0,0 @@ -#include "card_database_visual_display_model.h" - -#include "card_database_display_model.h" -#include "libcockatrice/card/database/card_database_querier.h" - -#include -#include -#include -#include - -/// Coalesces the bursts of proxy signals a filter, sort or database load produces. -static constexpr int REBUILD_COALESCE_MS = 30; - -CardDatabaseVisualDisplayModel::CardDatabaseVisualDisplayModel(CardDatabaseDisplayModel *_displayModel, - CardDatabaseQuerier *_querier, - QObject *parent) - : QAbstractListModel(parent), displayModel(_displayModel), querier(_querier) -{ - rebuildTimer.setSingleShot(true); - rebuildTimer.setInterval(REBUILD_COALESCE_MS); - connect(&rebuildTimer, &QTimer::timeout, this, &CardDatabaseVisualDisplayModel::rebuild); - - if (displayModel == nullptr) { - return; - } - - connect(displayModel, &QAbstractItemModel::modelReset, this, &CardDatabaseVisualDisplayModel::scheduleRebuild); - connect(displayModel, &QAbstractItemModel::layoutChanged, this, &CardDatabaseVisualDisplayModel::scheduleRebuild); - connect(displayModel, &QAbstractItemModel::rowsInserted, this, &CardDatabaseVisualDisplayModel::scheduleRebuild); - connect(displayModel, &QAbstractItemModel::rowsRemoved, this, &CardDatabaseVisualDisplayModel::scheduleRebuild); - connect(displayModel, &QAbstractItemModel::dataChanged, this, &CardDatabaseVisualDisplayModel::scheduleRebuild); -} - -void CardDatabaseVisualDisplayModel::setSetFilterTerms(const QStringList &terms) -{ - if (setFilterTerms == terms) { - return; - } - - setFilterTerms = terms; - refresh(); -} - -void CardDatabaseVisualDisplayModel::refresh() -{ - rebuildTimer.stop(); - rebuild(); -} - -void CardDatabaseVisualDisplayModel::scheduleRebuild() -{ - rebuildTimer.start(); -} - -void CardDatabaseVisualDisplayModel::rebuild() -{ - beginResetModel(); - entries.clear(); - - if (displayModel != nullptr) { - const int proxyRows = displayModel->rowCount(); - entries.reserve(proxyRows); - for (int row = 0; row < proxyRows; ++row) { - const CardInfoPtr info = displayModel->cardInfoAt(displayModel->index(row, 0)); - if (info) { - appendPrintingsFor(info); - } - } - } - - endResetModel(); -} - -void CardDatabaseVisualDisplayModel::appendPrintingsFor(const CardInfoPtr &info) -{ - if (setFilterTerms.isEmpty()) { - entries.append({info, PrintingInfo()}); - return; - } - - const auto &printingsBySet = info->getSets(); - for (const QString &term : setFilterTerms) { - const auto printings = printingsBySet.constFind(term); - if (printings == printingsBySet.constEnd()) { - continue; - } - for (const PrintingInfo &printing : printings.value()) { - entries.append({info, printing}); - } - } -} - -int CardDatabaseVisualDisplayModel::rowCount(const QModelIndex &parent) const -{ - return parent.isValid() ? 0 : static_cast(entries.size()); -} - -QVariant CardDatabaseVisualDisplayModel::data(const QModelIndex &index, int role) const -{ - if (!index.isValid() || index.row() >= entries.size()) { - return {}; - } - - if (role == Qt::DisplayRole || role == Qt::AccessibleTextRole) { - return cardAt(index.row()).getName(); - } - - return {}; -} - -ExactCard CardDatabaseVisualDisplayModel::cardAt(int row) const -{ - if (row < 0 || row >= entries.size()) { - return {}; - } - - const Entry &entry = entries.at(row); - if (entry.printing.isEmpty() && setFilterTerms.isEmpty()) { - return querier != nullptr ? querier->getPreferredCard(entry.info) : ExactCard(entry.info); - } - - return ExactCard(entry.info, entry.printing); -} diff --git a/libcockatrice_models/libcockatrice/models/database/card_database_visual_display_model.h b/libcockatrice_models/libcockatrice/models/database/card_database_visual_display_model.h deleted file mode 100644 index 1303b6464..000000000 --- a/libcockatrice_models/libcockatrice/models/database/card_database_visual_display_model.h +++ /dev/null @@ -1,90 +0,0 @@ -/** - * @file card_database_visual_display_model.h - * @ingroup CardDatabaseModels - * @brief A list model of the exact cards a visual card grid paints. - * - * The filtered and sorted CardDatabaseDisplayModel holds one row per card name. - * A visual grid shows one picture per *printing* instead, so this model flattens - * the proxy rows into ExactCards: the preferred printing per card normally, and - * every printing of the selected sets while set filters are active. - */ - -#ifndef COCKATRICE_CARD_DATABASE_VISUAL_DISPLAY_MODEL_H -#define COCKATRICE_CARD_DATABASE_VISUAL_DISPLAY_MODEL_H - -#include "libcockatrice/card/card_info.h" -#include "libcockatrice/card/printing/exact_card.h" -#include "libcockatrice/card/printing/printing_info.h" - -#include -#include -#include -#include -#include -#include -#include - -class CardDatabaseDisplayModel; -class CardDatabaseQuerier; - -class CardDatabaseVisualDisplayModel : public QAbstractListModel -{ - Q_OBJECT - -public: - /** - * @brief Flattens _displayModel into the cards a visual grid displays. - * @param _displayModel The filtered and sorted proxy model of the card database. - * @param _querier Resolves the preferred printing of a card. - * @param parent Parent object. - */ - explicit CardDatabaseVisualDisplayModel(CardDatabaseDisplayModel *_displayModel, - CardDatabaseQuerier *_querier, - QObject *parent = nullptr); - - /** - * @brief Sets the set shorthand names of the active set filters. - * - * With no terms every card is shown with its preferred printing. With terms - * every printing belonging to one of the named sets is shown instead, in term - * order. Rebuilds immediately when the terms change. - */ - void setSetFilterTerms(const QStringList &terms); - - /** - * @brief Rebuilds the row list from the proxy model right away. - * - * Proxy model changes normally rebuild on a short coalescing timer; call this - * when the caller needs the new rows before returning. - */ - void refresh(); - - [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; - [[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - - /** - * @brief Returns the card painted at @p row, with its preferred printing resolved. - * @param row Row in this model, not in the proxy model. - */ - [[nodiscard]] ExactCard cardAt(int row) const; - -private: - struct Entry - { - CardInfoPtr info; - /// Empty when the entry follows the preferred printing rather than a set filter. - PrintingInfo printing; - }; - - void scheduleRebuild(); - void rebuild(); - void appendPrintingsFor(const CardInfoPtr &info); - - QPointer displayModel; - CardDatabaseQuerier *querier; - QStringList setFilterTerms; - QList entries; - QTimer rebuildTimer; -}; - -#endif // COCKATRICE_CARD_DATABASE_VISUAL_DISPLAY_MODEL_H diff --git a/libcockatrice_models/libcockatrice/models/deck_list/CMakeLists.txt b/libcockatrice_models/libcockatrice/models/deck_list/CMakeLists.txt index ce5c4d54b..b46275437 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/CMakeLists.txt +++ b/libcockatrice_models/libcockatrice/models/deck_list/CMakeLists.txt @@ -1,3 +1,7 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + set(HEADERS deck_list_model.h deck_list_sort_filter_proxy_model.h) add_library( diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp index 617324fe3..4b4088972 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp @@ -134,13 +134,6 @@ void DeckListModel::rebuildTree() int DeckListModel::rowCount(const QModelIndex &parent) const { - // Only column 0 carries children. Column 1 and later indexes are siblings - // used by views for the remaining cells of a row, and a model that hangs - // children off them breaks QAbstractItemModelTester and proxy models. - if (parent.isValid() && parent.column() != 0) { - return 0; - } - // debugIndexInfo("rowCount", parent); auto *node = getNode(parent); if (node) { @@ -289,7 +282,7 @@ QVariant DeckListModel::headerData(const int section, const Qt::Orientation orie QModelIndex DeckListModel::index(int row, int column, const QModelIndex &parent) const { // debugIndexInfo("index", parent); - if ((parent.isValid() && parent.column() != 0) || !hasIndex(row, column, parent)) { + if (!hasIndex(row, column, parent)) { return {}; } diff --git a/libcockatrice_network/CMakeLists.txt b/libcockatrice_network/CMakeLists.txt index 925e4d445..3069c0db4 100644 --- a/libcockatrice_network/CMakeLists.txt +++ b/libcockatrice_network/CMakeLists.txt @@ -1,3 +1,7 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + add_subdirectory(libcockatrice/network/client) add_subdirectory(libcockatrice/network/server) diff --git a/libcockatrice_network/libcockatrice/network/client/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/client/CMakeLists.txt index cf8d9fc4d..bcf62463c 100644 --- a/libcockatrice_network/libcockatrice/network/client/CMakeLists.txt +++ b/libcockatrice_network/libcockatrice/network/client/CMakeLists.txt @@ -1,3 +1,7 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + add_subdirectory(abstract) add_subdirectory(local) add_subdirectory(remote) diff --git a/libcockatrice_network/libcockatrice/network/client/abstract/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/client/abstract/CMakeLists.txt index 33812e0bf..880cef5a1 100644 --- a/libcockatrice_network/libcockatrice/network/client/abstract/CMakeLists.txt +++ b/libcockatrice_network/libcockatrice/network/client/abstract/CMakeLists.txt @@ -1,3 +1,7 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + set(HEADERS abstract_client.h latency_tracker.h) set(SOURCES abstract_client.cpp latency_tracker.cpp) @@ -11,7 +15,7 @@ target_include_directories(libcockatrice_network_client_abstract PUBLIC .) target_link_libraries( libcockatrice_network_client_abstract PUBLIC ${COCKATRICE_QT_VERSION_NAME}::Network ${COCKATRICE_QT_VERSION_NAME}::WebSockets libcockatrice_protocol - libcockatrice_network_server_remote libcockatrice_utility + libcockatrice_network_server_remote ) target_precompile_headers(libcockatrice_network_client_abstract PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h") diff --git a/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.cpp b/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.cpp index 04e6beda0..0c523d776 100644 --- a/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.cpp +++ b/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.cpp @@ -10,7 +10,6 @@ #include "libcockatrice/protocol/pb/serverinfo_user.pb.h" #include "libcockatrice/protocol/pb/session_commands.pb.h" #include "libcockatrice/protocol/pb/session_event.pb.h" -#include "libcockatrice/utility/passwordhasher.h" #include #include @@ -81,26 +80,6 @@ AbstractClient::~AbstractClient() { } -Command_AccountPassword AbstractClient::makeAccountPasswordCommand(const QString &oldPassword, - const QString &newPassword) const -{ - Command_AccountPassword cmd; - cmd.set_old_password(oldPassword.toStdString()); - if (getServerSupportsChallengeResponse()) { - // A verifier lets the server upgrade the stored credential without ever seeing the - // password, and it is what a challenge-response server expects for a new password. - cmd.set_hashed_new_password(PasswordHasher::generatePasswordVerifier(newPassword).toStdString()); - } else if (getServerSupportsPasswordHash()) { - // The server hashes the password itself with its own salt; a salted hash is the - // strongest form it accepts, so the plaintext never goes on the wire. - const QString passwordSalt = PasswordHasher::generateRandomSalt(); - cmd.set_hashed_new_password(PasswordHasher::computeHash(newPassword, passwordSalt).toStdString()); - } else { - cmd.set_new_password(newPassword.toStdString()); - } - return cmd; -} - void AbstractClient::processProtocolItem(const ServerMessage &item) { switch (item.message_type()) { diff --git a/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.h b/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.h index 005587ccc..7efd67972 100644 --- a/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.h +++ b/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.h @@ -47,7 +47,6 @@ class Event_UserJoined; class Event_UserLeft; class Event_ServerMessage; class Event_ListRooms; -class Command_AccountPassword; class Event_GameJoined; class Event_UserMessage; class Event_NotifyUser; @@ -68,9 +67,6 @@ enum ClientStatus StatusSubmitForgotPasswordReset, StatusSubmitForgotPasswordChallenge, StatusGettingPasswordSalt, - //! The server accepted the credentials but keeps the session open until the account - //! password is replaced; the connection must survive for `Command_AccountPassword`. - StatusPasswordChangeRequired, }; class AbstractClient : public QObject @@ -175,16 +171,6 @@ public: { return serverSupportsChallengeResponse; } - - /** - * @brief Builds the account-password command in the form this server understands. - * - * Challenge-response servers take a scrypt verifier; servers that only advertise - * SupportsPasswordHash take a salted SHA-512 hash; anything older gets the plaintext, - * which is the only form it understands. Both password-change paths go through here so - * the two cannot drift apart and hand a plaintext password to a server that can hash it. - */ - Command_AccountPassword makeAccountPasswordCommand(const QString &oldPassword, const QString &newPassword) const; const QString &getUserName() const { return userName; diff --git a/libcockatrice_network/libcockatrice/network/client/local/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/client/local/CMakeLists.txt index 3eff7943a..4d5676fc0 100644 --- a/libcockatrice_network/libcockatrice/network/client/local/CMakeLists.txt +++ b/libcockatrice_network/libcockatrice/network/client/local/CMakeLists.txt @@ -1,3 +1,7 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + set(HEADERS local_client.h) set(SOURCES local_client.cpp) diff --git a/libcockatrice_network/libcockatrice/network/client/local/local_client.cpp b/libcockatrice_network/libcockatrice/network/client/local/local_client.cpp index b9a1566f8..f7d8645e8 100644 --- a/libcockatrice_network/libcockatrice/network/client/local/local_client.cpp +++ b/libcockatrice_network/libcockatrice/network/client/local/local_client.cpp @@ -33,18 +33,16 @@ LocalClient::LocalClient(LocalServerInterface *_lsi, sendCommand(prepareSessionCommand(joinCmd)); } +LocalClient::~LocalClient() +{ +} + void LocalClient::sendCommandContainer(const CommandContainer &cont) { if (LocalClientLog().isDebugEnabled()) { qCDebug(LocalClientLog).noquote() << userName << "OUT" << getSafeDebugString(cont); } - if (lsi.isNull()) { - qCWarning(LocalClientLog).noquote() - << userName << "dropping command, local server interface no longer exists:" << getSafeDebugString(cont); - return; - } - lsi->itemFromClient(cont); } diff --git a/libcockatrice_network/libcockatrice/network/client/local/local_client.h b/libcockatrice_network/libcockatrice/network/client/local/local_client.h index 15b36d3db..3ae570255 100644 --- a/libcockatrice_network/libcockatrice/network/client/local/local_client.h +++ b/libcockatrice_network/libcockatrice/network/client/local/local_client.h @@ -10,7 +10,6 @@ #include "../abstract/abstract_client.h" #include -#include #include #include @@ -25,15 +24,14 @@ class LocalClient : public AbstractClient { Q_OBJECT private: - // Guarded pointer: the LocalServer (and its interfaces) may be destroyed while this client - // still has pending commands queued on the event loop. - QPointer lsi; + LocalServerInterface *lsi; public: LocalClient(LocalServerInterface *_lsi, const QString &_playerName, const QString &_clientId, QObject *parent = nullptr); + ~LocalClient() override; void sendCommandContainer(const CommandContainer &cont) override; private slots: diff --git a/libcockatrice_network/libcockatrice/network/client/remote/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/client/remote/CMakeLists.txt index 006c18d42..ecc822b7c 100644 --- a/libcockatrice_network/libcockatrice/network/client/remote/CMakeLists.txt +++ b/libcockatrice_network/libcockatrice/network/client/remote/CMakeLists.txt @@ -1,3 +1,7 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + set(HEADERS remote_client.h) set(SOURCES remote_client.cpp) diff --git a/libcockatrice_network/libcockatrice/network/client/remote/remote_client.cpp b/libcockatrice_network/libcockatrice/network/client/remote/remote_client.cpp index faacdc9f7..7eab58668 100644 --- a/libcockatrice_network/libcockatrice/network/client/remote/remote_client.cpp +++ b/libcockatrice_network/libcockatrice/network/client/remote/remote_client.cpp @@ -125,7 +125,6 @@ RemoteClient::RemoteClient(QObject *parent, INetworkSettingsProvider *_networkSe &RemoteClient::doSubmitForgotPasswordResetToServer); connect(this, &RemoteClient::sigSubmitForgotPasswordChallengeToServer, this, &RemoteClient::doSubmitForgotPasswordChallengeToServer); - connect(this, &RemoteClient::sigChangePasswordToServer, this, &RemoteClient::doChangePasswordToServer); } RemoteClient::~RemoteClient() @@ -355,29 +354,6 @@ void RemoteClient::submitPasswordVerifierResponse(const Response &response) } } -void RemoteClient::changePasswordToServer(const QString &oldPassword, const QString &newPassword) -{ - emit sigChangePasswordToServer(oldPassword, newPassword); -} - -void RemoteClient::doChangePasswordToServer(const QString &oldPassword, const QString &newPassword) -{ - const Command_AccountPassword cmd = makeAccountPasswordCommand(oldPassword, newPassword); - - PendingCommand *pend = prepareSessionCommand(cmd); - connect(pend, &PendingCommand::finished, this, &RemoteClient::accountPasswordResponse); - sendCommand(pend); -} - -void RemoteClient::accountPasswordResponse(const Response &response) -{ - if (response.response_code() == Response::RespOk) { - emit sigPasswordChangeSuccess(); - } else { - emit sigPasswordChangeError(response.response_code()); - } -} - void RemoteClient::processConnectionClosedEvent(const Event_ConnectionClosed & /*event*/) { doDisconnectFromServer(); @@ -499,23 +475,6 @@ void RemoteClient::loginResponse(const Response &response) emit notifyUserAboutUpdate(); } - } else if (response.response_code() == Response::RespPasswordChangeRequired) { - // The credentials were accepted: the server keeps this session open and serves - // nothing but ping and Command_AccountPassword until the password is replaced. - // Treating it as a failed login would disconnect before the dialog that collects - // the new password (queued to the UI thread behind this slot) can send anything: - // the command would sit in pendingCommands forever, because the ping timer that - // times it out stops with the connection. Keep the session and let the controller - // report the outcome through accountPasswordResponse(). - QList missingFeatures; - if (resp.missing_features_size() > 0) { - for (int i = 0; i < resp.missing_features_size(); ++i) { - missingFeatures << QString::fromStdString(resp.missing_features(i)); - } - } - setStatus(StatusPasswordChangeRequired); - emit loginError(response.response_code(), QString::fromStdString(resp.denied_reason_str()), - static_cast(resp.denied_end_time()), missingFeatures); } else if (response.response_code() != Response::RespNotConnected) { QList missingFeatures; if (resp.missing_features_size() > 0) { diff --git a/libcockatrice_network/libcockatrice/network/client/remote/remote_client.h b/libcockatrice_network/libcockatrice/network/client/remote/remote_client.h index 1cf40491c..d0c0a18ce 100644 --- a/libcockatrice_network/libcockatrice/network/client/remote/remote_client.h +++ b/libcockatrice_network/libcockatrice/network/client/remote/remote_client.h @@ -71,11 +71,6 @@ signals: //! should only store it for the connection it is currently negotiating, so //! the hostname and user name are intentionally not part of the signal. void sigPasswordVerifierReady(const QString &verifier); - //! \brief Outcome of the password change the server demanded at login. - void sigPasswordChangeSuccess(); - void sigPasswordChangeError(int responseCode); - //! \brief Request a password change; queued to the client thread like the other requests. - void sigChangePasswordToServer(const QString &oldPassword, const QString &newPassword); private slots: void slotConnected(); void readData(); @@ -104,8 +99,6 @@ private slots: Command_Login generateCommandLogin(); void doSubmitPasswordVerifier(); void submitPasswordVerifierResponse(const Response &response); - void accountPasswordResponse(const Response &response); - void doChangePasswordToServer(const QString &oldPassword, const QString &newPassword); void doDisconnectFromServer(); void doActivateToServer(const QString &_token); void doRequestForgotPasswordToServer(const QString &hostname, unsigned int port, const QString &_userName); @@ -208,10 +201,6 @@ public: unsigned int port, const QString &_userName, const QString &_email); - //! \brief Change the password of the account that just logged in, following the - //! server's forced-password-change reply. Emits sigPasswordChangeSuccess or - //! sigPasswordChangeError when the server answers. - void changePasswordToServer(const QString &oldPassword, const QString &newPassword); }; #endif diff --git a/libcockatrice_network/libcockatrice/network/server/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/server/CMakeLists.txt index b59d61572..cbb717ad8 100644 --- a/libcockatrice_network/libcockatrice/network/server/CMakeLists.txt +++ b/libcockatrice_network/libcockatrice/network/server/CMakeLists.txt @@ -1,3 +1,7 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + add_subdirectory(local) add_subdirectory(remote) diff --git a/libcockatrice_network/libcockatrice/network/server/local/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/server/local/CMakeLists.txt index 4055efa64..c5f8ee432 100644 --- a/libcockatrice_network/libcockatrice/network/server/local/CMakeLists.txt +++ b/libcockatrice_network/libcockatrice/network/server/local/CMakeLists.txt @@ -1,3 +1,7 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + set(HEADERS local_server.h local_server_interface.h) set(SOURCES local_server.cpp local_server_interface.cpp) diff --git a/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt index 181218e1a..26c2be69a 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt +++ b/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt @@ -1,3 +1,7 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + set(HEADERS game/dump_zone_range.h game/server_abstract_participant.h @@ -8,7 +12,6 @@ set(HEADERS game/server_cardzone.h game/server_counter.h game/game_config.h - game/role_info.h game/server_deck_validation_strategy.h game/server_game.h game/server_game_lifecycle_strategy.h diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/role_info.h b/libcockatrice_network/libcockatrice/network/server/remote/game/role_info.h deleted file mode 100644 index ef8692361..000000000 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/role_info.h +++ /dev/null @@ -1,13 +0,0 @@ -#ifndef COCKATRICE_ROLE_INFO_H -#define COCKATRICE_ROLE_INFO_H - -/** - * @brief Represents information about a participant's roles. - */ -struct RoleInfo -{ - bool isJudge = false; ///< Whether the participant joined as a judge - bool isSpectator = false; ///< Whether the participant joined as a spectator -}; - -#endif // COCKATRICE_ROLE_INFO_H diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp index ff8ea14f3..5b8d901bb 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp @@ -61,10 +61,10 @@ Server_AbstractParticipant::Server_AbstractParticipant(Server_Game *_game, int _playerId, const ServerInfo_User &_userInfo, - const RoleInfo &_roleInfo, + bool _judge, Server_AbstractUserInterface *_userInterface) : ServerInfo_User_Container(_userInfo), game(_game), userInterface(_userInterface), pingTime(0), - playerId(_playerId), roleInfo(_roleInfo) + playerId(_playerId), judge(_judge) { } @@ -98,8 +98,8 @@ void Server_AbstractParticipant::getProperties(ServerInfo_PlayerProperties &resu if (withUserInfo) { copyUserInfo(*(result.mutable_user_info()), true); } - result.set_spectator(isSpectator()); - result.set_judge(isJudge()); + result.set_spectator(spectator); + result.set_judge(judge); result.set_ping_seconds(pingTime); getPlayerProperties(result); } @@ -176,7 +176,7 @@ Response::ResponseCode Server_AbstractParticipant::cmdUnconcede(const Command_Un Response::ResponseCode Server_AbstractParticipant::cmdJudge(const Command_Judge &cmd, ResponseContainer &rc, GameEventStorage &ges) { - if (!isJudge()) { + if (!judge) { return Response::RespFunctionNotAllowed; } @@ -204,7 +204,7 @@ Response::ResponseCode Server_AbstractParticipant::cmdReadyStart(const Command_R Response::ResponseCode Server_AbstractParticipant::cmdGameSay(const Command_GameSay &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) { - if (isSpectator()) { + if (spectator) { /* Spectators can only talk if: * (a) the game creator allows it * (b) the spectator is a moderator/administrator @@ -365,7 +365,7 @@ Response::ResponseCode Server_AbstractParticipant::cmdNextTurn(const Command_Nex return Response::RespGameNotStarted; } - if (!isJudge()) { + if (!judge) { return Response::RespFunctionNotAllowed; } @@ -381,7 +381,7 @@ Response::ResponseCode Server_AbstractParticipant::cmdSetActivePhase(const Comma return Response::RespGameNotStarted; } - if (!isJudge()) { + if (!judge) { return Response::RespFunctionNotAllowed; } @@ -415,8 +415,8 @@ Response::ResponseCode Server_AbstractParticipant::cmdReverseTurn(const Command_ ResponseContainer & /*rc*/, GameEventStorage &ges) { - if (!isJudge()) { - if (isSpectator()) { + if (!judge) { + if (spectator) { return Response::RespFunctionNotAllowed; } @@ -577,7 +577,7 @@ void Server_AbstractParticipant::setUserInterface(Server_AbstractUserInterface * void Server_AbstractParticipant::disconnectClient() { bool isRegistered = userInfo->user_level() & ServerInfo_User::IsRegistered; - if (!isRegistered || isSpectator()) { + if (!isRegistered || spectator) { game->removeParticipant(this, Event_Leave::USER_DISCONNECTED); } else { setUserInterface(nullptr); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h index b6d4a8025..195da9b7f 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h @@ -2,7 +2,6 @@ #define ABSTRACT_PARTICIPANT_H #include "../serverinfo_user_container.h" -#include "role_info.h" #include "server_arrowtarget.h" #include @@ -62,7 +61,8 @@ protected: Server_AbstractUserInterface *userInterface; int pingTime; int playerId; - RoleInfo roleInfo; + bool spectator; + bool judge; virtual void getPlayerProperties(ServerInfo_PlayerProperties &result); mutable QMutex playerMutex; @@ -70,7 +70,7 @@ public: Server_AbstractParticipant(Server_Game *_game, int _playerId, const ServerInfo_User &_userInfo, - const RoleInfo &_roleInfo, + bool _judge, Server_AbstractUserInterface *_handler); ~Server_AbstractParticipant() override; virtual void prepareDestroy() @@ -91,11 +91,11 @@ public: } bool isSpectator() const { - return roleInfo.isSpectator; + return spectator; } bool isJudge() const { - return roleInfo.isJudge; + return judge; } Server_Game *getGame() const { 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 b5a620798..c03f1e966 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 @@ -81,9 +81,10 @@ Server_AbstractPlayer::Server_AbstractPlayer(Server_Game *_game, const ServerInfo_User &_userInfo, bool _judge, Server_AbstractUserInterface *_userInterface) - : Server_AbstractParticipant(_game, _playerId, _userInfo, {.isJudge = _judge}, _userInterface), conceded(false), - deck(nullptr), sideboardLocked(true), readyStart(false), nextCardId(0) + : Server_AbstractParticipant(_game, _playerId, _userInfo, _judge, _userInterface), conceded(false), deck(nullptr), + sideboardLocked(true), readyStart(false), nextCardId(0) { + spectator = false; } Server_AbstractPlayer::~Server_AbstractPlayer() = default; @@ -294,7 +295,7 @@ Response::ResponseCode Server_AbstractPlayer::moveCard(GameEventStorage &ges, { // Disallow controller change to other zones than the table. if (((targetzone->getType() != ServerInfo_Zone::PublicZone) || !targetzone->hasCoords()) && - (startzone->getPlayer() != targetzone->getPlayer()) && !isJudge()) { + (startzone->getPlayer() != targetzone->getPlayer()) && !judge) { return Response::RespContextError; } @@ -807,7 +808,7 @@ Server_AbstractPlayer::cmdMoveCard(const Command_MoveCard &cmd, ResponseContaine return Response::RespNameNotFound; } - if ((startPlayer != this) && (!startZone->getPlayersWithWritePermission().contains(playerId)) && !isJudge()) { + if ((startPlayer != this) && (!startZone->getPlayersWithWritePermission().contains(playerId)) && !judge) { return Response::RespContextError; } @@ -820,7 +821,7 @@ Server_AbstractPlayer::cmdMoveCard(const Command_MoveCard &cmd, ResponseContaine return Response::RespNameNotFound; } - if ((startPlayer != this) && (targetPlayer != this) && !isJudge()) { + if ((startPlayer != this) && (targetPlayer != this) && !judge) { return Response::RespContextError; } @@ -1597,7 +1598,7 @@ Server_AbstractPlayer::cmdRevealCards(const Command_RevealCards &cmd, ResponseCo zone->addWritePermission(cmd.player_id()); } - if (isJudge()) { + if (judge) { ges.setOverwriteOwnership(true); } diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp index 49db999f6..b1863c91e 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp @@ -41,7 +41,6 @@ #include "libcockatrice/protocol/pb/serverinfo_replay_match.pb.h" #include "libcockatrice/protocol/pb/serverinfo_user.pb.h" #include "libcockatrice/protocol/pb/session_event.pb.h" -#include "role_info.h" #include "server_abstract_player.h" #include "server_abstractuserinterface.h" #include "server_arrow.h" @@ -480,8 +479,9 @@ void Server_Game::stopGameIfFinished() Response::ResponseCode Server_Game::checkJoin(ServerInfo_User *user, const QString &_password, - const RoleInfo &roleInfo, - bool overrideRestrictions) + bool spectator, + bool overrideRestrictions, + bool asJudge) { Server_DatabaseInterface *databaseInterface = room->getServer()->getDatabaseInterface(); for (auto *participant : participants.values()) { @@ -490,11 +490,11 @@ Response::ResponseCode Server_Game::checkJoin(ServerInfo_User *user, } } - if (roleInfo.isJudge && !(user->user_level() & ServerInfo_User::IsJudge)) { + if (asJudge && !(user->user_level() & ServerInfo_User::IsJudge)) { return Response::RespUserLevelTooLow; } if (!(overrideRestrictions && (user->user_level() & (ServerInfo_User::IsModerator | ServerInfo_User::IsJudge)))) { - if ((_password != password) && !(roleInfo.isSpectator && !spectatorsNeedPassword)) { + if ((_password != password) && !(spectator && !spectatorsNeedPassword)) { return Response::RespWrongPassword; } if (!(user->user_level() & ServerInfo_User::IsRegistered) && onlyRegistered) { @@ -510,13 +510,13 @@ Response::ResponseCode Server_Game::checkJoin(ServerInfo_User *user, QString::fromStdString(user->name()))) { return Response::RespInIgnoreList; } - if (roleInfo.isSpectator) { + if (spectator) { if (!spectatorsAllowed) { return Response::RespSpectatorsNotAllowed; } } } - if (!roleInfo.isSpectator && (gameStarted || (getPlayerCount() >= getMaxPlayers()))) { + if (!spectator && (gameStarted || (getPlayerCount() >= getMaxPlayers()))) { return Response::RespGameFull; } @@ -537,18 +537,19 @@ bool Server_Game::containsUser(const QString &userName) const void Server_Game::addPlayer(Server_AbstractUserInterface *userInterface, ResponseContainer &rc, - const RoleInfo &roleInfo, + bool spectator, + bool judge, bool broadcastUpdate) { QMutexLocker locker(&gameMutex); Server_AbstractParticipant *newParticipant; - if (roleInfo.isSpectator) { + if (spectator) { newParticipant = new Server_Spectator(this, nextPlayerId++, userInterface->copyUserInfo(true, true, true), - roleInfo.isJudge, userInterface); + judge, userInterface); } else { - newParticipant = new Server_Player(this, nextPlayerId++, userInterface->copyUserInfo(true, true, true), - roleInfo.isJudge, userInterface); + newParticipant = new Server_Player(this, nextPlayerId++, userInterface->copyUserInfo(true, true, true), judge, + userInterface); } newParticipant->moveToThread(thread()); @@ -559,7 +560,7 @@ void Server_Game::addPlayer(Server_AbstractUserInterface *userInterface, const QString playerName = QString::fromStdString(newParticipant->getUserInfo()->name()); participants.insert(newParticipant->getPlayerId(), newParticipant); - if (roleInfo.isSpectator) { + if (spectator) { allSpectatorsEver.insert(playerName); } else { allPlayersEver.insert(playerName); @@ -578,7 +579,7 @@ void Server_Game::addPlayer(Server_AbstractUserInterface *userInterface, emit gameInfoChanged(gameInfo); } - if ((newParticipant->getUserInfo()->user_level() & ServerInfo_User::IsRegistered) && !roleInfo.isSpectator) { + if ((newParticipant->getUserInfo()->user_level() & ServerInfo_User::IsRegistered) && !spectator) { room->getServer()->addPersistentPlayer(playerName, room->getId(), gameId, newParticipant->getPlayerId()); } @@ -870,13 +871,6 @@ void Server_Game::createGameJoinedEvent(Server_AbstractParticipant *joiningParti } } -bool Server_Game::canSeePrivateEventsFor(const Server_AbstractParticipant *participant, int privatePlayerId) const -{ - return participant->getPlayerId() == privatePlayerId // same player - || participant->isJudge() // judges see everything - || (participant->isSpectator() && spectatorsSeeEverything); // spectators see everything -} - void Server_Game::sendGameEventContainer(GameEventContainer *cont, GameEventStorageItem::EventRecipients recipients, int privatePlayerId) @@ -885,7 +879,8 @@ void Server_Game::sendGameEventContainer(GameEventContainer *cont, cont->set_game_id(gameId); for (auto *participant : participants.values()) { - const bool playerPrivate = canSeePrivateEventsFor(participant, privatePlayerId); + const bool playerPrivate = (participant->getPlayerId() == privatePlayerId) || participant->isJudge() || + (participant->isSpectator() && spectatorsSeeEverything); if ((recipients.testFlag(GameEventStorageItem::SendToPrivate) && playerPrivate) || (recipients.testFlag(GameEventStorageItem::SendToOthers) && !playerPrivate)) { participant->sendGameEvent(*cont); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h index 363c0ca94..c7f8d0929 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h @@ -62,7 +62,6 @@ class Message; } // namespace protobuf } // namespace google struct GameConfig; -struct RoleInfo; class Server_Game : public QObject { @@ -110,7 +109,6 @@ private: bool omniscient, bool withUserInfo); void storeGameInformation(); - bool canSeePrivateEventsFor(const Server_AbstractParticipant *participant, int privatePlayerId) const; signals: void sigStartGameIfReady(bool override); void gameInfoChanged(ServerInfo_Game gameInfo); @@ -190,11 +188,12 @@ public: return shareDecklistsOnLoad; } Response::ResponseCode - checkJoin(ServerInfo_User *user, const QString &_password, const RoleInfo &roleInfo, bool overrideRestrictions); + checkJoin(ServerInfo_User *user, const QString &_password, bool spectator, bool overrideRestrictions, bool asJudge); bool containsUser(const QString &userName) const; void addPlayer(Server_AbstractUserInterface *userInterface, ResponseContainer &rc, - const RoleInfo &roleInfo, + bool spectator, + bool judge, bool broadcastUpdate = true); void removeParticipant(Server_AbstractParticipant *participant, Event_Leave::LeaveReason reason); void removeArrowsRelatedToPlayer(GameEventStorage &ges, Server_AbstractPlayer *player); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp index b40dae09b..949d04b4b 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp @@ -573,7 +573,7 @@ Server_Player::cmdNextTurn(const Command_NextTurn & /*cmd*/, ResponseContainer & return Response::RespGameNotStarted; } - if (conceded && !isJudge()) { + if (conceded && !judge) { return Response::RespContextError; } @@ -589,7 +589,7 @@ Response::ResponseCode Server_Player::cmdSetActivePhase(const Command_SetActiveP return Response::RespGameNotStarted; } - if (!isJudge()) { + if (!judge) { if (conceded) { return Response::RespContextError; } @@ -623,7 +623,7 @@ Response::ResponseCode Server_Player::cmdReverseTurn(const Command_ReverseTurn &cmd, ResponseContainer &rc, GameEventStorage &ges) { - if (!isJudge() && conceded) { + if (!judge && conceded) { return Response::RespContextError; } return Server_AbstractParticipant::cmdReverseTurn(cmd, rc, ges); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_spectator.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_spectator.cpp index 164a4af5e..d1c369949 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_spectator.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_spectator.cpp @@ -11,6 +11,7 @@ Server_Spectator::Server_Spectator(Server_Game *_game, const ServerInfo_User &_userInfo, bool _judge, Server_AbstractUserInterface *_userInterface) - : Server_AbstractParticipant(_game, _playerId, _userInfo, {.isJudge = _judge, .isSpectator = true}, _userInterface) + : Server_AbstractParticipant(_game, _playerId, _userInfo, _judge, _userInterface) { + spectator = true; } diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server.cpp index 8ea2cd993..63989bbfa 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/server.cpp @@ -131,16 +131,6 @@ AuthenticationResult Server::loginUser(Server_ProtocolHandler *session, return authState; } - if (authState == PasswordChangeRequired) { - // The credentials were right, but the account must choose a new password before it may - // use the server. Give the session an identity so it can complete that change, without - // registering a user session or announcing the user as logged in. - ServerInfo_User pendingData = databaseInterface->getUserData(name, true); - pendingData.set_address(session->getAddress().toStdString()); - session->setUserInfo(pendingData); - return authState; - } - ServerInfo_User data = databaseInterface->getUserData(name, true); data.set_address(session->getAddress().toStdString()); name = QString::fromStdString(data.name()); // Compensate for case indifference @@ -306,9 +296,7 @@ void Server::removeClient(Server_ProtocolHandler *client) QWriteLocker locker(&clientsLock); clients.removeAt(clientIndex); ServerInfo_User *data = client->getUserInfo(); - // A session that never completed login (for example one that still owes a password change) - // has no session id: it was never announced as joined, so it must not be announced as leaving. - if (data && data->has_session_id()) { + if (data) { Event_UserLeft event; event.set_name(data->name()); SessionEvent *se = Server_ProtocolHandler::prepareSessionEvent(event); @@ -323,10 +311,12 @@ void Server::removeClient(Server_ProtocolHandler *client) users.remove(QString::fromStdString(data->name())); qDebug() << "Server::removeClient: name=" << QString::fromStdString(data->name()); - const qint64 sessionId = data->session_id(); - usersBySessionId.remove(sessionId); - emit endSession(sessionId); - qDebug() << "closed session id:" << sessionId; + if (data->has_session_id()) { + const qint64 sessionId = data->session_id(); + usersBySessionId.remove(sessionId); + emit endSession(sessionId); + qDebug() << "closed session id:" << sessionId; + } } qDebug() << "Server::removeClient: removed" << (void *)client << ";" << clients.size() << "clients; " << users.size() << "users left"; @@ -340,9 +330,7 @@ QList Server::getOnlineModeratorList() const ServerInfo_User *data = client->getUserInfo(); //! \todo This line should be updated in the event there is any type of new user level created. - // A session that still owes a forced password change has user info but no session id: - // it never joined, so it must not be reported as an online moderator either. - if (data && data->has_session_id() && + if (data && (data->user_level() & ServerInfo_User::IsModerator || data->user_level() & ServerInfo_User::IsAdmin)) { results << QString::fromStdString(data->name()).simplified(); } diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp index f9cadc9aa..dd074a517 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp @@ -64,9 +64,8 @@ Server_ProtocolHandler::Server_ProtocolHandler(Server *_server, Server_DatabaseInterface *_databaseInterface, QObject *parent) : QObject(parent), Server_AbstractUserInterface(_server), deleted(false), databaseInterface(_databaseInterface), - authState(NotLoggedIn), usingRealPassword(false), passwordChangePending(false), acceptsUserListChanges(false), - acceptsRoomListChanges(false), idleClientWarningSent(false), timeRunning(0), lastDataReceived(0), - lastActionReceived(0) + authState(NotLoggedIn), usingRealPassword(false), acceptsUserListChanges(false), acceptsRoomListChanges(false), + idleClientWarningSent(false), timeRunning(0), lastDataReceived(0), lastActionReceived(0) { connect(server, &Server::pingClockTimeout, this, &Server_ProtocolHandler::pingClockTimeout); @@ -213,10 +212,7 @@ Response::ResponseCode Server_ProtocolHandler::processSessionCommandContainer(co logDebugMessage(getSafeDebugString(sc)); } const auto commandType = static_cast(num); - if (passwordChangePending && commandType != SessionCommand::PING && - commandType != SessionCommand::ACCOUNT_PASSWORD) { - resp = Response::RespFunctionNotAllowed; - } else if (authState == NotLoggedIn && !isPreAuthSessionCommand(commandType)) { + if (authState == NotLoggedIn && !isPreAuthSessionCommand(commandType)) { resp = Response::RespLoginNeeded; } else { switch (commandType) { @@ -493,19 +489,6 @@ void Server_ProtocolHandler::processCommandContainer(const CommandContainer &con lastDataReceived = timeRunning; - // A session that still owes a forced password change may only reach the session family, - // where processSessionCommandContainer narrows it further to ping and the change itself. - // Every other family is rejected here, including a container that also carries a session - // command: the dispatch below prefers game/room over session, so testing only for the - // absence of a session command would let such a container through. - if (passwordChangePending && - (cont.game_command_size() || cont.room_command_size() || cont.moderator_command_size() || - cont.admin_command_size() || cont.developer_command_size())) { - sendResponseContainer(ResponseContainer(cont.has_cmd_id() ? cont.cmd_id() : -1), - Response::RespFunctionNotAllowed); - return; - } - ResponseContainer responseContainer(cont.has_cmd_id() ? cont.cmd_id() : -1); Response::ResponseCode finalResponseCode; @@ -713,11 +696,6 @@ Response::ResponseCode Server_ProtocolHandler::cmdLogin(const Command_Login &cmd server->recordFailedLogin(getAddress(), userName); return Response::RespAccountNotActivated; case PasswordChangeRequired: - // The credentials were right, but the account must choose a new password first. - // Keep the session authenticated just far enough for the client to do that: - // processCommandContainer refuses everything except the change itself. - authState = PasswordRight; - passwordChangePending = true; return Response::RespPasswordChangeRequired; default: server->clearFailedLogins(getAddress(), userName); @@ -1141,8 +1119,7 @@ Server_ProtocolHandler::cmdCreateGame(const Command_CreateGame &cmd, Server_Room auto *game = new Server_Game(config, room); - RoleInfo roleInfo{.isJudge = asJudge, .isSpectator = asSpectator}; - game->addPlayer(this, rc, roleInfo, false); + game->addPlayer(this, rc, asSpectator, asJudge, false); room->addGame(game); return Response::RespOk; diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.h b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.h index 8ce110354..266c4b26c 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.h @@ -46,12 +46,6 @@ protected: Server_DatabaseInterface *databaseInterface; AuthenticationResult authState; bool usingRealPassword; - /** - * @brief True when the credentials were right but the account must choose a new password - * before it may use the server. Only ping, the password change and disconnecting - * are served while this is set. - */ - bool passwordChangePending; bool acceptsUserListChanges; bool acceptsRoomListChanges; bool idleClientWarningSent; diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_room.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server_room.cpp index 78ef2b289..68b137cd4 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_room.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_room.cpp @@ -1,6 +1,5 @@ #include "server_room.h" -#include "game/role_info.h" #include "game/server_game.h" #include "libcockatrice/protocol/pb/response.pb.h" #include "libcockatrice/protocol/pb/room_event.pb.h" @@ -373,11 +372,11 @@ Response::ResponseCode Server_Room::processJoinGameCommand(const Command_JoinGam QMutexLocker gameLocker(&game->gameMutex); - RoleInfo roleInfo{.isJudge = cmd.join_as_judge(), .isSpectator = cmd.spectator()}; - Response::ResponseCode result = game->checkJoin( - userInterface->getUserInfo(), QString::fromStdString(cmd.password()), roleInfo, cmd.override_restrictions()); + Response::ResponseCode result = + game->checkJoin(userInterface->getUserInfo(), QString::fromStdString(cmd.password()), cmd.spectator(), + cmd.override_restrictions(), cmd.join_as_judge()); if (result == Response::RespOk) { - game->addPlayer(userInterface, rc, roleInfo); + game->addPlayer(userInterface, rc, cmd.spectator(), cmd.join_as_judge()); } return result; diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto index 04cb31d98..292917fa9 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto @@ -87,16 +87,14 @@ message Command_ViewLogHistory { extend DeveloperCommand { optional Command_ViewLogHistory dev_ext = 1001; } - optional string user_name = 1; // user that created message - optional string ip_address = 2; // ip address of user that created message - optional string game_name = 3; // client id of user that created the message - optional string game_id = 4; // game number the message was sent to - optional string message = 5; // raw message that was sent - repeated string log_location = 6; // destination of message (ex: main room, game room, private chat) - required uint32 date_range = 7; // the length of time (in minutes) to look back for - optional uint32 maximum_results = 8; // the maximum number of query results (page size when paginating) - optional uint32 offset = 9 [default = 0]; // rows to skip for pagination, ORDER BY log_time DESC - optional string before_time = 10; // page those rows against this `log_time` snapshot (newest first page row) + optional string user_name = 1; // user that created message + optional string ip_address = 2; // ip address of user that created message + optional string game_name = 3; // client id of user that created the message + optional string game_id = 4; // game number the message was sent to + optional string message = 5; // raw message that was sent + repeated string log_location = 6; // destination of message (ex: main room, game room, private chat) + required uint32 date_range = 7; // the length of time (in minutes) to look back for + optional uint32 maximum_results = 8; // the maximum number of query results } message Command_GrantReplayAccess { diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_viewlog_history.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_viewlog_history.proto index 1041bca74..2ec0aff28 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/response_viewlog_history.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response_viewlog_history.proto @@ -7,5 +7,4 @@ message Response_ViewLogHistory { optional Response_ViewLogHistory ext = 1015; } repeated ServerInfo_ChatMessage log_message = 1; - optional bool has_more = 2 [default = false]; // true when more rows exist past this page } diff --git a/libcockatrice_rng/CMakeLists.txt b/libcockatrice_rng/CMakeLists.txt index 77165122c..c5b19ae85 100644 --- a/libcockatrice_rng/CMakeLists.txt +++ b/libcockatrice_rng/CMakeLists.txt @@ -1,3 +1,7 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + set(HEADERS libcockatrice/rng/rng_abstract.h libcockatrice/rng/rng_sfmt.h libcockatrice/rng/sfmt/SFMT.h) add_library( diff --git a/libcockatrice_settings/CMakeLists.txt b/libcockatrice_settings/CMakeLists.txt index a3b9a68db..d23fb3350 100644 --- a/libcockatrice_settings/CMakeLists.txt +++ b/libcockatrice_settings/CMakeLists.txt @@ -1,3 +1,7 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + set(HEADERS libcockatrice/settings/appearance_settings.h libcockatrice/settings/cache_storage_settings.h diff --git a/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp b/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp index 38b87b97c..4595da2a2 100644 --- a/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp @@ -5,118 +5,16 @@ #include #include #include -#include -#include +#include #include -#include -#include +#include #include -namespace -{ -constexpr auto SERVER_GROUP = "server"; -constexpr auto SERVER_DETAILS_GROUP = "server_details"; -constexpr auto SERVERS_ARRAY = "servers"; - -QString legacyKey(const char *field, int index) -{ - return QStringLiteral("%1/%2%3") - .arg(QString::fromLatin1(SERVER_DETAILS_GROUP), QString::fromLatin1(field)) - .arg(index); -} - -//! Expects \a settings to be positioned on the server group. -QList readServerArray(QSettings &settings) -{ - QList servers; - - settings.beginGroup(SERVER_DETAILS_GROUP); - const int size = settings.beginReadArray(SERVERS_ARRAY); - servers.reserve(size); - for (int i = 0; i < size; ++i) { - settings.setArrayIndex(i); - ServersSettings::ServerEntry entry; - entry.saveName = settings.value("saveName").toString(); - entry.host = settings.value("server").toString(); - entry.port = settings.value("port").toString(); - entry.username = settings.value("username").toString(); - entry.password = settings.value("password").toString(); - entry.savePassword = settings.value("savePassword").toBool(); - entry.site = settings.value("site").toString(); - servers.append(entry); - } - settings.endArray(); - settings.endGroup(); - - return servers; -} - -//! Expects \a settings to be positioned on the server group. -QList readLegacyServers(QSettings &settings, int lastIndex) -{ - QList servers; - - for (int i = 0; i <= lastIndex; ++i) { - ServersSettings::ServerEntry entry; - entry.saveName = settings.value(legacyKey("saveName", i)).toString(); - entry.host = settings.value(legacyKey("server", i)).toString(); - entry.port = settings.value(legacyKey("port", i)).toString(); - entry.username = settings.value(legacyKey("username", i)).toString(); - entry.password = settings.value(legacyKey("password", i)).toString(); - entry.savePassword = settings.value(legacyKey("savePassword", i)).toBool(); - entry.site = settings.value(legacyKey("site", i)).toString(); - - // Older builds could leave the unused index 0 behind. - if (!entry.saveName.isEmpty() || !entry.host.isEmpty()) { - servers.append(entry); - } - } - - return servers; -} - -void writeServerArray(QSettings &settings, const QList &servers) -{ - settings.beginGroup(SERVER_DETAILS_GROUP); - // beginWriteArray() never drops entries past the new size, so without this - // a removed profile would keep its slot - including its saved password - - // in servers.ini forever. The legacy keys live beside the array, not in it. - settings.remove(SERVERS_ARRAY); - settings.beginWriteArray(SERVERS_ARRAY, servers.size()); - for (int i = 0; i < servers.size(); ++i) { - settings.setArrayIndex(i); - const ServersSettings::ServerEntry &entry = servers.at(i); - settings.setValue("saveName", entry.saveName); - settings.setValue("server", entry.host); - settings.setValue("port", entry.port); - settings.setValue("username", entry.username); - settings.setValue("password", entry.password); - settings.setValue("savePassword", entry.savePassword); - settings.setValue("site", entry.site); - } - settings.endArray(); - settings.endGroup(); -} -} // namespace +class QObject; ServersSettings::ServersSettings(const QString &settingPath, QObject *parent) - : SettingsManager(settingPath + "servers.ini", SERVER_GROUP, QString(), parent) + : SettingsManager(settingPath + "servers.ini", "server", QString(), parent) { - migrateLegacyServers(); -} - -QList ServersSettings::getServers() const -{ - return loadServers(); -} - -std::optional ServersSettings::getServer(int index) const -{ - const QList servers = loadServers(); - if (index < 0 || index >= servers.size()) { - return std::nullopt; - } - return servers.at(index); } void ServersSettings::setPreviousHostLogin(int previous) @@ -147,14 +45,16 @@ void ServersSettings::setPrevioushostName(const QString &name) QString ServersSettings::getSaveName(QString defaultname) { - const auto server = getServer(getPrevioushostindex(getPrevioushostName())); - return server ? server->saveName : std::move(defaultname); + int index = getPrevioushostindex(getPrevioushostName()); + QVariant saveName = getValue(QString("saveName%1").arg(index), "server", "server_details"); + return saveName == QVariant() ? std::move(defaultname) : saveName.toString(); } QString ServersSettings::getSite(QString defaultSite) { - const auto server = getServer(getPrevioushostindex(getPrevioushostName())); - return server ? server->site : std::move(defaultSite); + int index = getPrevioushostindex(getPrevioushostName()); + QVariant site = getValue(QString("site%1").arg(index), "server", "server_details"); + return site == QVariant() ? std::move(defaultSite) : site.toString(); } QString ServersSettings::getPrevioushostName() const @@ -165,9 +65,10 @@ QString ServersSettings::getPrevioushostName() const int ServersSettings::getPrevioushostindex(const QString &saveName) const { - const QList servers = loadServers(); - for (int i = 0; i < servers.size(); ++i) { - if (servers.at(i).saveName == saveName) { + int size = getValue("totalServers", "server", "server_details").toInt(); + + for (int i = 0; i <= size; ++i) { + if (saveName == getValue(QString("saveName%1").arg(i), "server", "server_details").toString()) { return i; } } @@ -177,38 +78,43 @@ int ServersSettings::getPrevioushostindex(const QString &saveName) const QString ServersSettings::getHostname(QString defaultHost) const { - const auto server = getServer(getPrevioushostindex(getPrevioushostName())); - return server ? server->host : std::move(defaultHost); + int index = getPrevioushostindex(getPrevioushostName()); + QVariant hostname = getValue(QString("server%1").arg(index), "server", "server_details"); + return hostname == QVariant() ? std::move(defaultHost) : hostname.toString(); } QString ServersSettings::getPort(QString defaultPort) const { - const auto server = getServer(getPrevioushostindex(getPrevioushostName())); - qCDebug(ServersSettingsLog) << "getPort() found server = " << server.has_value(); - return server ? server->port : std::move(defaultPort); + int index = getPrevioushostindex(getPrevioushostName()); + QVariant port = getValue(QString("port%1").arg(index), "server", "server_details"); + qCDebug(ServersSettingsLog) << "getPort() index = " << index << " port.val = " << port.toString(); + return port == QVariant() ? std::move(defaultPort) : port.toString(); } QString ServersSettings::getPlayerName(QString defaultName) const { - const auto server = getServer(getPrevioushostindex(getPrevioushostName())); - qCDebug(ServersSettingsLog) << "getPlayerName() found server = " << server.has_value(); - return server ? server->username : std::move(defaultName); + int index = getPrevioushostindex(getPrevioushostName()); + QVariant name = getValue(QString("username%1").arg(index), "server", "server_details"); + qCDebug(ServersSettingsLog) << "getPlayerName() index = " << index << " name.val = " << name.toString(); + return name == QVariant() ? std::move(defaultName) : name.toString(); } QString ServersSettings::getPassword() { - const auto server = getServer(getPrevioushostindex(getPrevioushostName())); - if (!server || !server->savePassword) { - return QString(); + int index = getPrevioushostindex(getPrevioushostName()); + + if (getSavePassword()) { + return getValue(QString("password%1").arg(index), "server", "server_details").toString(); } - return server->password; + return QString(); } bool ServersSettings::getSavePassword() const { - const auto server = getServer(getPrevioushostindex(getPrevioushostName())); - return server && server->savePassword; + int index = getPrevioushostindex(getPrevioushostName()); + bool save = getValue(QString("savePassword%1").arg(index), "server", "server_details").toBool(); + return save; } void ServersSettings::setAutoConnect(int autoconnect) @@ -251,19 +157,15 @@ void ServersSettings::setFPPlayerName(QString playerName) void ServersSettings::setServerPassword(const QString &saveName, const QString &password) { - QList servers = loadServers(); - for (ServerEntry &server : servers) { - if (server.saveName == saveName) { - server.password = password; - storeServers(servers); - return; - } + const int index = getPrevioushostindex(saveName); + if (index >= 0) { + setValue(password, QString("password%1").arg(index), "server", "server_details"); + } else { + // A credential write that silently no-ops is hard to diagnose from a + // bug report; surface a mismatched profile name instead. + qCWarning(ServersSettingsLog) << "setServerPassword() could not find profile:" << saveName + << "- password not saved"; } - - // A credential write that silently no-ops is hard to diagnose from a bug - // report; surface a mismatched profile name instead. - qCWarning(ServersSettingsLog) << "setServerPassword() could not find profile:" << saveName - << "- password not saved"; } QString ServersSettings::getFPPlayerName(QString defaultName) const @@ -300,29 +202,62 @@ void ServersSettings::addNewServer(const QString &saveName, return; } - QList servers = loadServers(); - ServerEntry entry; - entry.saveName = saveName; - entry.host = serv; - entry.port = port; - entry.username = username; - entry.password = password; - entry.savePassword = savePassword; - entry.site = site; - servers.append(entry); - storeServers(servers); + int index = getValue("totalServers", "server", "server_details").toInt() + 1; + + setValue(saveName, QString("saveName%1").arg(index), "server", "server_details"); + setValue(serv, QString("server%1").arg(index), "server", "server_details"); + setValue(port, QString("port%1").arg(index), "server", "server_details"); + setValue(username, QString("username%1").arg(index), "server", "server_details"); + setValue(savePassword, QString("savePassword%1").arg(index), "server", "server_details"); + setValue(index, "totalServers", "server", "server_details"); + setValue(password, QString("password%1").arg(index), "server", "server_details"); + setValue(site, QString("site%1").arg(index), "server", "server_details"); } void ServersSettings::removeServer(QString servAddr) { - QList servers = loadServers(); - for (int i = 0; i < servers.size(); ++i) { - if (servers.at(i).host == servAddr) { - servers.removeAt(i); - storeServers(servers); - return; + int size = getValue("totalServers", "server", "server_details").toInt(); + + bool found = false; + for (int i = 0; i <= size; ++i) { + if (!found) { + // find entry and overwrite it + if (servAddr == getValue(QString("server%1").arg(i), "server", "server_details").toString()) { + found = true; + } + } else { + // move all other entries after it one back, overwriting the previous one + int previous = i - 1; // we delete only one entry + setValue(getValue(QString("server%1").arg(i), "server", "server_details"), + QString("server%1").arg(previous), "server", "server_details"); + setValue(getValue(QString("port%1").arg(i), "server", "server_details"), QString("port%1").arg(previous), + "server", "server_details"); + setValue(getValue(QString("username%1").arg(i), "server", "server_details"), + QString("username%1").arg(previous), "server", "server_details"); + setValue(getValue(QString("savePassword%1").arg(i), "server", "server_details"), + QString("savePassword%1").arg(previous), "server", "server_details"); + setValue(getValue(QString("password%1").arg(i), "server", "server_details"), + QString("password%1").arg(previous), "server", "server_details"); + setValue(getValue(QString("saveName%1").arg(i), "server", "server_details"), + QString("saveName%1").arg(previous), "server", "server_details"); + setValue(getValue(QString("site%1").arg(i), "server", "server_details"), QString("site%1").arg(previous), + "server", "server_details"); } } + + // if we have deleted an entry, adjust the total + if (found) { + setValue(size - 1, "totalServers", "server", "server_details"); + + // delete last value + deleteValue(QString("server%1").arg(size), "server", "server_details"); + deleteValue(QString("port%1").arg(size), "server", "server_details"); + deleteValue(QString("username%1").arg(size), "server", "server_details"); + deleteValue(QString("savePassword%1").arg(size), "server", "server_details"); + deleteValue(QString("password%1").arg(size), "server", "server_details"); + deleteValue(QString("saveName%1").arg(size), "server", "server_details"); + deleteValue(QString("site%1").arg(size), "server", "server_details"); + } } /** @@ -330,23 +265,23 @@ void ServersSettings::removeServer(QString servAddr) */ bool ServersSettings::updateExistingServerWithoutLoss(QString saveName, QString serv, QString port, QString site) { - QList servers = loadServers(); - for (ServerEntry &server : servers) { - if (server.host != serv) { - continue; - } + int size = getValue("totalServers", "server", "server_details").toInt(); - if (!port.isEmpty()) { - server.port = port; + for (int i = 0; i <= size; ++i) { + if (serv == getValue(QString("server%1").arg(i), "server", "server_details").toString()) { + if (!port.isEmpty()) { + setValue(port, QString("port%1").arg(i), "server", "server_details"); + } + + if (!site.isEmpty()) { + setValue(site, QString("site%1").arg(i), "server", "server_details"); + } + + setValue(saveName, QString("saveName%1").arg(i), "server", "server_details"); + + return true; } - if (!site.isEmpty()) { - server.site = site; - } - server.saveName = saveName; - storeServers(servers); - return true; } - return false; } @@ -358,13 +293,17 @@ bool ServersSettings::updateExistingServer(QString saveName, bool savePassword, QString site) { - const int index = findServerIndex(serv, port); - if (index < 0) { - return false; - } + int size = getValue("totalServers", "server", "server_details").toInt(); - updateServerFields(index, saveName, username, password, savePassword, site); - return true; + for (int i = 0; i <= size; ++i) { + if (serv == getValue(QString("server%1").arg(i), "server", "server_details").toString()) { + setValue(port, QString("port%1").arg(i), "server", "server_details"); + updateServerFields(i, saveName, username, password, savePassword, site); + + return true; + } + } + return false; } void ServersSettings::updateServerFields(int index, @@ -374,33 +313,33 @@ void ServersSettings::updateServerFields(int index, bool savePassword, const QString &site) { - QList servers = loadServers(); - if (index < 0 || index >= servers.size()) { - return; + if (!username.isEmpty()) { + setValue(username, QString("username%1").arg(index), "server", "server_details"); } - ServerEntry &server = servers[index]; - if (!username.isEmpty()) { - server.username = username; - } if (savePassword && !password.isEmpty()) { - server.password = password; + setValue(password, QString("password%1").arg(index), "server", "server_details"); } else { - server.password.clear(); + setValue(QString(), QString("password%1").arg(index), "server", "server_details"); } + if (!site.isEmpty()) { - server.site = site; + setValue(site, QString("site%1").arg(index), "server", "server_details"); } - server.savePassword = savePassword; - server.saveName = saveName; - storeServers(servers); + + setValue(savePassword, QString("savePassword%1").arg(index), "server", "server_details"); + setValue(saveName, QString("saveName%1").arg(index), "server", "server_details"); } int ServersSettings::findServerIndex(const QString &host, const QString &port) const { - const QList servers = loadServers(); - for (int i = 0; i < servers.size(); ++i) { - if (servers.at(i).host == host && servers.at(i).port == port) { + int size = getValue("totalServers", "server", "server_details").toInt(); + + for (int i = 0; i <= size; ++i) { + QString storedHost = getValue(QString("server%1").arg(i), "server", "server_details").toString(); + QString storedPort = getValue(QString("port%1").arg(i), "server", "server_details").toString(); + + if (storedHost == host && storedPort == port) { return i; } } @@ -410,9 +349,12 @@ int ServersSettings::findServerIndex(const QString &host, const QString &port) c int ServersSettings::findHostIndex(const QString &host) const { - const QList servers = loadServers(); - for (int i = 0; i < servers.size(); ++i) { - if (servers.at(i).host.compare(host, Qt::CaseInsensitive) == 0) { + int size = getValue("totalServers", "server", "server_details").toInt(); + + for (int i = 0; i <= size; ++i) { + QString storedHost = getValue(QString("server%1").arg(i), "server", "server_details").toString(); + + if (storedHost.compare(host, Qt::CaseInsensitive) == 0) { return i; } } @@ -422,100 +364,29 @@ int ServersSettings::findHostIndex(const QString &host) const bool ServersSettings::hasUsername(const QString &host, const QString &port) const { - const auto server = getServer(findServerIndex(host, port)); - return server && !server->username.isEmpty(); + int index = findServerIndex(host, port); + if (index < 0) { + return false; + } + + QString user = getValue(QString("username%1").arg(index), "server", "server_details").toString(); + return !user.isEmpty(); } bool ServersSettings::hasCredentials(const QString &host, const QString &port) const { - const auto server = getServer(findServerIndex(host, port)); - return server && server->savePassword && !server->password.isEmpty(); + int index = findServerIndex(host, port); + if (index < 0) { + return false; + } + + bool save = getValue(QString("savePassword%1").arg(index), "server", "server_details").toBool(); + QString password = getValue(QString("password%1").arg(index), "server", "server_details").toString(); + + return save && !password.isEmpty(); } bool ServersSettings::hasLoginData(const QString &host, const QString &port) const { return hasUsername(host, port) && hasCredentials(host, port); } - -QList ServersSettings::loadServers() const -{ - QSettings settings = getSettings(); - settings.beginGroup(SERVER_GROUP); - const QList servers = readServerArray(settings); - settings.endGroup(); - - return servers; -} - -void ServersSettings::storeServers(const QList &servers) -{ - batchWrite([&servers](QSettings &settings) { - settings.beginGroup(SERVER_GROUP); - writeServerArray(settings, servers); - settings.endGroup(); - }); -} - -void ServersSettings::migrateLegacyServers() -{ - { - QSettings settings = getSettings(); - settings.beginGroup(SERVER_GROUP); - // Only builds that predate the array format write this marker. Once it - // is gone there is nothing left to import and no reason to touch the - // file (a sync on every start is a needless open-and-write). - const bool hasLegacy = settings.contains(QStringLiteral("%1/totalServers").arg(SERVER_DETAILS_GROUP)); - settings.endGroup(); - - if (!hasLegacy) { - return; - } - } - - const QSettings::Status status = batchWrite([](QSettings &settings) { - settings.beginGroup(SERVER_GROUP); - - const int lastLegacyIndex = - settings.value(QStringLiteral("%1/totalServers").arg(SERVER_DETAILS_GROUP), -1).toInt(); - - QList servers = readServerArray(settings); - - // An older build can write the legacy keys again after the array - // migration, so the array is not automatically complete: append every - // legacy profile the array does not already hold instead of dropping it. - int imported = 0; - const QList legacyServers = readLegacyServers(settings, lastLegacyIndex); - for (const ServersSettings::ServerEntry &entry : legacyServers) { - const bool known = - std::any_of(servers.cbegin(), servers.cend(), [&entry](const ServersSettings::ServerEntry &existing) { - return existing.host == entry.host && existing.port == entry.port; - }); - if (!known) { - servers.append(entry); - ++imported; - } - } - - writeServerArray(settings, servers); - if (imported > 0) { - qCDebug(ServersSettingsLog) << "Migrated" << imported << "server profiles to the array format"; - } - - // The array (freshly written or already present) is authoritative now. - for (int i = 0; i <= lastLegacyIndex; ++i) { - settings.remove(legacyKey("saveName", i)); - settings.remove(legacyKey("server", i)); - settings.remove(legacyKey("port", i)); - settings.remove(legacyKey("username", i)); - settings.remove(legacyKey("password", i)); - settings.remove(legacyKey("savePassword", i)); - settings.remove(legacyKey("site", i)); - } - settings.remove(QStringLiteral("%1/totalServers").arg(SERVER_DETAILS_GROUP)); - settings.endGroup(); - }); - - if (status != QSettings::NoError) { - qCWarning(ServersSettingsLog) << "Failed to migrate server profiles:" << status; - } -} diff --git a/libcockatrice_settings/libcockatrice/settings/servers_settings.h b/libcockatrice_settings/libcockatrice/settings/servers_settings.h index b403b1cea..38bb02876 100644 --- a/libcockatrice_settings/libcockatrice/settings/servers_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/servers_settings.h @@ -9,11 +9,9 @@ #include "settings_manager.h" -#include #include #include #include -#include #include class QObject; @@ -29,22 +27,6 @@ class ServersSettings : public SettingsManager friend class SettingsCache; public: - struct ServerEntry - { - QString saveName; - QString host; - QString port; - QString username; - QString password; - bool savePassword = false; - QString site; - }; - - explicit ServersSettings(const QString &settingPath, QObject *parent = nullptr); - - QList getServers() const; - std::optional getServer(int index) const; - int getPreviousHostLogin() const; int getPrevioushostindex(const QString &) const; QStringList getPreviousHostList() const; @@ -106,11 +88,8 @@ public: bool getClearDebugLogStatus(bool abDefaultValue) const; private: + explicit ServersSettings(const QString &settingPath, QObject *parent = nullptr); ServersSettings(const ServersSettings & /*other*/); - - QList loadServers() const; - void storeServers(const QList &servers); - void migrateLegacyServers(); }; #endif // SERVERSSETTINGS_H diff --git a/libcockatrice_settings/libcockatrice/settings/settings_manager.cpp b/libcockatrice_settings/libcockatrice/settings/settings_manager.cpp index 7cc1fd7be..f98778ca0 100644 --- a/libcockatrice_settings/libcockatrice/settings/settings_manager.cpp +++ b/libcockatrice_settings/libcockatrice/settings/settings_manager.cpp @@ -193,12 +193,13 @@ QVariant SettingsManager::getValue(const QString &name, return value; } -QSettings::Status SettingsManager::batchWrite(std::function batchWriteFunction) +void SettingsManager::batchWrite(std::function batchWriteFunction) { auto settings = getSettings(); + settings.setAtomicSyncRequired(false); batchWriteFunction(settings); settings.sync(); // single flush - return settings.status(); + settings.setAtomicSyncRequired(true); } /** diff --git a/libcockatrice_settings/libcockatrice/settings/settings_manager.h b/libcockatrice_settings/libcockatrice/settings/settings_manager.h index 760d095e6..8d3110106 100644 --- a/libcockatrice_settings/libcockatrice/settings/settings_manager.h +++ b/libcockatrice_settings/libcockatrice/settings/settings_manager.h @@ -28,9 +28,7 @@ public: QVariant getValue(const QString &name, const QString &group, const QString &subGroup = QString()) const; QVariant getValue(const QString &name, const QString &group, const QString &subGroup, const QVariant &defaultValue) const; - //! \brief Runs \a batchWriteFunction on a temporary QSettings and syncs once. - //! \return the status of the underlying file after the final sync. - QSettings::Status batchWrite(std::function batchWriteFunction); + void batchWrite(std::function batchWriteFunction); void sync(); diff --git a/libcockatrice_utility/CMakeLists.txt b/libcockatrice_utility/CMakeLists.txt index 8d2cb5b92..55f1daf24 100644 --- a/libcockatrice_utility/CMakeLists.txt +++ b/libcockatrice_utility/CMakeLists.txt @@ -1,6 +1,10 @@ cmake_minimum_required(VERSION 3.16) project(Utility VERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}") +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + set(UTILITY_SOURCES libcockatrice/utility/cryptoutil.cpp libcockatrice/utility/expression.cpp diff --git a/libcockatrice_utility/libcockatrice/utility/passwordhasher.cpp b/libcockatrice_utility/libcockatrice/utility/passwordhasher.cpp index 8e876d5bc..a7106aadc 100644 --- a/libcockatrice_utility/libcockatrice/utility/passwordhasher.cpp +++ b/libcockatrice_utility/libcockatrice/utility/passwordhasher.cpp @@ -62,11 +62,9 @@ QByteArray PasswordHasher::deriveKey(const QString &password, const QByteArray & { QByteArray key(SCRYPT_VERIFIER_LENGTH, '\0'); const QByteArray passwordUtf8 = password.toUtf8(); - // EVP_PBE_scrypt aborts unless maxmem covers the required working memory: V is 128 * n * - // r bytes, B is 128 * r * p, and XY is 256 * r. Under-budgeting XY makes OpenSSL fail - // for r >= 17, which costParamsAreSane still accepts. - const auto maxmem = - static_cast(128) * n * r + static_cast(128) * r * p + static_cast(256) * r + 4096; + // EVP_PBE_scrypt aborts unless maxmem covers the required working memory, + // which is roughly 128 * n * r bytes (plus the small Salsa20/8 block array). + const auto maxmem = static_cast(128) * n * r + static_cast(128) * r * p + 4096; if (EVP_PBE_scrypt(passwordUtf8.constData(), passwordUtf8.size(), reinterpret_cast(salt.constData()), salt.size(), n, r, p, maxmem, reinterpret_cast(key.data()), key.size()) != 1) { diff --git a/servatrice/servatrice.ini.example b/servatrice/servatrice.ini.example index ac291fd7f..021f5157f 100644 --- a/servatrice/servatrice.ini.example +++ b/servatrice/servatrice.ini.example @@ -455,11 +455,9 @@ authentication_strictness=mixed ; the scrypt verifier, so old clients can still log in during the window below without ; the database holding a fast-crackable hash in the clear. ; Generate with: openssl rand -hex 32 -; If unset there is no downgrade path at all: a migrated account keeps only a scrypt -; verifier, and an administrator's password reset stores a verifier too, so pre-challenge -; clients cannot log in with the temporary password either. Set the key if you still serve -; them. Rotating the key invalidates existing backups (they become unusable immediately; -; expired ones are still purged). +; If unset, migrated accounts have no downgrade window: old clients must use a password +; reset to get back in. Rotating the key invalidates existing backups (they become +; unusable immediately; expired ones are still purged). legacy_backup_key= ; Days a legacy password backup may go unused before it is purged from the database. diff --git a/servatrice/src/isl_interface.cpp b/servatrice/src/isl_interface.cpp index 1898cd5fa..8635808ac 100644 --- a/servatrice/src/isl_interface.cpp +++ b/servatrice/src/isl_interface.cpp @@ -58,9 +58,9 @@ void IslInterface::sharedCtor(const QSslCertificate &cert, const QSslKey &privat socket->setLocalCertificate(cert); socket->setPrivateKey(privateKey); - connect(socket, &QSslSocket::readyRead, this, &IslInterface::readClient, Qt::QueuedConnection); + connect(socket, SIGNAL(readyRead()), this, SLOT(readClient()), Qt::QueuedConnection); connect(socket, &QAbstractSocket::errorOccurred, this, &IslInterface::catchSocketError); - connect(this, &IslInterface::outputBufferChanged, this, &IslInterface::flushOutputBuffer, Qt::QueuedConnection); + connect(this, SIGNAL(outputBufferChanged()), this, SLOT(flushOutputBuffer()), Qt::QueuedConnection); } IslInterface::IslInterface(int _socketDescriptor, diff --git a/servatrice/src/servatrice.cpp b/servatrice/src/servatrice.cpp index afe028f32..49d8e1e4c 100644 --- a/servatrice/src/servatrice.cpp +++ b/servatrice/src/servatrice.cpp @@ -100,10 +100,8 @@ Servatrice_GameServer::Servatrice_GameServer(Servatrice *_server, newThread->start(); server->watchWorkerThread(newThread); - QMetaObject::invokeMethod( - newDatabaseInterface, - [newDatabaseInterface, _sqlDatabase] { newDatabaseInterface->initDatabase(_sqlDatabase); }, - Qt::BlockingQueuedConnection); + QMetaObject::invokeMethod(newDatabaseInterface, "initDatabase", Qt::BlockingQueuedConnection, + Q_ARG(QSqlDatabase, _sqlDatabase)); connectionPools.append(newPool); } @@ -127,10 +125,9 @@ void Servatrice_GameServer::incomingConnection(qintptr socketDescriptor) auto ssi = new TcpServerSocketInterface(server, pool->getDatabaseInterface()); ssi->moveToThread(pool->thread()); pool->addClient(); - connect(ssi, &QObject::destroyed, pool, &Servatrice_ConnectionPool::removeClient); + connect(ssi, SIGNAL(destroyed()), pool, SLOT(removeClient())); - QMetaObject::invokeMethod( - ssi, [ssi, socketDescriptor] { ssi->initConnection(socketDescriptor); }, Qt::QueuedConnection); + QMetaObject::invokeMethod(ssi, "initConnection", Qt::QueuedConnection, Q_ARG(int, socketDescriptor)); } Servatrice_ConnectionPool *Servatrice_GameServer::findLeastUsedConnectionPool() @@ -171,15 +168,13 @@ Servatrice_WebsocketGameServer::Servatrice_WebsocketGameServer(Servatrice *_serv newThread->start(); server->watchWorkerThread(newThread); - QMetaObject::invokeMethod( - newDatabaseInterface, - [newDatabaseInterface, _sqlDatabase] { newDatabaseInterface->initDatabase(_sqlDatabase); }, - Qt::BlockingQueuedConnection); + QMetaObject::invokeMethod(newDatabaseInterface, "initDatabase", Qt::BlockingQueuedConnection, + Q_ARG(QSqlDatabase, _sqlDatabase)); connectionPools.append(newPool); - } - connect(this, &QWebSocketServer::newConnection, this, &Servatrice_WebsocketGameServer::onNewConnection); + connect(this, SIGNAL(newConnection()), this, SLOT(onNewConnection())); + } } Servatrice_WebsocketGameServer::~Servatrice_WebsocketGameServer() @@ -204,10 +199,9 @@ void Servatrice_WebsocketGameServer::onNewConnection() */ // ssi->moveToThread(pool->thread()); pool->addClient(); - connect(ssi, &QObject::destroyed, pool, &Servatrice_ConnectionPool::removeClient); + connect(ssi, SIGNAL(destroyed()), pool, SLOT(removeClient())); - void *socket = nextPendingConnection(); - QMetaObject::invokeMethod(ssi, [ssi, socket] { ssi->initConnection(socket); }, Qt::QueuedConnection); + QMetaObject::invokeMethod(ssi, "initConnection", Qt::QueuedConnection, Q_ARG(void *, nextPendingConnection())); } Servatrice_ConnectionPool *Servatrice_WebsocketGameServer::findLeastUsedConnectionPool() @@ -230,14 +224,14 @@ Servatrice_ConnectionPool *Servatrice_WebsocketGameServer::findLeastUsedConnecti void Servatrice_IslServer::incomingConnection(qintptr socketDescriptor) { auto thread = new QThread; - connect(thread, &QThread::finished, thread, &QObject::deleteLater); + connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater())); auto interface = new IslInterface(static_cast(socketDescriptor), cert, privateKey, server); interface->moveToThread(thread); - connect(interface, &QObject::destroyed, thread, &QThread::quit); + connect(interface, SIGNAL(destroyed()), thread, SLOT(quit())); thread->start(); - QMetaObject::invokeMethod(interface, [interface] { interface->initServer(); }, Qt::QueuedConnection); + QMetaObject::invokeMethod(interface, "initServer", Qt::QueuedConnection); } Servatrice::Servatrice(QObject *parent) @@ -591,16 +585,15 @@ bool Servatrice::initServer() auto *thread = new QThread; thread->setObjectName("isl_" + QString::number(prop.id)); - connect(thread, &QThread::finished, thread, &QObject::deleteLater); + connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater())); IslInterface *interface = new IslInterface(prop.id, prop.hostname, prop.address.toString(), prop.controlPort, prop.cert, cert, key, this); interface->moveToThread(thread); - connect(interface, &QObject::destroyed, thread, &QThread::quit); + connect(interface, SIGNAL(destroyed()), thread, SLOT(quit())); thread->start(); - QMetaObject::invokeMethod( - interface, [interface] { interface->initClient(); }, Qt::BlockingQueuedConnection); + QMetaObject::invokeMethod(interface, "initClient", Qt::BlockingQueuedConnection); } qDebug() << "Starting ISL server on port" << getISLNetworkPort(); @@ -617,18 +610,18 @@ bool Servatrice::initServer() } pingClock = new QTimer(this); - connect(pingClock, &QTimer::timeout, this, &Servatrice::pingClockTimeout); + connect(pingClock, SIGNAL(timeout()), this, SIGNAL(pingClockTimeout())); pingClock->start(getClientKeepAlive() * 1000); statusUpdateClock = new QTimer(this); - connect(statusUpdateClock, &QTimer::timeout, this, &Servatrice::statusUpdate); + connect(statusUpdateClock, SIGNAL(timeout()), this, SLOT(statusUpdate())); if (getServerStatusUpdateTime() != 0) { qDebug() << "Starting status update clock, interval" << getServerStatusUpdateTime() << "ms"; statusUpdateClock->start(getServerStatusUpdateTime()); } deckShareCleanupClock = new QTimer(this); - connect(deckShareCleanupClock, &QTimer::timeout, this, &Servatrice::cleanupExpiredDeckShares); + connect(deckShareCleanupClock, SIGNAL(timeout()), this, SLOT(cleanupExpiredDeckShares())); const int deckShareCleanupInterval = getDeckShareCleanupInterval(); if (deckShareCleanupInterval > 0) { qDebug() << "Starting deck share cleanup clock, interval" << deckShareCleanupInterval << "ms"; @@ -1045,7 +1038,7 @@ void Servatrice::scheduleShutdown(const QString &reason, int minutes) nextShutdownMessageMinutes = shutdownMinutes; if (minutes > 0) { shutdownTimer = new QTimer; - connect(shutdownTimer, &QTimer::timeout, this, &Servatrice::shutdownTimeout); + connect(shutdownTimer, SIGNAL(timeout()), this, SLOT(shutdownTimeout())); shutdownTimer->start(60000); } shutdownStateMutex.unlock(); @@ -1108,19 +1101,25 @@ void Servatrice::addIslInterface(int _serverId, IslInterface *interface) { // Only call with islLock locked for writing islInterfaces.insert(_serverId, interface); - connect(interface, &IslInterface::externalUserJoined, this, &Servatrice::externalUserJoined); - connect(interface, &IslInterface::externalUserLeft, this, &Servatrice::externalUserLeft); - connect(interface, &IslInterface::externalRoomUserJoined, this, &Servatrice::externalRoomUserJoined); - connect(interface, &IslInterface::externalRoomUserLeft, this, &Servatrice::externalRoomUserLeft); - connect(interface, &IslInterface::externalRoomSay, this, &Servatrice::externalRoomSay); - connect(interface, &IslInterface::externalRoomRemoveMessages, this, &Servatrice::externalRoomRemoveMessages); - connect(interface, &IslInterface::externalRoomGameListChanged, this, &Servatrice::externalRoomGameListChanged); - connect(interface, &IslInterface::joinGameCommandReceived, this, &Servatrice::externalJoinGameCommandReceived); - connect(interface, &IslInterface::gameCommandContainerReceived, this, - &Servatrice::externalGameCommandContainerReceived); - connect(interface, &IslInterface::responseReceived, this, &Servatrice::externalResponseReceived); - connect(interface, &IslInterface::gameEventContainerReceived, this, - &Servatrice::externalGameEventContainerReceived); + connect(interface, SIGNAL(externalUserJoined(ServerInfo_User)), this, SLOT(externalUserJoined(ServerInfo_User))); + connect(interface, SIGNAL(externalUserLeft(QString)), this, SLOT(externalUserLeft(QString))); + connect(interface, SIGNAL(externalRoomUserJoined(int, ServerInfo_User)), this, + SLOT(externalRoomUserJoined(int, ServerInfo_User))); + connect(interface, SIGNAL(externalRoomUserLeft(int, QString)), this, SLOT(externalRoomUserLeft(int, QString))); + connect(interface, SIGNAL(externalRoomSay(int, QString, QString, QString)), this, + SLOT(externalRoomSay(int, QString, QString, QString))); + connect(interface, SIGNAL(externalRoomRemoveMessages(int, QString, int)), this, + SLOT(externalRoomRemoveMessages(int, QString, int))); + connect(interface, SIGNAL(externalRoomGameListChanged(int, ServerInfo_Game)), this, + SLOT(externalRoomGameListChanged(int, ServerInfo_Game))); + connect(interface, SIGNAL(joinGameCommandReceived(Command_JoinGame, int, int, int, qint64)), this, + SLOT(externalJoinGameCommandReceived(Command_JoinGame, int, int, int, qint64))); + connect(interface, SIGNAL(gameCommandContainerReceived(CommandContainer, int, int, qint64)), this, + SLOT(externalGameCommandContainerReceived(CommandContainer, int, int, qint64))); + connect(interface, SIGNAL(responseReceived(Response, qint64)), this, + SLOT(externalResponseReceived(Response, qint64))); + connect(interface, SIGNAL(gameEventContainerReceived(GameEventContainer, qint64)), this, + SLOT(externalGameEventContainerReceived(GameEventContainer, qint64))); } void Servatrice::removeIslInterface(int _serverId) diff --git a/servatrice/src/servatrice_database_interface.cpp b/servatrice/src/servatrice_database_interface.cpp index a4f184e38..b5c45065c 100644 --- a/servatrice/src/servatrice_database_interface.cpp +++ b/servatrice/src/servatrice_database_interface.cpp @@ -33,7 +33,6 @@ #include #include #include -#include #include #include #include @@ -483,10 +482,6 @@ AuthenticationResult Servatrice_DatabaseInterface::checkUserPassword(Server_Prot const QByteArray expected = PasswordHasher::computeResponse(key, nonce); handler->clearAuthNonce(); if (PasswordHasher::constantTimeEquals(expected, response)) { - if (forceChange) { - qCDebug(DatabaseInterfaceLog) << "Login accepted but password change required"; - return PasswordChangeRequired; - } qCDebug(DatabaseInterfaceLog) << "Login accepted: challenge-response password right"; return PasswordRight; } @@ -760,20 +755,6 @@ QString Servatrice_DatabaseInterface::getUserPasswordData(const QString &user) return query->value(0).toString(); } -QString Servatrice_DatabaseInterface::sealLegacyBackup(const QString &storedName, const QString &credential) -{ - const QByteArray key = server->getLegacyBackupKey(); - if (key.isEmpty()) { - return {}; - } - const QByteArray aad = storedName.simplified().toUtf8(); - const QString sealed = CryptoUtil::encryptSecret(credential.toUtf8(), key, aad); - if (sealed.isEmpty() || sealed.length() > MAX_LEGACY_BACKUP_LENGTH) { - return {}; - } - return sealed; -} - bool Servatrice_DatabaseInterface::submitPasswordVerifier(const QString &user, const QString &passwordVerifier) { if (server->getAuthenticationMethod() != Servatrice::AuthenticationSql) { @@ -798,13 +779,18 @@ bool Servatrice_DatabaseInterface::submitPasswordVerifier(const QString &user, c QString sealedBackup; QDateTime backupUsedAt; - if (PasswordHasher::isLegacyFormat(currentCredential) && !server->getLegacyBackupKey().isEmpty()) { - sealedBackup = sealLegacyBackup(storedName, currentCredential); - if (sealedBackup.isEmpty()) { - qCWarning(DatabaseInterfaceLog) - << "Failed to seal legacy backup for user" << user << "- account migrates without a downgrade path"; - } else { - backupUsedAt = QDateTime::currentDateTimeUtc(); + if (PasswordHasher::isLegacyFormat(currentCredential)) { + const QByteArray key = server->getLegacyBackupKey(); + if (!key.isEmpty()) { + const QByteArray aad = storedName.simplified().toUtf8(); + sealedBackup = CryptoUtil::encryptSecret(currentCredential.toUtf8(), key, aad); + if (sealedBackup.isEmpty() || sealedBackup.length() > MAX_LEGACY_BACKUP_LENGTH) { + qCWarning(DatabaseInterfaceLog) + << "Failed to seal legacy backup for user" << user << "- account migrates without a downgrade path"; + sealedBackup.clear(); + } else { + backupUsedAt = QDateTime::currentDateTimeUtc(); + } } } @@ -1658,54 +1644,6 @@ bool Servatrice_DatabaseInterface::changeUserPassword(const QString &user, return passwordQuery->numRowsAffected() > 0; } -bool Servatrice_DatabaseInterface::changeUserPasswordWithLegacyBackup(const QString &user, - const QString &passwordVerifier, - const QString &legacyCredential) -{ - if (server->getAuthenticationMethod() != Servatrice::AuthenticationSql) { - return false; - } - - if (!checkSql()) { - return false; - } - - // Seal against the stored row name, not the caller-supplied one: the name column is - // case-insensitive, so a differently-cased name would fail the tag check on open. - QSqlQuery *nameQuery = prepareQuery("SELECT name FROM {prefix}_users WHERE name = :name"); - nameQuery->bindValue(":name", user); - if (!execSqlQuery(nameQuery) || !nameQuery->next()) { - return false; - } - const QString storedName = nameQuery->value(0).toString(); - - const QString sealedBackup = sealLegacyBackup(storedName, legacyCredential); - const QDateTime backupUsedAt = sealedBackup.isEmpty() ? QDateTime() : QDateTime::currentDateTimeUtc(); - if (sealedBackup.isEmpty()) { - // Without a backup a pre-challenge client has nothing to authenticate against: the - // row holds a verifier and the legacy hash it would send cannot match it. Say so, - // because the symptom - the temporary password appearing not to work - shows up on - // a different machine from the one that reset it. - qCWarning(DatabaseInterfaceLog) << "Stored a verifier without a legacy backup for user" << user - << "- pre-challenge clients cannot use this password" - << "- check security/legacy_backup_key"; - } - - QSqlQuery *query = prepareQuery("update {prefix}_users set password_sha512=:password, " - "password_legacy_backup=:backup, password_legacy_backup_used_at=:used_at, " - "passwordLastChangedDate = NOW() where name = :name"); - query->bindValue(":password", passwordVerifier); - query->bindValue(":backup", sealedBackup); - query->bindValue(":used_at", backupUsedAt.isValid() - ? QVariant(backupUsedAt.toString(QStringLiteral("yyyy-MM-dd HH:mm:ss"))) - : QVariant()); - query->bindValue(":name", storedName); - if (!execSqlQuery(query)) { - return false; - } - return query->numRowsAffected() > 0; -} - void Servatrice_DatabaseInterface::setForcePasswordChange(const QString &user, bool force) { if (!checkSql()) { @@ -2250,14 +2188,8 @@ QList Servatrice_DatabaseInterface::getMessageLogHistory bool &game, bool &room, int &range, - int &maxresults, - int offset, - const QString &beforeTime, - bool *hasMore) + int &maxresults) { - if (hasMore != nullptr) { - *hasMore = false; - } QList results; ServerInfo_ChatMessage chatMessage; @@ -2266,31 +2198,8 @@ QList Servatrice_DatabaseInterface::getMessageLogHistory return results; } - // Page size guard: always bound the scan so a filter-less browse of room - // history cannot become a 50M-row fetch. Zero means "client did not set a - // size", so fall back to one smooth-scroll page. - const int pageSize = maxresults <= 0 ? 200 : qBound(1, maxresults, 1000); - maxresults = pageSize; - const int safeOffset = qMax(0, offset); - - const bool hasIndexedFilter = !user.isEmpty() || !ipaddress.isEmpty() || !gameid.isEmpty() || !gamename.isEmpty(); - const bool hasLocationFilter = chat || game || room; - if (!hasLocationFilter) { - // No target_type restriction would mix private chat into a room/game - // browse, so refuse rather than leak. - return results; - } - // A browse narrowed only by a message substring has no index to serve it, and `range` - // has no upper bound on the wire: the client offers 20 days, but a hand-built command - // can ask for any value and every LIKE candidate in it is a row the server examines. - // Clamping here keeps the guard independent of what the client happens to send. - constexpr int maxFilterlessRangeHours = 20 * 24; - if (!hasIndexedFilter && range > maxFilterlessRangeHours) { - range = maxFilterlessRangeHours; - } - if (!hasIndexedFilter && range <= 0) { - // Filter-less browse must stay time-bounded (idx_log_time) so the - // query stays an index range scan instead of a full table scan. + if (user.isEmpty() && ipaddress.isEmpty() && gameid.isEmpty() && gamename.isEmpty()) { + // To ensure quick results and minimal lag, require an indexed field return results; } @@ -2345,23 +2254,10 @@ QList Servatrice_DatabaseInterface::getMessageLogHistory queryString.append(" AND log_time >= DATE_SUB(now(), INTERVAL :range_time HOUR)"); } - if (!beforeTime.isEmpty()) { - // Pages after the first run against the snapshot fixed by the first one, so rows - // logged while the moderator scrolls can no longer push the OFFSET window down and - // repeat a row already on screen. - queryString.append(" AND log_time <= :before_time"); + if (maxresults) { + queryString.append(" LIMIT :limit_size"); } - // Newest-first keeps "going back from today" stable across pages and lets - // the client append older rows at the bottom for smooth infinite scroll. - queryString.append(" ORDER BY log_time DESC"); - - // Fetch one extra row when the caller wants a has_more cursor so a full - // page does not need a second empty round-trip to prove exhaustion. - const bool detectMore = (hasMore != nullptr); - const int fetchSize = detectMore ? pageSize + 1 : pageSize; - queryString.append(" LIMIT :limit_size OFFSET :offset_size"); - QSqlQuery *query = prepareQuery(queryString); if (!user.isEmpty()) { query->bindValue(":user_name", user); @@ -2381,11 +2277,9 @@ QList Servatrice_DatabaseInterface::getMessageLogHistory if (range) { query->bindValue(":range_time", range); } - if (!beforeTime.isEmpty()) { - query->bindValue(":before_time", beforeTime); + if (maxresults) { + query->bindValue(":limit_size", maxresults); } - query->bindValue(":limit_size", fetchSize); - query->bindValue(":offset_size", safeOffset); if (!execSqlQuery(query)) { qCWarning(DatabaseInterfaceLog) << "Failed to collect log history information: SQL Error"; @@ -2405,11 +2299,6 @@ QList Servatrice_DatabaseInterface::getMessageLogHistory results << chatMessage; } - if (detectMore && results.size() > pageSize) { - results.removeLast(); - *hasMore = true; - } - return results; } diff --git a/servatrice/src/servatrice_database_interface.h b/servatrice/src/servatrice_database_interface.h index eb74063ac..6b38384e4 100644 --- a/servatrice/src/servatrice_database_interface.h +++ b/servatrice/src/servatrice_database_interface.h @@ -62,9 +62,6 @@ private: bool checkUserIsIpBanned(const QString &ipAddress, QString &banReason, int &banSecondsRemaining); /** Must be called after checkSql and server is known to be in auth mode. */ bool checkUserIsNameBanned(QString const &userName, QString &banReason, int &banSecondsRemaining); - /** Seal a legacy credential for the account's downgrade backup; empty when the - * backup key is unset or the sealed value does not fit the column. */ - QString sealLegacyBackup(const QString &storedName, const QString &credential); bool isStrictModeEnabled(bool &ok) const; protected: @@ -195,11 +192,6 @@ public: const QString &targetName, const QString &channel = QString()) override; bool changeUserPassword(const QString &user, const QString &password, bool passwordNeedsHash) override; - /** Replace a credential with an already-derived verifier, keeping a sealed legacy hash of the - * same password as the downgrade backup for pre-challenge clients. */ - bool changeUserPasswordWithLegacyBackup(const QString &user, - const QString &passwordVerifier, - const QString &legacyCredential); bool changeUserPassword(const QString &user, const QString &oldPassword, bool oldPasswordNeedsHash, @@ -219,10 +211,7 @@ public: bool &game, bool &room, int &range, - int &maxresults, - int offset = 0, - const QString &beforeTime = QString(), - bool *hasMore = nullptr); + int &maxresults); QList getUserSessions(const QString &userName, int limit); QList getUserAlts(const QString &userName); QList getModeratorLastLogins(); diff --git a/servatrice/src/serversocketinterface.cpp b/servatrice/src/serversocketinterface.cpp index c38d790b1..78593f6dc 100644 --- a/servatrice/src/serversocketinterface.cpp +++ b/servatrice/src/serversocketinterface.cpp @@ -199,8 +199,7 @@ AbstractServerSocketInterface::AbstractServerSocketInterface(Servatrice *_server // Never call flushOutputQueue directly from outputQueueChanged. In case of a socket error, // it could lead to this object being destroyed while another function is still on the call stack. -> mutex // deadlocks etc. - connect(this, &AbstractServerSocketInterface::outputQueueChanged, this, - &AbstractServerSocketInterface::flushOutputQueue, Qt::QueuedConnection); + connect(this, SIGNAL(outputQueueChanged()), this, SLOT(flushOutputQueue()), Qt::QueuedConnection); } bool AbstractServerSocketInterface::initSession() @@ -1743,17 +1742,12 @@ Response::ResponseCode AbstractServerSocketInterface::cmdGetLogHistory(const Com int dateRange = cmd.date_range(); int maximumResults = cmd.maximum_results(); - const int offset = cmd.has_offset() ? static_cast(cmd.offset()) : 0; - const QString beforeTime = cmd.has_before_time() ? QString::fromStdString(cmd.before_time()) : QString(); Response_ViewLogHistory *re = new Response_ViewLogHistory; if (servatrice->getEnableLogQuery()) { - bool hasMore = false; - QList history = - sqlInterface->getMessageLogHistory(userName, ipAddress, gameName, gameID, message, chatType, gameType, - roomType, dateRange, maximumResults, offset, beforeTime, &hasMore); - QListIterator messageIterator(history); + QListIterator messageIterator(sqlInterface->getMessageLogHistory( + userName, ipAddress, gameName, gameID, message, chatType, gameType, roomType, dateRange, maximumResults)); while (messageIterator.hasNext()) { ServerInfo_ChatMessage chatMessage = messageIterator.next(); if (!allowPrivateChat) { @@ -1761,7 +1755,6 @@ Response::ResponseCode AbstractServerSocketInterface::cmdGetLogHistory(const Com } re->add_log_message()->CopyFrom(chatMessage); } - re->set_has_more(hasMore); } else { ServerInfo_ChatMessage chatMessage; @@ -1989,8 +1982,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdBanFromServer(const Com SessionEvent *se = userList[i]->prepareSessionEvent(event); userList[i]->sendProtocolItem(*se); delete se; - QMetaObject::invokeMethod( - userList[i], [user = userList[i]] { user->prepareDestroy(); }, Qt::QueuedConnection); + QMetaObject::invokeMethod(userList[i], "prepareDestroy", Qt::QueuedConnection); } } @@ -2649,15 +2641,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdResetUserPassword(const } const QString tempPassword = PasswordHasher::generateRandomSalt(); - // Store a scrypt verifier rather than a legacy salt+hash: a legacy row makes the next - // login look like an authentication downgrade, which modern clients refuse when they - // still hold a verifier for the account. Where security/legacy_backup_key is configured, - // keep a sealed legacy hash of the same temporary password as the downgrade backup so - // pre-challenge clients keep the path the reset used to provide; without the key they - // have none, which changeUserPasswordWithLegacyBackup warns about. - const QString verifier = PasswordHasher::generatePasswordVerifier(tempPassword); - const QString legacyCredential = PasswordHasher::computeHash(tempPassword, PasswordHasher::generateRandomSalt()); - if (!sqlInterface->changeUserPasswordWithLegacyBackup(userName, verifier, legacyCredential)) { + if (!sqlInterface->changeUserPassword(userName, tempPassword, true)) { return Response::RespInternalError; } sqlInterface->setForcePasswordChange(userName, true); @@ -3795,9 +3779,6 @@ Response::ResponseCode AbstractServerSocketInterface::cmdAccountPassword(const C } databaseInterface->setForcePasswordChange(userName, false); - // The forced change is done, but this session never completed login (see - // Server_ProtocolHandler::cmdLogin). It stays restricted to ping and another password - // change; the client reconnects with the new password to actually enter the server. return Response::RespOk; } @@ -4199,15 +4180,15 @@ Response::ResponseCode AbstractServerSocketInterface::cmdUpdateServerMessage(const Command_UpdateServerMessage & /*cmd*/, ResponseContainer & /*rc*/) { - QMetaObject::invokeMethod(servatrice, [srv = servatrice] { srv->updateLoginMessage(); }); + QMetaObject::invokeMethod(server, "updateLoginMessage"); return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdShutdownServer(const Command_ShutdownServer &cmd, ResponseContainer & /*rc*/) { - QMetaObject::invokeMethod(servatrice, [srv = servatrice, reason = textFromStdString(cmd.reason()), - minutes = cmd.minutes()] { srv->scheduleShutdown(reason, minutes); }); + QMetaObject::invokeMethod(server, "scheduleShutdown", Q_ARG(QString, textFromStdString(cmd.reason())), + Q_ARG(int, cmd.minutes())); return Response::RespOk; } @@ -4216,9 +4197,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReloadConfig(const Comm { logDebugMessage("Received admin command: reloading configuration"); settingsCache->sync(); - QMetaObject::invokeMethod(servatrice, [srv = servatrice, features = servatrice->getRequiredFeatures()] { - srv->setRequiredFeatures(features); - }); + QMetaObject::invokeMethod(server, "setRequiredFeatures", Q_ARG(QString, server->getRequiredFeatures())); return Response::RespOk; } @@ -4270,7 +4249,7 @@ bool AbstractServerSocketInterface::removeAdminFlagFromUser(const QString &userN delete se; } - QMetaObject::invokeMethod(user, [user] { user->prepareDestroy(); }, Qt::QueuedConnection); + QMetaObject::invokeMethod(user, "prepareDestroy", Qt::QueuedConnection); return true; } @@ -4445,10 +4424,10 @@ TcpServerSocketInterface::TcpServerSocketInterface(Servatrice *_server, { socket = new QTcpSocket(this); socket->setSocketOption(QAbstractSocket::LowDelayOption, 1); - connect(socket, &QTcpSocket::readyRead, this, [this] { readClient(); }); - connect(socket, &QTcpSocket::disconnected, this, [this] { catchSocketDisconnected(); }); - connect(socket, &QTcpSocket::errorOccurred, this, - [this](QAbstractSocket::SocketError error) { catchSocketError(error); }); + connect(socket, SIGNAL(readyRead()), this, SLOT(readClient())); + connect(socket, SIGNAL(disconnected()), this, SLOT(catchSocketDisconnected())); + connect(socket, SIGNAL(errorOccurred(QAbstractSocket::SocketError)), this, + SLOT(catchSocketError(QAbstractSocket::SocketError))); } TcpServerSocketInterface::~TcpServerSocketInterface() @@ -4670,7 +4649,8 @@ void WebsocketServerSocketInterface::initConnection(void *_socket) } } - connect(socket, &QWebSocket::binaryMessageReceived, this, &WebsocketServerSocketInterface::binaryMessageReceived); + connect(socket, SIGNAL(binaryMessageReceived(const QByteArray &)), this, + SLOT(binaryMessageReceived(const QByteArray &))); // QWebSocket::errorOccurred was added in Qt 6.5 (error() is deprecated from 6.5 on), // so a typed connect cannot compile against the 6.4 floor. #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) @@ -4680,7 +4660,7 @@ void WebsocketServerSocketInterface::initConnection(void *_socket) connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(catchSocketError(QAbstractSocket::SocketError))); #endif - connect(socket, &QWebSocket::disconnected, this, [this] { catchSocketDisconnected(); }); + connect(socket, SIGNAL(disconnected()), this, SLOT(catchSocketDisconnected())); // Add this object to the server's list of connections before it can receive socket events. // Otherwise, in case of a socket error, it could be removed from the list before it is added. diff --git a/servatrice/src/smtpclient.cpp b/servatrice/src/smtpclient.cpp index bfdfae905..82824e496 100644 --- a/servatrice/src/smtpclient.cpp +++ b/servatrice/src/smtpclient.cpp @@ -25,7 +25,7 @@ static const qint64 IDLE_TIMEOUT_MS = 60000; // 60 seconds // MAX_RETRY_DELAY cap is applied, preventing unbounded intermediate values. static const int MAX_BACKOFF_SHIFT = 10; -inline Q_LOGGING_CATEGORY(SmtpClientLog, "smtp_client"); +inline Q_LOGGING_CATEGORY(SmtpClientLog, "smtp_client") static QString pendingKey(const QString &userName, EmailType type, const QString &token) { @@ -55,23 +55,24 @@ SmtpClient::SmtpClient(QObject *parent) : QObject(parent) idleTimer = new QTimer(this); idleTimer->setSingleShot(true); - connect(idleTimer, &QTimer::timeout, this, &SmtpClient::idleTimeout); + connect(idleTimer, SIGNAL(timeout()), this, SLOT(idleTimeout())); - connect(smtp, &QxtSmtp::authenticated, this, &SmtpClient::authenticated); - connect(smtp, qOverload(&QxtSmtp::authenticationFailed), this, - &SmtpClient::authenticationFailed); - connect(smtp, &QxtSmtp::connected, this, &SmtpClient::connected); - connect(smtp, qOverload(&QxtSmtp::connectionFailed), this, &SmtpClient::connectionFailed); - connect(smtp, &QxtSmtp::disconnected, this, &SmtpClient::disconnected); - connect(smtp, &QxtSmtp::encrypted, this, &SmtpClient::encrypted); - connect(smtp, qOverload(&QxtSmtp::encryptionFailed), this, &SmtpClient::encryptionFailed); - connect(smtp, &QxtSmtp::finished, this, &SmtpClient::finished); - connect(smtp, qOverload(&QxtSmtp::mailFailed), this, &SmtpClient::mailFailed); - connect(smtp, &QxtSmtp::mailSent, this, &SmtpClient::mailSent); - connect(smtp, qOverload(&QxtSmtp::recipientRejected), this, - &SmtpClient::recipientRejected); - connect(smtp, qOverload(&QxtSmtp::senderRejected), this, - &SmtpClient::senderRejected); + connect(smtp, SIGNAL(authenticated()), this, SLOT(authenticated())); + connect(smtp, SIGNAL(authenticationFailed(const QByteArray &)), this, + SLOT(authenticationFailed(const QByteArray &))); + connect(smtp, SIGNAL(connected()), this, SLOT(connected())); + connect(smtp, SIGNAL(connectionFailed(const QByteArray &)), this, SLOT(connectionFailed(const QByteArray &))); + connect(smtp, SIGNAL(disconnected()), this, SLOT(disconnected())); + connect(smtp, SIGNAL(encrypted()), this, SLOT(encrypted())); + connect(smtp, SIGNAL(encryptionFailed(const QByteArray &)), this, SLOT(encryptionFailed(const QByteArray &))); + connect(smtp, SIGNAL(finished()), this, SLOT(finished())); + connect(smtp, SIGNAL(mailFailed(int, int, const QByteArray &)), this, + SLOT(mailFailed(int, int, const QByteArray &))); + connect(smtp, SIGNAL(mailSent(int)), this, SLOT(mailSent(int))); + connect(smtp, SIGNAL(recipientRejected(int, const QString &, const QByteArray &)), this, + SLOT(recipientRejected(int, const QString &, const QByteArray &))); + connect(smtp, SIGNAL(senderRejected(int, const QString &, const QByteArray &)), this, + SLOT(senderRejected(int, const QString &, const QByteArray &))); } SmtpClient::~SmtpClient() diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0bb9654a1..c8d3fbd59 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -328,40 +328,6 @@ cockatrice_add_test( LABELS server ) -cockatrice_add_test( - NAME - server_forced_password_change_test - SOURCES - server_forced_password_change_test.cpp - LIBS - libcockatrice_network_server_remote - libcockatrice_rng - LABELS - server -) -cockatrice_add_test( - NAME - client_login_status_test - SOURCES - ${VERSION_STRING_CPP} - client_login_status_test.cpp - LIBS - libcockatrice_network_client_remote - LABELS - ci -) -# The local game runs one client and one server in the same process. -# Either of them can be destroyed first; the survivor has to stay usable. -cockatrice_add_test( - NAME - local_client_lifetime_test - SOURCES - local_client_lifetime_test.cpp - LIBS - libcockatrice_network - LABELS - ci -) cockatrice_add_test( NAME server_room_channels_test @@ -528,7 +494,6 @@ add_subdirectory(carddatabase) add_subdirectory(deck_list_model) add_subdirectory(deck_list_zones) add_subdirectory(loading_from_clipboard) -add_subdirectory(model_contract) add_subdirectory(movecard_tests) add_subdirectory(oracle) add_subdirectory(settings) diff --git a/tests/carddatabase/CMakeLists.txt b/tests/carddatabase/CMakeLists.txt index 7d83640a1..22171a9e8 100644 --- a/tests/carddatabase/CMakeLists.txt +++ b/tests/carddatabase/CMakeLists.txt @@ -35,21 +35,6 @@ if(WITH_ORACLE OR WITH_CLIENT) LIBS libcockatrice_filters ) - - # ------------------------ - # Visual Display Model Test - # ------------------------ - cockatrice_add_test( - NAME - visual_display_model_test - SOURCES - ${VERSION_STRING_CPP} - visual_display_model_test.cpp - mocks.cpp - LIBS - libcockatrice_card - libcockatrice_models - ) endif() # ------------------------ diff --git a/tests/carddatabase/visual_display_model_test.cpp b/tests/carddatabase/visual_display_model_test.cpp deleted file mode 100644 index 5fa554bb3..000000000 --- a/tests/carddatabase/visual_display_model_test.cpp +++ /dev/null @@ -1,188 +0,0 @@ -/** - * @file visual_display_model_test.cpp - * @brief Tests for the flattened model behind the visual card database grid. - */ - -#include "card_database_display_model.h" -#include "card_database_model.h" -#include "card_database_visual_display_model.h" -#include "libcockatrice/card/card_info.h" -#include "libcockatrice/card/database/card_database.h" -#include "libcockatrice/card/database/card_database_querier.h" -#include "libcockatrice/card/printing/exact_card.h" -#include "libcockatrice/card/printing/printing_info.h" -#include "libcockatrice/card/set/card_set.h" -#include "libcockatrice/interfaces/noop_card_preference_provider.h" -#include "libcockatrice/interfaces/noop_card_set_priority_controller.h" -#include "test_card_database_path_provider.h" - -#include "gtest/gtest.h" -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ -// GTest's main() creates no QCoreApplication, which QTimer needs for its event loop. -const QCoreApplication &application() -{ - static int argc = 1; - static char executable[] = "visual_display_model_test"; - static char *argv[] = {executable, nullptr}; - static const QCoreApplication app(argc, argv); - return app; -} - -/// Lets the rebuild timer fire, as it would in the running client. -void waitForRebuild() -{ - QEventLoop loop; - QTimer::singleShot(100, &loop, &QEventLoop::quit); - loop.exec(); -} - -class CardDatabaseVisualDisplayModelTest : public ::testing::Test -{ -protected: - void SetUp() override - { - (void)application(); - - db = std::make_unique(nullptr, new NoopCardPreferenceProvider(), - new TestCardDatabasePathProvider(), new NoopCardSetPriorityController()); - db->loadCardDatabases(); - - cardModel = std::make_unique(db.get(), false); - displayModel = std::make_unique(); - displayModel->setSourceModel(cardModel.get()); - visualModel = std::make_unique(displayModel.get(), db->query()); - visualModel->refresh(); - } - - /** - * @brief Builds a card with @p printingCount printings in each named set. - * @param printingCountPerSet Set short name to the number of printings in that set. - */ - [[nodiscard]] CardInfoPtr makeCard(const QString &name, const QMap &printingCountPerSet) - { - SetToPrintingsMap setsInfo; - for (auto it = printingCountPerSet.constBegin(); it != printingCountPerSet.constEnd(); ++it) { - CardSetPtr set = CardSet::newInstance(&setController, it.key(), it.key() + " test set", "expansion", - QDate(), CardSet::PriorityPrimary); - for (int printing = 0; printing < it.value(); ++printing) { - QHash properties; - properties["uuid"] = QString("%1-%2-%3").arg(it.key(), name).arg(printing); - setsInfo[it.key()].append(PrintingInfo(set, LazyPropertiesHash(properties))); - } - } - - CardInfo::UiAttributes attributes = {.tableRow = 1}; - return CardInfo::newInstance(name, "", false, {}, {}, {}, setsInfo, attributes); - } - - NoopCardSetPriorityController setController; - std::unique_ptr db; - std::unique_ptr cardModel; - std::unique_ptr displayModel; - std::unique_ptr visualModel; -}; - -TEST_F(CardDatabaseVisualDisplayModelTest, ExposesTheCardInfoBehindAProxyRow) -{ - ASSERT_GT(displayModel->rowCount(), 0); - - const QVariant info = displayModel->data(displayModel->index(0, CardDatabaseModel::NameColumn), - CardDatabaseDisplayModel::CardInfoRole); - ASSERT_TRUE(info.isValid()); - EXPECT_FALSE(info.value().isNull()); -} - -TEST_F(CardDatabaseVisualDisplayModelTest, FlattensOneRowPerProxyCardWithoutSetFilters) -{ - ASSERT_GT(displayModel->rowCount(), 0); - EXPECT_EQ(displayModel->rowCount(), visualModel->rowCount()); - - for (int row = 0; row < visualModel->rowCount(); ++row) { - const ExactCard card = visualModel->cardAt(row); - ASSERT_TRUE(card) << "row " << row; - EXPECT_EQ( - displayModel->data(displayModel->index(row, CardDatabaseModel::NameColumn), Qt::DisplayRole).toString(), - card.getName()) - << "row " << row; - } -} - -TEST_F(CardDatabaseVisualDisplayModelTest, ShowsOneRowPerCardWhenNoSetIsFiltered) -{ - const int rowsBefore = visualModel->rowCount(); - - db->addCard(makeCard("Multi", {{"TST", 2}, {"OTH", 1}})); - visualModel->refresh(); - - EXPECT_EQ(rowsBefore + 1, visualModel->rowCount()); - EXPECT_EQ("Multi", visualModel->cardAt(visualModel->rowCount() - 1).getName()); -} - -TEST_F(CardDatabaseVisualDisplayModelTest, ShowsEveryPrintingOfAFilteredSet) -{ - db->addCard(makeCard("Multi", {{"TST", 2}, {"OTH", 1}})); - visualModel->refresh(); - - visualModel->setSetFilterTerms({"TST"}); - - ASSERT_EQ(2, visualModel->rowCount()); - for (int row = 0; row < visualModel->rowCount(); ++row) { - const ExactCard card = visualModel->cardAt(row); - ASSERT_TRUE(card) << "row " << row; - EXPECT_EQ("Multi", card.getName()); - ASSERT_FALSE(card.getPrinting().isEmpty()) << "row " << row; - EXPECT_EQ("TST", card.getPrinting().getSet()->getShortName()) << "row " << row; - } - EXPECT_NE(visualModel->cardAt(0).getPrinting(), visualModel->cardAt(1).getPrinting()); -} - -TEST_F(CardDatabaseVisualDisplayModelTest, HidesCardsWithoutAPrintingInTheFilteredSets) -{ - visualModel->setSetFilterTerms({"NO SUCH SET"}); - - EXPECT_EQ(0, visualModel->rowCount()); -} - -TEST_F(CardDatabaseVisualDisplayModelTest, RestoresOneRowPerCardWhenTheSetTermsClear) -{ - db->addCard(makeCard("Multi", {{"TST", 2}, {"OTH", 1}})); - visualModel->refresh(); - const int rowsBefore = visualModel->rowCount(); - - visualModel->setSetFilterTerms({"TST"}); - ASSERT_EQ(2, visualModel->rowCount()); - - visualModel->setSetFilterTerms({}); - EXPECT_EQ(rowsBefore, visualModel->rowCount()); -} - -TEST_F(CardDatabaseVisualDisplayModelTest, RebuildsByItselfWhenTheProxyGainsARow) -{ - const int rowsBefore = visualModel->rowCount(); - - db->addCard(makeCard("Late", {{"TST", 1}})); - waitForRebuild(); - - EXPECT_EQ(rowsBefore + 1, visualModel->rowCount()); -} - -TEST_F(CardDatabaseVisualDisplayModelTest, ReportsTheCardNameForDisplayAndAccessibleText) -{ - ASSERT_GT(visualModel->rowCount(), 0); - const QString expected = visualModel->cardAt(0).getName(); - - EXPECT_EQ(expected, visualModel->data(visualModel->index(0), Qt::DisplayRole).toString()); - EXPECT_EQ(expected, visualModel->data(visualModel->index(0), Qt::AccessibleTextRole).toString()); - EXPECT_FALSE(visualModel->data(QModelIndex(), Qt::DisplayRole).isValid()); -} -} // namespace diff --git a/tests/client_login_status_test.cpp b/tests/client_login_status_test.cpp deleted file mode 100644 index d8ea63550..000000000 --- a/tests/client_login_status_test.cpp +++ /dev/null @@ -1,116 +0,0 @@ -/** @file client_login_status_test.cpp - * @brief The login response state machine must keep the session a forced password change needs. - * @ingroup Tests - */ - -#include "abstract_client.h" -#include "libcockatrice/interfaces/interface_network_settings_provider.h" -#include "libcockatrice/protocol/pb/response.pb.h" -#include "libcockatrice/protocol/pb/response_login.pb.h" -#include "remote_client.h" - -#include -#include -#include -#include - -namespace -{ - -class StubNetworkSettingsProvider : public INetworkSettingsProvider -{ -public: - QString getClientID() override - { - return {}; - } - [[nodiscard]] int getTimeOut() const override - { - return 1000; - } - [[nodiscard]] int getKeepAlive() const override - { - return 60; - } - [[nodiscard]] bool getNotifyAboutUpdates() const override - { - return false; - } - void setKnownMissingFeatures(const QString &) override - { - } - QString getKnownMissingFeatures() override - { - return {}; - } -}; - -/** - * @brief Feeds a login response to RemoteClient without a socket. - * - * `loginResponse` is the slot the socket thread runs for the login command's answer, so - * invoking it directly exercises the status transition that decides whether `readData()` - * tears the connection down right after it. - */ -class ClientLoginStatusTest : public ::testing::Test -{ -protected: - void SetUp() override - { - client = new RemoteClient(nullptr, &provider); - } - - void TearDown() override - { - delete client; - client = nullptr; - } - - void deliverLoginResponse(Response::ResponseCode code) - { - Response response; - response.set_response_code(code); - response.MutableExtension(Response_Login::ext); - ASSERT_TRUE( - QMetaObject::invokeMethod(client, "loginResponse", Qt::DirectConnection, Q_ARG(Response, response))); - } - - StubNetworkSettingsProvider provider; - RemoteClient *client = nullptr; -}; - -/** - * @brief A required password change is not a failed login. - * - * The server keeps the session open and accepts only ping and Command_AccountPassword until - * the password is replaced. StatusDisconnecting would make readData() close the socket right - * after this slot returns - before the dialog that collects the new password (queued to the - * UI thread) can send anything - and the pending command would never be answered because the - * ping timer that times it out dies with the connection. StatusLoggedIn would start the - * normal post-login flow for a session that cannot serve it. - */ -TEST_F(ClientLoginStatusTest, PasswordChangeRequiredKeepsTheSessionOpen) -{ - deliverLoginResponse(Response::RespPasswordChangeRequired); - - EXPECT_EQ(client->getStatus(), StatusPasswordChangeRequired); - EXPECT_NE(client->getStatus(), StatusDisconnecting); - EXPECT_NE(client->getStatus(), StatusLoggedIn); -} - -/** @brief Every other login error still ends the connection, as it did before. */ -TEST_F(ClientLoginStatusTest, OtherLoginErrorsDisconnect) -{ - deliverLoginResponse(Response::RespWrongPassword); - - EXPECT_EQ(client->getStatus(), StatusDisconnecting); -} - -} // namespace - -int main(int argc, char **argv) -{ - QCoreApplication app(argc, argv); - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/tests/game/CMakeLists.txt b/tests/game/CMakeLists.txt index 42c935c8f..4a7492c71 100644 --- a/tests/game/CMakeLists.txt +++ b/tests/game/CMakeLists.txt @@ -1,18 +1,28 @@ -cockatrice_add_test( - NAME - game_logic_test - SOURCES - game_test_main.cpp - card_state_test.cpp - card_zone_logic_test.cpp - game_event_handler_test.cpp - player_actions_test.cpp - replay_test.cpp +add_executable( + game_smoke_test + ${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/cache_settings.cpp + ${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/card_counter_settings.cpp + ${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/shortcuts_settings.cpp + ${CMAKE_SOURCE_DIR}/cockatrice/src/client/network/update/client/release_channel.cpp ${VERSION_STRING_CPP} - LIBS - libcockatrice_game - INCLUDE_DIRS - ${CMAKE_CURRENT_SOURCE_DIR} - DEFINITIONS - GAME_TEST_DATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data" + game_smoke_test.cpp ) + +# SettingsCache is still reached from the game logic, but it lives in the application sources +# rather than in libcockatrice_settings, so the client settings sources are compiled in here +# exactly like loader_local_matching_test and theme_migration_test do. +target_include_directories(game_smoke_test PRIVATE ${CMAKE_SOURCE_DIR}/cockatrice/src) + +target_link_libraries( + game_smoke_test + PRIVATE libcockatrice_game + PRIVATE Threads::Threads + PRIVATE ${GTEST_BOTH_LIBRARIES} + PRIVATE ${TEST_QT_MODULES} +) + +add_test(NAME game_smoke_test COMMAND game_smoke_test) + +if(NOT GTEST_FOUND) + add_dependencies(game_smoke_test gtest) +endif() diff --git a/tests/game/card_state_test.cpp b/tests/game/card_state_test.cpp deleted file mode 100644 index 2f2c43bf4..000000000 --- a/tests/game/card_state_test.cpp +++ /dev/null @@ -1,110 +0,0 @@ -#include "gtest/gtest.h" -#include -#include -#include -#include -#include - -namespace -{ - -CardRef cardRefFor(const QString &name) -{ - CardRef cardRef; - cardRef.name = name; - return cardRef; -} - -} // namespace - -TEST(CardStateTest, AttachmentGraphKeepsBothEndsConsistent) -{ - CardState host(nullptr, nullptr, cardRefFor(QStringLiteral("Host")), 1); - CardState guest(nullptr, nullptr, cardRefFor(QStringLiteral("Guest")), 2); - - guest.setAttachedTo(&host); - host.addAttachedCard(&guest); - - EXPECT_EQ(guest.getAttachedTo(), &host); - ASSERT_EQ(host.getAttachedCards().size(), 1); - EXPECT_EQ(host.getAttachedCards().first(), &guest); - - host.removeAttachedCard(&guest); - guest.setAttachedTo(nullptr); - - EXPECT_TRUE(host.getAttachedCards().isEmpty()); - EXPECT_EQ(guest.getAttachedTo(), nullptr); -} - -TEST(CardStateTest, CountersAddUpdateAndClear) -{ - CardState card(nullptr, nullptr, cardRefFor(QStringLiteral("Cat")), 1); - int changes = 0; - QObject::connect(&card, &CardState::countersChanged, [&changes]() { ++changes; }); - - card.insertCounter(1, 2); - EXPECT_EQ(card.getCounters().value(1), 2); - - card.setCounter(1, 3); - EXPECT_EQ(card.getCounters().value(1), 3); - - card.setCounter(1, 0); - EXPECT_FALSE(card.getCounters().contains(1)); - - card.setCounter(2, 1); - card.clearCounters(); - EXPECT_TRUE(card.getCounters().isEmpty()); - - EXPECT_EQ(changes, 5); -} - -TEST(CardStateTest, TapFlipAttackingAndReset) -{ - CardState card(nullptr, nullptr, cardRefFor(QStringLiteral("Cat")), 1); - card.setTapped(true); - card.setFaceDown(true); - card.setAttacking(true); - card.setAnnotation(QStringLiteral("note")); - card.setPT(QStringLiteral("4/4")); - card.setDoesntUntap(true); - card.setDestroyOnZoneChange(true); - card.setColor(QStringLiteral("red")); - card.insertCounter(1, 2); - - card.resetState(true); - - EXPECT_FALSE(card.getTapped()); - EXPECT_FALSE(card.getAttacking()); - EXPECT_TRUE(card.getCounters().isEmpty()); - EXPECT_TRUE(card.getPT().isEmpty()); - EXPECT_FALSE(card.getDoesntUntap()); - EXPECT_EQ(card.getAnnotation(), QStringLiteral("note")); - // resetState() deliberately leaves these three alone. - EXPECT_TRUE(card.getFaceDown()); - EXPECT_EQ(card.getColor(), QStringLiteral("red")); - EXPECT_TRUE(card.getDestroyOnZoneChange()); - - card.setAnnotation(QStringLiteral("other")); - card.resetState(false); - EXPECT_TRUE(card.getAnnotation().isEmpty()); -} - -TEST(CardStateTest, ParsePT) -{ - const QVariantList plain = CardState::parsePT(QStringLiteral("3/3")); - ASSERT_EQ(plain.size(), 2); - EXPECT_EQ(plain.at(0).toString(), QStringLiteral("3")); - EXPECT_EQ(plain.at(1).toString(), QStringLiteral("3")); - - const QVariantList negative = CardState::parsePT(QStringLiteral("-1/2")); - ASSERT_EQ(negative.size(), 2); - EXPECT_EQ(negative.at(0).typeId(), QMetaType::Int); - EXPECT_EQ(negative.at(0).toInt(), -1); - EXPECT_EQ(negative.at(1).toString(), QStringLiteral("2")); - - const QVariantList star = CardState::parsePT(QStringLiteral("*")); - ASSERT_EQ(star.size(), 1); - EXPECT_EQ(star.at(0).toString(), QStringLiteral("*")); - - EXPECT_TRUE(CardState::parsePT(QString()).isEmpty()); -} diff --git a/tests/game/card_zone_logic_test.cpp b/tests/game/card_zone_logic_test.cpp deleted file mode 100644 index 7f0c39b47..000000000 --- a/tests/game/card_zone_logic_test.cpp +++ /dev/null @@ -1,107 +0,0 @@ -#include "gtest/gtest.h" -#include -#include -#include -#include -#include -#include - -namespace -{ - -CardRef cardRefFor(const QString &name) -{ - CardRef cardRef; - cardRef.name = name; - return cardRef; -} - -} // namespace - -TEST(CardZoneLogicTest, AddsAndTakesCardsWithoutAGraphicsSide) -{ - HandZoneLogic zone(nullptr, QStringLiteral("hand"), false, false, true); - CardState card(nullptr, nullptr, cardRefFor(QStringLiteral("Lightning Bolt")), 7); - - QSignalSpy cardAddedSpy(&zone, &CardZoneLogic::cardAdded); - QSignalSpy cardCountSpy(&zone, &CardZoneLogic::cardCountChanged); - - zone.addCard(&card, false, 0); - - ASSERT_EQ(zone.getCards().size(), 1); - EXPECT_EQ(zone.getCards().first(), &card); - EXPECT_EQ(card.getZone(), &zone); - EXPECT_EQ(cardAddedSpy.count(), 1); - EXPECT_GE(cardCountSpy.count(), 1); - - EXPECT_EQ(zone.takeCard(0, 7), &card); - EXPECT_TRUE(zone.getCards().isEmpty()); - EXPECT_EQ(card.getId(), 7); -} - -TEST(CardZoneLogicTest, AsksTheGraphicsSideToCreateCards) -{ - HandZoneLogic zone(nullptr, QStringLiteral("library"), false, true, true); - // The graphics side owns the view logic and registers it on the zone it - // shows; adding a card then goes through prepareAddCard and the request - // signal instead of the test emitting the signal itself. - ZoneViewZoneLogic view(nullptr, &zone, 1, true, false, false); - zone.getViews().append(&view); - - QSignalSpy requestSpy(&view, &CardZoneLogic::createCardRequested); - - CardState card(nullptr, nullptr, cardRefFor(QStringLiteral("Lightning Bolt")), 7); - zone.addCard(&card, true, 0, 0); - - ASSERT_EQ(requestSpy.count(), 1); - const QList arguments = requestSpy.takeFirst(); - const ServerInfo_Card cardInfo = arguments.at(0).value(); - EXPECT_EQ(cardInfo.id(), 7); - EXPECT_EQ(cardInfo.name(), "Lightning Bolt"); - EXPECT_TRUE(arguments.at(1).toBool()); - EXPECT_EQ(card.getZone(), &zone); - EXPECT_EQ(zone.getCards().size(), 1); -} - -TEST(CardZoneLogicTest, MoveBetweenZonesUpdatesCardZone) -{ - HandZoneLogic first(nullptr, QStringLiteral("hand"), false, false, true); - HandZoneLogic second(nullptr, QStringLiteral("grave"), false, false, true); - CardState card(nullptr, nullptr, cardRefFor(QStringLiteral("Cat")), 7); - - first.addCard(&card, false, 0); - ASSERT_EQ(first.getCards().size(), 1); - - CardState *taken = first.takeCard(0, 7); - ASSERT_EQ(taken, &card); - EXPECT_TRUE(first.getCards().isEmpty()); - - second.addCard(taken, false, 0); - EXPECT_EQ(second.getCards().size(), 1); - EXPECT_EQ(card.getZone(), &second); -} - -TEST(CardZoneLogicTest, GetCardFindsById) -{ - HandZoneLogic zone(nullptr, QStringLiteral("hand"), false, false, true); - CardState first(nullptr, nullptr, cardRefFor(QStringLiteral("Cat")), 7); - CardState second(nullptr, nullptr, cardRefFor(QStringLiteral("Dog")), 8); - - zone.addCard(&first, false, 0); - zone.addCard(&second, false, 0); - - EXPECT_EQ(zone.getCard(7), &first); - EXPECT_EQ(zone.getCard(8), &second); - EXPECT_EQ(zone.getCard(99), nullptr); -} - -TEST(CardZoneLogicTest, ReorganizeSignalFiresOnAdd) -{ - HandZoneLogic zone(nullptr, QStringLiteral("hand"), false, false, true); - CardState card(nullptr, nullptr, cardRefFor(QStringLiteral("Cat")), 7); - QSignalSpy reorganizeSpy(&zone, &CardZoneLogic::reorganizeCards); - - zone.addCard(&card, true, 0); - - EXPECT_EQ(reorganizeSpy.count(), 1); -} diff --git a/tests/game/data/cards.xml b/tests/game/data/cards.xml deleted file mode 100644 index e315dbf48..000000000 --- a/tests/game/data/cards.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - Cat - CAT - 0 - Meow! - - 111 - G - 2G - 2 - Creature — Cat - Creature - 3/3 - - - - Grizzly Bears - LEA - 0 - 1 - - - 112 - G - 1G - 2 - Creature — Bear - Creature - 2/2 - - - - Lightning Bolt - LEA - 3 - Lightning Bolt deals 3 damage to any target. - - 113 - R - R - 1 - Instant - Instant - - - - Forest - LEA - 2 - (Add G.) - - 114 - G - - 0 - Land - Land - - - - diff --git a/tests/game/data/spoilers.xml b/tests/game/data/spoilers.xml deleted file mode 100644 index 414ae10d4..000000000 --- a/tests/game/data/spoilers.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/tests/game/data/tokens.xml b/tests/game/data/tokens.xml deleted file mode 100644 index 30edbb24c..000000000 --- a/tests/game/data/tokens.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - Kitten - CAT - 0 - - 1 - - 211 - G - - - Token - Token - 1/1 - - - - diff --git a/tests/game/game_event_handler_test.cpp b/tests/game/game_event_handler_test.cpp deleted file mode 100644 index 2db6a1660..000000000 --- a/tests/game/game_event_handler_test.cpp +++ /dev/null @@ -1,566 +0,0 @@ -#include "game_test_fixture.h" - -#include "gtest/gtest.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ - -/** - * Wraps one player-scoped event in the container the server would send and - * pushes it through the real dispatcher, so the test exercises the same - * GameEventHandler -> PlayerEventHandler path as a live client. - */ -template -void sendGameEvent(GameHarness &harness, - int playerId, - const EventType &message, - const GameEventContext *context = nullptr) -{ - GameEventContainer container; - container.set_game_id(42); - if (context != nullptr) { - container.mutable_context()->CopyFrom(*context); - } - - GameEvent *event = container.add_event_list(); - event->set_player_id(playerId); - event->MutableExtension(EventType::ext)->CopyFrom(message); - - harness.game()->getGameEventHandler()->processGameEventContainer(container, harness.client(), - EventProcessingOptions()); -} - -Event_SetCardAttr cardAttrEvent(const char *zoneName, int cardId, CardAttribute attribute, const char *value) -{ - Event_SetCardAttr event; - event.set_zone_name(zoneName); - event.set_card_id(cardId); - event.set_attribute(attribute); - event.set_attr_value(value); - return event; -} - -} // namespace - -TEST(GameEventHandlerTest, MoveCardMovesBetweenZones) -{ - GameHarness harness; - CardState *cat = harness.addCardToZone(ZoneNames::HAND, QStringLiteral("Cat"), 7); - ASSERT_NE(cat, nullptr); - - Event_MoveCard event; - event.set_card_id(7); - event.set_start_player_id(1); - event.set_start_zone(ZoneNames::HAND); - event.set_position(0); - event.set_target_player_id(1); - event.set_target_zone(ZoneNames::TABLE); - event.set_x(0); - event.set_y(TableZoneLogic::tableRowToGridY(0)); - event.set_new_card_id(7); - event.set_face_down(false); - - sendGameEvent(harness, 1, event); - - EXPECT_TRUE(harness.zone(ZoneNames::HAND)->getCards().isEmpty()); - CardZoneLogic *table = harness.zone(ZoneNames::TABLE); - ASSERT_EQ(table->getCards().size(), 1); - EXPECT_EQ(table->getCards().first(), cat); - EXPECT_EQ(cat->getZone(), table); - EXPECT_EQ(cat->getId(), 7); - EXPECT_EQ(cat->getCardRef().name, QStringLiteral("Cat")); - EXPECT_FALSE(cat->getFaceDown()); - EXPECT_EQ(cat->getPT(), QStringLiteral("3/3")); - EXPECT_EQ(cat->getGridPoint(), QPoint(0, TableZoneLogic::tableRowToGridY(0))); -} - -TEST(GameEventHandlerTest, MoveCardFaceDownKeepsTheCardHidden) -{ - GameHarness harness; - CardState *cat = harness.addCardToZone(ZoneNames::HAND, QStringLiteral("Cat"), 7); - ASSERT_NE(cat, nullptr); - - Event_MoveCard event; - event.set_card_id(7); - event.set_start_player_id(1); - event.set_start_zone(ZoneNames::HAND); - event.set_position(0); - event.set_target_player_id(1); - event.set_target_zone(ZoneNames::TABLE); - event.set_x(1); - event.set_y(TableZoneLogic::tableRowToGridY(2)); - event.set_new_card_id(7); - event.set_face_down(true); - - sendGameEvent(harness, 1, event); - - ASSERT_EQ(harness.zone(ZoneNames::TABLE)->getCards().size(), 1); - EXPECT_TRUE(cat->getFaceDown()); - // A face-down card keeps whatever P/T it had instead of picking up the - // printed one from the card database. - EXPECT_TRUE(cat->getPT().isEmpty()); -} - -TEST(GameEventHandlerTest, SetCardAttrUpdatesSingleCard) -{ - GameHarness harness; - CardState *cat = harness.addCardToZone(ZoneNames::TABLE, QStringLiteral("Cat"), 7); - ASSERT_NE(cat, nullptr); - - sendGameEvent(harness, 1, cardAttrEvent(ZoneNames::TABLE, 7, AttrTapped, "1")); - sendGameEvent(harness, 1, cardAttrEvent(ZoneNames::TABLE, 7, AttrAttacking, "1")); - sendGameEvent(harness, 1, cardAttrEvent(ZoneNames::TABLE, 7, AttrPT, "5/5")); - sendGameEvent(harness, 1, cardAttrEvent(ZoneNames::TABLE, 7, AttrAnnotation, "note")); - sendGameEvent(harness, 1, cardAttrEvent(ZoneNames::TABLE, 7, AttrColor, "red")); - sendGameEvent(harness, 1, cardAttrEvent(ZoneNames::TABLE, 7, AttrDoesntUntap, "1")); - sendGameEvent(harness, 1, cardAttrEvent(ZoneNames::TABLE, 7, AttrFaceDown, "1")); - - EXPECT_TRUE(cat->getTapped()); - EXPECT_TRUE(cat->getAttacking()); - EXPECT_EQ(cat->getPT(), QStringLiteral("5/5")); - EXPECT_EQ(cat->getAnnotation(), QStringLiteral("note")); - EXPECT_EQ(cat->getColor(), QStringLiteral("red")); - EXPECT_TRUE(cat->getDoesntUntap()); - EXPECT_TRUE(cat->getFaceDown()); -} - -TEST(GameEventHandlerTest, SetCardAttrWithoutCardIdTouchesEveryCardInZone) -{ - GameHarness harness; - CardState *cat = harness.addCardToZone(ZoneNames::TABLE, QStringLiteral("Cat"), 7); - CardState *bears = harness.addCardToZone(ZoneNames::TABLE, QStringLiteral("Grizzly Bears"), 9); - ASSERT_NE(cat, nullptr); - ASSERT_NE(bears, nullptr); - - Event_SetCardAttr event; - event.set_zone_name(ZoneNames::TABLE); - event.set_attribute(AttrAnnotation); - event.set_attr_value("marked"); - - sendGameEvent(harness, 1, event); - - EXPECT_EQ(cat->getAnnotation(), QStringLiteral("marked")); - EXPECT_EQ(bears->getAnnotation(), QStringLiteral("marked")); -} - -TEST(GameEventHandlerTest, UntapAllLeavesDoesntUntapCardsTapped) -{ - GameHarness harness; - CardState *cat = harness.addCardToZone(ZoneNames::TABLE, QStringLiteral("Cat"), 7); - CardState *bears = harness.addCardToZone(ZoneNames::TABLE, QStringLiteral("Grizzly Bears"), 9); - ASSERT_NE(cat, nullptr); - ASSERT_NE(bears, nullptr); - cat->setDoesntUntap(true); - - sendGameEvent(harness, 1, cardAttrEvent(ZoneNames::TABLE, 7, AttrTapped, "1")); - sendGameEvent(harness, 1, cardAttrEvent(ZoneNames::TABLE, 9, AttrTapped, "1")); - - Event_SetCardAttr untapAll; - untapAll.set_zone_name(ZoneNames::TABLE); - untapAll.set_attribute(AttrTapped); - untapAll.set_attr_value("0"); - - sendGameEvent(harness, 1, untapAll); - - EXPECT_TRUE(cat->getTapped()); - EXPECT_FALSE(bears->getTapped()); -} - -TEST(GameEventHandlerTest, SetCardCounterAddsAndRemoves) -{ - GameHarness harness; - CardState *cat = harness.addCardToZone(ZoneNames::HAND, QStringLiteral("Cat"), 7); - ASSERT_NE(cat, nullptr); - - Event_SetCardCounter add; - add.set_zone_name(ZoneNames::HAND); - add.set_card_id(7); - add.set_counter_id(1); - add.set_counter_value(2); - - sendGameEvent(harness, 1, add); - - EXPECT_EQ(cat->getCounters().value(1), 2); - - Event_SetCardCounter remove = add; - remove.set_counter_value(0); - - sendGameEvent(harness, 1, remove); - - EXPECT_FALSE(cat->getCounters().contains(1)); -} - -TEST(GameEventHandlerTest, AttachCardLinksBothEndsAndUnattachClearsThem) -{ - GameHarness harness; - CardState *host = harness.addCardToZone(ZoneNames::TABLE, QStringLiteral("Cat"), 7); - CardState *guest = harness.addCardToZone(ZoneNames::TABLE, QStringLiteral("Grizzly Bears"), 9); - ASSERT_NE(host, nullptr); - ASSERT_NE(guest, nullptr); - - Event_AttachCard attach; - attach.set_start_zone(ZoneNames::TABLE); - attach.set_card_id(9); - attach.set_target_player_id(1); - attach.set_target_zone(ZoneNames::TABLE); - attach.set_target_card_id(7); - - sendGameEvent(harness, 1, attach); - - EXPECT_EQ(guest->getAttachedTo(), host); - ASSERT_EQ(host->getAttachedCards().size(), 1); - EXPECT_EQ(host->getAttachedCards().first(), guest); - - Event_AttachCard detach; - detach.set_start_zone(ZoneNames::TABLE); - detach.set_card_id(9); - - sendGameEvent(harness, 1, detach); - - EXPECT_EQ(guest->getAttachedTo(), nullptr); - EXPECT_TRUE(host->getAttachedCards().isEmpty()); -} - -TEST(GameEventHandlerTest, ChangeZonePropertiesUpdatesReveal) -{ - GameHarness harness; - CardZoneLogic *deck = harness.zone(ZoneNames::DECK); - ASSERT_NE(deck, nullptr); - - Event_ChangeZoneProperties reveal; - reveal.set_zone_name(ZoneNames::DECK); - reveal.set_always_reveal_top_card(true); - - sendGameEvent(harness, 1, reveal); - EXPECT_TRUE(deck->getAlwaysRevealTopCard()); - - reveal.set_always_reveal_top_card(false); - sendGameEvent(harness, 1, reveal); - EXPECT_FALSE(deck->getAlwaysRevealTopCard()); -} - -TEST(GameEventHandlerTest, DrawCardsMovesNamedCardsFromDeckToHand) -{ - GameHarness harness; - ASSERT_NE(harness.addCardToZone(ZoneNames::DECK, QStringLiteral("Cat"), 7), nullptr); - - Event_DrawCards draw; - draw.set_number(1); - ServerInfo_Card *cardInfo = draw.add_cards(); - cardInfo->set_id(7); - cardInfo->set_name("Cat"); - - sendGameEvent(harness, 1, draw); - - EXPECT_TRUE(harness.zone(ZoneNames::DECK)->getCards().isEmpty()); - ASSERT_EQ(harness.zone(ZoneNames::HAND)->getCards().size(), 1); - CardState *drawn = harness.zone(ZoneNames::HAND)->getCards().first(); - EXPECT_EQ(drawn->getId(), 7); - EXPECT_EQ(drawn->getName(), QStringLiteral("Cat")); -} - -TEST(GameEventHandlerTest, FlipCardHidesAndRevealsTheCard) -{ - GameHarness harness; - CardState *cat = harness.addCardToZone(ZoneNames::TABLE, QStringLiteral("Cat"), 7); - ASSERT_NE(cat, nullptr); - - Event_FlipCard flip; - flip.set_zone_name(ZoneNames::TABLE); - flip.set_card_id(7); - flip.set_face_down(true); - - sendGameEvent(harness, 1, flip); - - EXPECT_TRUE(cat->getFaceDown()); - // Flipping face down does not touch the card reference: the client must not - // learn (or overwrite) the name of a card it is not allowed to see. - EXPECT_EQ(cat->getName(), QStringLiteral("Cat")); - - // Turning it face up carries the real printing, so the state picks it up. - flip.set_face_down(false); - flip.set_card_name("Grizzly Bears"); - - sendGameEvent(harness, 1, flip); - - EXPECT_FALSE(cat->getFaceDown()); - EXPECT_EQ(cat->getName(), QStringLiteral("Grizzly Bears")); -} - -TEST(GameEventHandlerTest, DestroyCardRemovesItAndDetachesAttachedCards) -{ - GameHarness harness; - CardState *host = harness.addCardToZone(ZoneNames::TABLE, QStringLiteral("Cat"), 7); - CardState *guest = harness.addCardToZone(ZoneNames::TABLE, QStringLiteral("Grizzly Bears"), 9); - ASSERT_NE(host, nullptr); - ASSERT_NE(guest, nullptr); - - guest->setAttachedTo(host); - ASSERT_EQ(guest->getAttachedTo(), host); - ASSERT_EQ(host->getAttachedCards().size(), 1); - - Event_DestroyCard destroy; - destroy.set_zone_name(ZoneNames::TABLE); - destroy.set_card_id(7); - - sendGameEvent(harness, 1, destroy); - - CardZoneLogic *table = harness.zone(ZoneNames::TABLE); - ASSERT_EQ(table->getCards().size(), 1); - EXPECT_EQ(table->getCards().first(), guest); - EXPECT_FALSE(table->getCards().contains(host)); - - // The attached card survives the host and is left un-attached, ready to be - // laid out again by the zone. - EXPECT_EQ(guest->getAttachedTo(), nullptr); - EXPECT_TRUE(host->getAttachedCards().isEmpty()); - EXPECT_EQ(guest->getZone(), table); -} - -TEST(GameEventHandlerTest, CreateTokenUsesDatabasePTUnlessFaceDown) -{ - GameHarness harness; - CardZoneLogic *table = harness.zone(ZoneNames::TABLE); - ASSERT_NE(table, nullptr); - - QList created; - QObject::connect(table, &CardZoneLogic::createCardRequested, table, - [&created](const ServerInfo_Card &cardInfo, bool) { created.append(cardInfo); }); - - Event_CreateToken token; - token.set_zone_name(ZoneNames::TABLE); - token.set_card_id(11); - token.set_card_name("Kitten"); - token.set_x(2); - token.set_y(1); - token.set_color("green"); - token.set_annotation("a kitten"); - token.set_destroy_on_zone_change(true); - - sendGameEvent(harness, 1, token); - - ASSERT_EQ(created.size(), 1); - // The event carries no P/T, so the token takes the printed one. - EXPECT_EQ(created.first().pt(), "1/1"); - EXPECT_EQ(created.first().name(), "Kitten"); - EXPECT_EQ(created.first().x(), 2); - EXPECT_EQ(created.first().y(), 1); - EXPECT_EQ(created.first().color(), "green"); - EXPECT_EQ(created.first().annotation(), "a kitten"); - EXPECT_TRUE(created.first().destroy_on_zone_change()); - EXPECT_FALSE(created.first().face_down()); - - // An explicit P/T always wins over the database. - Event_CreateToken explicitPt = token; - explicitPt.set_card_id(12); - explicitPt.set_pt("3/3"); - - sendGameEvent(harness, 1, explicitPt); - - ASSERT_EQ(created.size(), 2); - EXPECT_EQ(created.last().pt(), "3/3"); - - // A face-down token must not leak the printed P/T. - Event_CreateToken faceDown = token; - faceDown.set_card_id(13); - faceDown.set_face_down(true); - - sendGameEvent(harness, 1, faceDown); - - ASSERT_EQ(created.size(), 3); - EXPECT_TRUE(created.last().face_down()); - EXPECT_TRUE(created.last().pt().empty()); -} - -TEST(GameEventHandlerTest, ConcedeAndUnconcedeUpdatePlayerState) -{ - GameHarness harness; - ASSERT_NE(harness.addCardToZone(ZoneNames::TABLE, QStringLiteral("Cat"), 7), nullptr); - EXPECT_FALSE(harness.player()->getConceded()); - - Event_PlayerPropertiesChanged properties; - properties.mutable_player_properties()->set_player_id(1); - properties.mutable_player_properties()->set_conceded(true); - - GameEventContext concedeContext; - concedeContext.MutableExtension(Context_Concede::ext); - - sendGameEvent(harness, 1, properties, &concedeContext); - - EXPECT_TRUE(harness.player()->getConceded()); - // Conceding tears the board down for that player. - EXPECT_TRUE(harness.zone(ZoneNames::TABLE)->getCards().isEmpty()); - - Event_PlayerPropertiesChanged back = properties; - back.mutable_player_properties()->set_conceded(false); - - GameEventContext unconcedeContext; - unconcedeContext.MutableExtension(Context_Unconcede::ext); - - sendGameEvent(harness, 1, back, &unconcedeContext); - - EXPECT_FALSE(harness.player()->getConceded()); -} - -TEST(GameEventHandlerTest, SetActivePlayerAndPhaseUpdateGameState) -{ - GameHarness harness; - GameState *state = harness.game()->getGameState(); - ASSERT_NE(state, nullptr); - EXPECT_EQ(state->getActivePlayer(), -1); - EXPECT_EQ(state->getCurrentPhase(), -1); - - Event_SetActivePlayer setPlayer; - setPlayer.set_active_player_id(1); - - sendGameEvent(harness, 1, setPlayer); - - EXPECT_EQ(state->getActivePlayer(), 1); - - Event_SetActivePhase setPhase; - setPhase.set_phase(3); - - sendGameEvent(harness, 1, setPhase); - - EXPECT_EQ(state->getCurrentPhase(), 3); -} - -TEST(GameEventHandlerTest, CreateSetAndDeletePlayerCounter) -{ - GameHarness harness; - - ServerInfo_Counter counter; - counter.set_id(1); - counter.set_name("life"); - counter.set_radius(1); - counter.set_count(20); - - Event_CreateCounter create; - create.mutable_counter_info()->CopyFrom(counter); - - sendGameEvent(harness, 1, create); - - CounterState *state = harness.player()->getCounters().value(1); - ASSERT_NE(state, nullptr); - EXPECT_EQ(state->getName(), QStringLiteral("life")); - EXPECT_EQ(state->getValue(), 20); - EXPECT_EQ(harness.player()->getLifeCounter(), state); - - Event_SetCounter set; - set.set_counter_id(1); - set.set_value(15); - - sendGameEvent(harness, 1, set); - - EXPECT_EQ(state->getValue(), 15); - - // Updating a counter that does not exist is a no-op, not a crash. - Event_SetCounter unknown = set; - unknown.set_counter_id(99); - sendGameEvent(harness, 1, unknown); - - EXPECT_FALSE(harness.player()->getCounters().contains(99)); - - Event_DelCounter remove; - remove.set_counter_id(1); - - sendGameEvent(harness, 1, remove); - - EXPECT_FALSE(harness.player()->getCounters().contains(1)); - EXPECT_EQ(harness.player()->getLifeCounter(), nullptr); -} - -TEST(GameEventHandlerTest, LeaveRemovesThePlayer) -{ - GameHarness harness; - ASSERT_NE(harness.addCardToZone(ZoneNames::TABLE, QStringLiteral("Cat"), 7), nullptr); - ASSERT_NE(harness.game()->getPlayerManager()->getPlayer(1), nullptr); - - Event_Leave leave; - leave.set_reason(Event_Leave::USER_LEFT); - - sendGameEvent(harness, 1, leave); - - EXPECT_EQ(harness.game()->getPlayerManager()->getPlayer(1), nullptr); - EXPECT_TRUE(harness.game()->getPlayerManager()->getPlayers().isEmpty()); -} - -TEST(GameEventHandlerTest, KickedSignalsGameClosed) -{ - GameHarness harness; - - bool closed = false; - QObject::connect(harness.game()->getGameEventHandler(), &GameEventHandler::gameClosed, - [&closed] { closed = true; }); - - sendGameEvent(harness, 1, Event_Kicked()); - - EXPECT_TRUE(closed); -} - -TEST(GameEventHandlerTest, GameClosedMarksTheSessionClosed) -{ - GameHarness harness; - ASSERT_FALSE(harness.game()->getGameState()->isGameClosed()); - - Event_GameStateChanged started; - started.set_game_started(true); - sendGameEvent(harness, 1, started); - ASSERT_TRUE(harness.game()->getGameMetaInfo()->started()); - - sendGameEvent(harness, 1, Event_GameClosed()); - - EXPECT_TRUE(harness.game()->getGameState()->isGameClosed()); - EXPECT_FALSE(harness.game()->getGameMetaInfo()->started()); -} - -TEST(GameEventHandlerTest, GameHostChangedMovesTheHostRole) -{ - GameHarness harness; - - ServerInfo_User bob; - bob.set_name("Bob"); - bob.set_user_level(ServerInfo_User::IsUser); - ASSERT_NE(harness.game()->getPlayerManager()->addPlayer(2, bob), nullptr); - - sendGameEvent(harness, 2, Event_GameHostChanged()); - - EXPECT_EQ(harness.game()->getGameState()->getHostId(), 2); -} diff --git a/tests/game/game_smoke_test.cpp b/tests/game/game_smoke_test.cpp new file mode 100644 index 000000000..356d544bf --- /dev/null +++ b/tests/game/game_smoke_test.cpp @@ -0,0 +1,106 @@ +/** + * @file game_smoke_test.cpp + * + * Links the game logic library into a target that contains no graphics code at all, and drives + * the two seams the graphics layer used to own: the attachment graph on CardState and the + * card-creation request a zone makes so the graphics side can build the matching card item. + */ + +#include "gtest/gtest.h" +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +CardRef cardRefFor(const QString &name) +{ + CardRef cardRef; + cardRef.name = name; + return cardRef; +} + +} // namespace + +TEST(CardStateTest, AttachmentGraphKeepsBothEndsConsistent) +{ + CardState host(nullptr, nullptr, cardRefFor(QStringLiteral("Host")), 1); + CardState guest(nullptr, nullptr, cardRefFor(QStringLiteral("Guest")), 2); + + guest.setAttachedTo(&host); + host.addAttachedCard(&guest); + + EXPECT_EQ(guest.getAttachedTo(), &host); + ASSERT_EQ(host.getAttachedCards().size(), 1); + EXPECT_EQ(host.getAttachedCards().first(), &guest); + + host.removeAttachedCard(&guest); + guest.setAttachedTo(nullptr); + + EXPECT_TRUE(host.getAttachedCards().isEmpty()); + EXPECT_EQ(guest.getAttachedTo(), nullptr); +} + +TEST(CardZoneLogicTest, AddsAndTakesCardsWithoutAGraphicsSide) +{ + HandZoneLogic zone(nullptr, QStringLiteral("hand"), false, false, true); + CardState card(nullptr, nullptr, cardRefFor(QStringLiteral("Lightning Bolt")), 7); + + QSignalSpy cardAddedSpy(&zone, &CardZoneLogic::cardAdded); + QSignalSpy cardCountSpy(&zone, &CardZoneLogic::cardCountChanged); + + zone.addCard(&card, false, 0); + + ASSERT_EQ(zone.getCards().size(), 1); + EXPECT_EQ(zone.getCards().first(), &card); + EXPECT_EQ(card.getZone(), &zone); + EXPECT_EQ(cardAddedSpy.count(), 1); + EXPECT_GE(cardCountSpy.count(), 1); + + EXPECT_EQ(zone.takeCard(0, 7), &card); + EXPECT_TRUE(zone.getCards().isEmpty()); + EXPECT_EQ(card.getId(), 7); +} + +TEST(CardZoneLogicTest, AsksTheGraphicsSideToCreateCards) +{ + HandZoneLogic zone(nullptr, QStringLiteral("library"), false, true, true); + QSignalSpy requestSpy(&zone, &CardZoneLogic::createCardRequested); + + ServerInfo_Card cardInfo; + cardInfo.set_name("Lightning Bolt"); + cardInfo.set_id(7); + cardInfo.set_x(0); + cardInfo.set_y(1); + + zone.createCardRequested(cardInfo, true); + + ASSERT_EQ(requestSpy.count(), 1); + const QList arguments = requestSpy.takeFirst(); + EXPECT_EQ(arguments.at(0).value().id(), 7); + EXPECT_TRUE(arguments.at(1).toBool()); +} + +int main(int argc, char **argv) +{ + // Keep SettingsCache reads and writes away from the real user profile: the game logic still + // reaches for a handful of settings, and the client settings sources linked into this test + // would otherwise touch the developer's own configuration. + QStandardPaths::setTestModeEnabled(true); + + QTemporaryDir home; + if (home.isValid()) { + qputenv("HOME", home.path().toLocal8Bit()); + } + + QCoreApplication app(argc, argv); + QLoggingCategory::setFilterRules("settings_cache.*=false"); + + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tests/game/game_test_fixture.h b/tests/game/game_test_fixture.h deleted file mode 100644 index e4703c917..000000000 --- a/tests/game/game_test_fixture.h +++ /dev/null @@ -1,129 +0,0 @@ -/** - * @file game_test_fixture.h - * - * Fixtures for tests that need a complete game/player/zone graph without the - * client, a server or any graphics object. Commands are captured on a recording - * AbstractClient, which is the same seam the real client uses. - */ - -#ifndef COCKATRICE_TESTS_GAME_TEST_FIXTURE_H -#define COCKATRICE_TESTS_GAME_TEST_FIXTURE_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -/** - * Captures every command the game logic sends, instead of writing it to a socket. - * - * AbstractClient::sendCommand uses a direct signal/slot connection in the same - * thread, so a command is in `sent` as soon as sendCommand returns. - */ -class RecordingClient : public AbstractClient -{ -public: - QList sent; - -protected: - void sendCommandContainer(const CommandContainer &cont) override - { - sent.append(cont); - } -}; - -/** - * Owns a local, single-client Game with one local player, plus the cards added - * to its zones. - */ -class GameHarness -{ -public: - explicit GameHarness(int playerId = 1) : playerId_(playerId), client_(std::make_unique()) - { - Event_GameJoined event; - event.set_host_id(playerId_); - event.set_player_id(playerId_); - event.mutable_game_info()->set_game_id(42); - event.mutable_game_info()->set_started(true); - - QList clients{client_.get()}; - QMap roomGameTypes; - game_ = std::make_unique(nullptr, true, clients, event, roomGameTypes); - - ServerInfo_User user; - user.set_name("Alice"); - user.set_user_level(ServerInfo_User::IsUser | ServerInfo_User::IsRegistered); - game_->getPlayerManager()->addPlayer(playerId_, user); - } - - ~GameHarness() - { - qDeleteAll(ownedCards_); - ownedCards_.clear(); - } - - GameHarness(const GameHarness &) = delete; - GameHarness &operator=(const GameHarness &) = delete; - - [[nodiscard]] Game *game() const - { - return game_.get(); - } - - [[nodiscard]] RecordingClient *client() const - { - return client_.get(); - } - - [[nodiscard]] PlayerLogic *player() const - { - return game_->getPlayerManager()->getPlayer(playerId_); - } - - [[nodiscard]] CardZoneLogic *zone(const QString &zoneName) const - { - return player()->getZones().value(zoneName); - } - - CardState *addCardToZone(const QString &zoneName, const QString &cardName, int cardId) - { - CardZoneLogic *targetZone = zone(zoneName); - if (!targetZone) { - return nullptr; - } - - auto *card = new CardState(nullptr, targetZone, CardRef{cardName, QString()}, cardId); - // The constructor only stores the ref; this is what fills in the card - // data (table row, P/T, ...) the actions read. - card->refreshCardInfo(); - targetZone->addCard(card, false, 0); - ownedCards_.append(card); - return card; - } - - [[nodiscard]] const CommandContainer &lastCommand() const - { - return client_->sent.last(); - } - -private: - int playerId_; - std::unique_ptr client_; - std::unique_ptr game_; - QList ownedCards_; -}; - -#endif // COCKATRICE_TESTS_GAME_TEST_FIXTURE_H diff --git a/tests/game/game_test_main.cpp b/tests/game/game_test_main.cpp deleted file mode 100644 index 47b9a6b8e..000000000 --- a/tests/game/game_test_main.cpp +++ /dev/null @@ -1,126 +0,0 @@ -/** - * @file game_test_main.cpp - * - * Test entry point for the headless game-logic suite. The game library has no - * graphics dependency, so a QCoreApplication is enough; the only process-wide - * setup left is the card database, which the game logic still reaches through - * CardDatabaseManager and which therefore has to exist before the first zone is - * constructed. - */ - -#include "gtest/gtest.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ - -class GameTestPathProvider : public ICardDatabasePathProvider -{ -public: - explicit GameTestPathProvider(QString baseDir) : baseDir_(std::move(baseDir)) - { - } - - QString getCardDatabasePath() const override - { - return baseDir_ + QStringLiteral("/cards.xml"); - } - - QString getCustomCardDatabasePath() const override - { - return baseDir_ + QStringLiteral("/customsets/"); - } - - QString getTokenDatabasePath() const override - { - return baseDir_ + QStringLiteral("/tokens.xml"); - } - - QString getSpoilerCardDatabasePath() const override - { - return baseDir_ + QStringLiteral("/spoilers.xml"); - } - -private: - QString baseDir_; -}; - -bool copyFixture(const QString &fileName, const QString &targetDir) -{ - const QString source = QString::fromUtf8(GAME_TEST_DATA_DIR) + QLatin1Char('/') + fileName; - const QString target = targetDir + QLatin1Char('/') + fileName; - if (QFile::copy(source, target)) { - return true; - } - - std::fprintf(stderr, "Failed to copy test fixture %s to %s\n", qPrintable(source), qPrintable(target)); - return false; -} - -} // namespace - -int main(int argc, char **argv) -{ - ::testing::InitGoogleTest(&argc, argv); - - // gtest_discover_tests runs the binary with --gtest_list_tests while the - // build is still in progress. No test runs there, and building the card - // database would leave process-lifetime allocations that the sanitizer - // leg's default leak detection reports at exit, failing discovery. - if (::testing::GTEST_FLAG(list_tests)) { - return RUN_ALL_TESTS(); - } - - QStandardPaths::setTestModeEnabled(true); - - QTemporaryDir home; - if (!home.isValid()) { - std::fprintf(stderr, "Failed to create a temporary home directory\n"); - return EXIT_FAILURE; - } - qputenv("HOME", home.path().toLocal8Bit()); - - QCoreApplication app(argc, argv); - QLoggingCategory::setFilterRules(QStringLiteral("card_database.loading=false\n" - "card_database.loading.success_or_failure=false\n" - "settings_cache.*=false")); - - // Copy the fixtures so the database loader never writes its .cache file into - // the source tree, then point the singleton at the copy. - QTemporaryDir dataDir; - if (!dataDir.isValid() || !QDir().mkpath(dataDir.filePath(QStringLiteral("customsets")))) { - std::fprintf(stderr, "Failed to create a temporary card database directory\n"); - return EXIT_FAILURE; - } - if (!copyFixture(QStringLiteral("cards.xml"), dataDir.path()) || - !copyFixture(QStringLiteral("tokens.xml"), dataDir.path()) || - !copyFixture(QStringLiteral("spoilers.xml"), dataDir.path())) { - return EXIT_FAILURE; - } - - // Function-local static: constructed before the CardDatabase singleton below, - // so it is destroyed after it and the raw pointer stored in the manager stays - // valid for the database's lifetime. - static GameTestPathProvider pathProvider(dataDir.path()); - CardDatabaseManager::setCardDatabasePathProvider(&pathProvider); - - CardDatabase *database = CardDatabaseManager::getInstance(); - database->loadCardDatabases(); - if (database->getLoadStatus() != Ok) { - std::fprintf(stderr, "Failed to load the game test card database (status %d)\n", - static_cast(database->getLoadStatus())); - return EXIT_FAILURE; - } - - return RUN_ALL_TESTS(); -} diff --git a/tests/game/player_actions_test.cpp b/tests/game/player_actions_test.cpp deleted file mode 100644 index 11ae7338d..000000000 --- a/tests/game/player_actions_test.cpp +++ /dev/null @@ -1,119 +0,0 @@ -#include "game_test_fixture.h" - -#include "gtest/gtest.h" -#include -#include -#include -#include -#include -#include - -TEST(PlayerActionsTest, PlayCreatureSendsMoveToTable) -{ - GameHarness harness; - CardState *cat = harness.addCardToZone(ZoneNames::HAND, QStringLiteral("Cat"), 7); - ASSERT_NE(cat, nullptr); - - harness.player()->getPlayerActions()->playCard(cat, false); - - ASSERT_FALSE(harness.client()->sent.isEmpty()); - const CommandContainer &container = harness.lastCommand(); - ASSERT_EQ(container.game_command_size(), 1); - const Command_MoveCard command = container.game_command(0).GetExtension(Command_MoveCard::ext); - - EXPECT_EQ(command.start_player_id(), 1); - EXPECT_EQ(command.start_zone(), std::string(ZoneNames::HAND)); - EXPECT_EQ(command.target_player_id(), 1); - EXPECT_EQ(command.target_zone(), std::string(ZoneNames::TABLE)); - EXPECT_EQ(command.x(), -1); - EXPECT_EQ(command.y(), TableZoneLogic::tableRowToGridY(0)); - - ASSERT_EQ(command.cards_to_move().card_size(), 1); - const CardToMove &card = command.cards_to_move().card(0); - EXPECT_EQ(card.card_id(), 7); - EXPECT_TRUE(card.has_face_down()); - EXPECT_FALSE(card.face_down()); - EXPECT_TRUE(card.has_pt()); - EXPECT_EQ(card.pt(), "3/3"); - EXPECT_TRUE(card.has_tapped()); - EXPECT_FALSE(card.tapped()); -} - -TEST(PlayerActionsTest, PlayInstantSendsMoveToStack) -{ - GameHarness harness; - CardState *bolt = harness.addCardToZone(ZoneNames::HAND, QStringLiteral("Lightning Bolt"), 8); - ASSERT_NE(bolt, nullptr); - - harness.player()->getPlayerActions()->playCard(bolt, false); - - ASSERT_FALSE(harness.client()->sent.isEmpty()); - const CommandContainer &container = harness.lastCommand(); - ASSERT_EQ(container.game_command_size(), 1); - const Command_MoveCard command = container.game_command(0).GetExtension(Command_MoveCard::ext); - - EXPECT_EQ(command.target_zone(), std::string(ZoneNames::STACK)); - EXPECT_EQ(command.x(), -1); - EXPECT_EQ(command.y(), 0); - - ASSERT_EQ(command.cards_to_move().card_size(), 1); - const CardToMove &card = command.cards_to_move().card(0); - EXPECT_EQ(card.card_id(), 8); - // The stack branch deliberately leaves the other attributes unset. - EXPECT_FALSE(card.has_face_down()); - EXPECT_FALSE(card.has_pt()); - EXPECT_FALSE(card.has_tapped()); -} - -TEST(PlayerActionsTest, PlayFaceDownCreatureUsesFaceDownRow) -{ - GameHarness harness; - CardState *cat = harness.addCardToZone(ZoneNames::HAND, QStringLiteral("Cat"), 7); - ASSERT_NE(cat, nullptr); - - harness.player()->getPlayerActions()->playCard(cat, true); - - ASSERT_FALSE(harness.client()->sent.isEmpty()); - const CommandContainer &container = harness.lastCommand(); - ASSERT_EQ(container.game_command_size(), 1); - const Command_MoveCard command = container.game_command(0).GetExtension(Command_MoveCard::ext); - - EXPECT_EQ(command.target_zone(), std::string(ZoneNames::TABLE)); - EXPECT_EQ(command.x(), -1); - EXPECT_EQ(command.y(), TableZoneLogic::tableRowToGridY(2)); - - ASSERT_EQ(command.cards_to_move().card_size(), 1); - const CardToMove &card = command.cards_to_move().card(0); - EXPECT_EQ(card.card_id(), 7); - EXPECT_TRUE(card.has_face_down()); - EXPECT_TRUE(card.face_down()); - EXPECT_FALSE(card.has_pt()); - EXPECT_TRUE(card.has_tapped()); - EXPECT_FALSE(card.tapped()); -} - -TEST(PlayerActionsTest, PlayCiptCreatureSendsTapped) -{ - GameHarness harness; - CardState *bears = harness.addCardToZone(ZoneNames::HAND, QStringLiteral("Grizzly Bears"), 9); - ASSERT_NE(bears, nullptr); - - harness.player()->getPlayerActions()->playCard(bears, false); - - ASSERT_FALSE(harness.client()->sent.isEmpty()); - const CommandContainer &container = harness.lastCommand(); - ASSERT_EQ(container.game_command_size(), 1); - const Command_MoveCard command = container.game_command(0).GetExtension(Command_MoveCard::ext); - - EXPECT_EQ(command.target_zone(), std::string(ZoneNames::TABLE)); - - ASSERT_EQ(command.cards_to_move().card_size(), 1); - const CardToMove &card = command.cards_to_move().card(0); - EXPECT_EQ(card.card_id(), 9); - EXPECT_TRUE(card.has_face_down()); - EXPECT_FALSE(card.face_down()); - EXPECT_TRUE(card.has_pt()); - EXPECT_EQ(card.pt(), "2/2"); - EXPECT_TRUE(card.has_tapped()); - EXPECT_TRUE(card.tapped()); -} diff --git a/tests/game/replay_test.cpp b/tests/game/replay_test.cpp deleted file mode 100644 index 218ffdf51..000000000 --- a/tests/game/replay_test.cpp +++ /dev/null @@ -1,349 +0,0 @@ -/** - * @file replay_test.cpp - * - * Replays a synthetic GameReplay through the real dispatcher. This is the only - * place that exercises PlayerLogic::processPlayerInfo / processCardAttachment - * end to end: the game state arrives as a GAME_STATE_CHANGED event and the - * graphics side is replaced by a small materializer that mirrors what - * CardZone::onCreateCardRequested does with the logic objects. - * - * The replay is built in code rather than from a .cor file: event application - * is deterministic (the client never shuffles on its own for a replay), and a - * checked-in binary fixture would be opaque about what it asserts. - */ - -#include "gtest/gtest.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ - -ServerInfo_Card makeCard(int id, const QString &name, int x = -1, int y = -1) -{ - ServerInfo_Card card; - card.set_id(id); - card.set_name(name.toStdString()); - card.set_x(x); - card.set_y(y); - return card; -} - -ServerInfo_Zone makeZone(const char *name, - ServerInfo_Zone::ZoneType type, - bool withCoords, - const QList &cards, - int cardCount = 0) -{ - ServerInfo_Zone zone; - zone.set_name(name); - zone.set_type(type); - zone.set_with_coords(withCoords); - for (const auto &card : cards) { - zone.add_card_list()->CopyFrom(card); - } - if (cardCount > 0) { - zone.set_card_count(cardCount); - } - return zone; -} - -ServerInfo_Player makePlayer(int playerId, const QString &name, const QList &zones) -{ - ServerInfo_Player player; - ServerInfo_PlayerProperties *properties = player.mutable_properties(); - properties->set_player_id(playerId); - properties->mutable_user_info()->set_name(name.toStdString()); - properties->mutable_user_info()->set_user_level(ServerInfo_User::IsUser); - for (const auto &zone : zones) { - player.add_zone_list()->CopyFrom(zone); - } - return player; -} - -template GameEventContainer makeContainer(int playerId, const EventType &message) -{ - GameEventContainer container; - container.set_game_id(42); - - GameEvent *event = container.add_event_list(); - event->set_player_id(playerId); - event->MutableExtension(EventType::ext)->CopyFrom(message); - - return container; -} - -/** - * Stands in for the graphics side of a replay: when the logic asks for a card - * item, create the CardState and add it to the zone, copying the state fields - * the real CardItem::processCardInfo applies. - * - * The production view owns the visual item; the state itself is all a headless - * test needs, and this is the smallest thing that lets processPlayerInfo build - * a real zone. - */ -class HeadlessZoneView : public QObject -{ -public: - using QObject::QObject; - - ~HeadlessZoneView() override - { - qDeleteAll(cards_); - cards_.clear(); - } - - void attach(PlayerLogic *player) - { - for (CardZoneLogic *zone : player->getZones()) { - connect(zone, &CardZoneLogic::createCardRequested, this, - [this, zone](const ServerInfo_Card &cardInfo, bool reorganize) { - materialize(zone, cardInfo, reorganize); - }); - } - } - -private: - void materialize(CardZoneLogic *zone, const ServerInfo_Card &info, bool reorganize) - { - const CardRef cardRef{QString::fromStdString(info.name()), QString::fromStdString(info.provider_id())}; - auto *card = new CardState(nullptr, zone, cardRef, info.id()); - // The real CardItem constructor refreshes the card info before - // processCardInfo overwrites the state fields. - card->refreshCardInfo(); - - card->clearCounters(); - for (int i = 0; i < info.counter_list_size(); ++i) { - card->insertCounter(info.counter_list(i).id(), info.counter_list(i).value()); - } - card->setAttacking(info.attacking()); - card->setFaceDown(info.face_down()); - card->setPT(QString::fromStdString(info.pt())); - card->setAnnotation(QString::fromStdString(info.annotation())); - card->setColor(QString::fromStdString(info.color())); - card->setTapped(info.tapped()); - card->setDestroyOnZoneChange(info.destroy_on_zone_change()); - card->setDoesntUntap(info.doesnt_untap()); - - zone->addCard(card, reorganize, info.x(), info.y()); - cards_.append(card); - } - - QList cards_; -}; - -/** Owns a Replay built from a GameReplay and feeds its containers through it. */ -class ReplayHarness -{ -public: - explicit ReplayHarness(GameReplay replay) - : replayProto_(std::move(replay)), view_(std::make_unique()), - replay_(std::make_unique(nullptr, &replayProto_, false)) - { - QObject::connect(replay_->getPlayerManager(), &PlayerManager::playerAdded, view_.get(), - &HeadlessZoneView::attach); - } - - ReplayHarness(const ReplayHarness &) = delete; - ReplayHarness &operator=(const ReplayHarness &) = delete; - - void processAll() - { - for (const GameEventContainer &container : replayProto_.event_list()) { - replay_->getGameEventHandler()->processGameEventContainer(container, nullptr, EventProcessingOptions()); - } - } - - [[nodiscard]] Replay *replay() const - { - return replay_.get(); - } - - [[nodiscard]] PlayerLogic *player(int playerId) const - { - return replay_->getPlayerManager()->getPlayer(playerId); - } - -private: - GameReplay replayProto_; - std::unique_ptr view_; - std::unique_ptr replay_; -}; - -GameReplay makeReplay(const QList &containers) -{ - GameReplay replay; - replay.mutable_game_info()->set_game_id(42); - replay.mutable_game_info()->set_started(true); - for (const auto &container : containers) { - replay.add_event_list()->CopyFrom(container); - } - return replay; -} - -Event_GameStateChanged initialGameState() -{ - Event_GameStateChanged state; - state.set_game_started(true); - state.set_active_player_id(1); - state.set_active_phase(0); - return state; -} - -} // namespace - -TEST(ReplayTest, MaterializesGameStateZonesAndAttachments) -{ - Event_GameStateChanged state = initialGameState(); - - ServerInfo_Card cat = makeCard(7, QStringLiteral("Cat")); - // Cross-player attachment: Alice's card is attached to Bob's creature. - cat.set_attach_player_id(2); - cat.set_attach_zone(ZoneNames::TABLE); - cat.set_attach_card_id(9); - - const ServerInfo_Player alice = makePlayer(1, QStringLiteral("Alice"), - {makeZone(ZoneNames::HAND, ServerInfo_Zone::PrivateZone, false, {cat}), - makeZone(ZoneNames::TABLE, ServerInfo_Zone::PublicZone, true, - {makeCard(9, QStringLiteral("Grizzly Bears"), 2, 1)}), - makeZone(ZoneNames::DECK, ServerInfo_Zone::HiddenZone, false, {}, 2)}); - const ServerInfo_Player bob = makePlayer(2, QStringLiteral("Bob"), - {makeZone(ZoneNames::TABLE, ServerInfo_Zone::PublicZone, true, - {makeCard(9, QStringLiteral("Grizzly Bears"), 1, 0)})}); - - state.add_player_list()->CopyFrom(alice); - state.add_player_list()->CopyFrom(bob); - - ReplayHarness harness(makeReplay({makeContainer(-1, state)})); - harness.processAll(); - - PlayerLogic *aliceLogic = harness.player(1); - PlayerLogic *bobLogic = harness.player(2); - ASSERT_NE(aliceLogic, nullptr); - ASSERT_NE(bobLogic, nullptr); - - CardZoneLogic *hand = aliceLogic->getZones().value(ZoneNames::HAND); - CardZoneLogic *table = aliceLogic->getZones().value(ZoneNames::TABLE); - CardZoneLogic *deck = aliceLogic->getZones().value(ZoneNames::DECK); - ASSERT_NE(hand, nullptr); - ASSERT_NE(table, nullptr); - ASSERT_NE(deck, nullptr); - - ASSERT_EQ(hand->getCards().size(), 1); - CardState *catState = hand->getCards().first(); - EXPECT_EQ(catState->getId(), 7); - EXPECT_EQ(catState->getName(), QStringLiteral("Cat")); - - ASSERT_EQ(table->getCards().size(), 1); - CardState *host = table->getCards().first(); - EXPECT_EQ(host->getName(), QStringLiteral("Grizzly Bears")); - EXPECT_EQ(host->getGridPoint(), QPoint(2, 1)); - - // The hidden deck only carries a count, and the view materializes that many - // unknown cards. - EXPECT_EQ(deck->getCards().size(), 2); - for (const CardState *hidden : deck->getCards()) { - EXPECT_EQ(hidden->getId(), -1); - } - - // processCardAttachment links both sides after every player is materialized. - CardZoneLogic *bobTable = bobLogic->getZones().value(ZoneNames::TABLE); - ASSERT_NE(bobTable, nullptr); - ASSERT_EQ(bobTable->getCards().size(), 1); - CardState *bobHost = bobTable->getCards().first(); - EXPECT_EQ(catState->getAttachedTo(), bobHost); - ASSERT_EQ(bobHost->getAttachedCards().size(), 1); - EXPECT_EQ(bobHost->getAttachedCards().first(), catState); - // The identically numbered card on Alice's own table is a different card. - EXPECT_TRUE(host->getAttachedCards().isEmpty()); - - EXPECT_TRUE(harness.replay()->getGameState()->isGameStateKnown()); -} - -TEST(ReplayTest, AppliesMovesAttributesAndCountersInOrder) -{ - Event_GameStateChanged state = initialGameState(); - state.add_player_list()->CopyFrom(makePlayer( - 1, QStringLiteral("Alice"), - {makeZone(ZoneNames::HAND, ServerInfo_Zone::PrivateZone, false, {makeCard(7, QStringLiteral("Cat"))}), - makeZone(ZoneNames::TABLE, ServerInfo_Zone::PublicZone, true, - {makeCard(9, QStringLiteral("Grizzly Bears"), 0, TableZoneLogic::tableRowToGridY(0))}), - makeZone(ZoneNames::DECK, ServerInfo_Zone::HiddenZone, false, {}, 2)})); - - Event_MoveCard move; - move.set_card_id(7); - move.set_start_player_id(1); - move.set_start_zone(ZoneNames::HAND); - move.set_position(0); - move.set_target_player_id(1); - move.set_target_zone(ZoneNames::TABLE); - move.set_x(3); - move.set_y(TableZoneLogic::tableRowToGridY(1)); - move.set_new_card_id(7); - move.set_face_down(false); - - Event_SetCardAttr tap; - tap.set_zone_name(ZoneNames::TABLE); - tap.set_card_id(7); - tap.set_attribute(AttrTapped); - tap.set_attr_value("1"); - - Event_SetCardCounter counter; - counter.set_zone_name(ZoneNames::TABLE); - counter.set_card_id(7); - counter.set_counter_id(1); - counter.set_counter_value(2); - - ReplayHarness harness(makeReplay( - {makeContainer(-1, state), makeContainer(1, move), makeContainer(1, tap), makeContainer(1, counter)})); - harness.processAll(); - - PlayerLogic *alice = harness.player(1); - ASSERT_NE(alice, nullptr); - CardZoneLogic *hand = alice->getZones().value(ZoneNames::HAND); - CardZoneLogic *table = alice->getZones().value(ZoneNames::TABLE); - ASSERT_NE(hand, nullptr); - ASSERT_NE(table, nullptr); - - EXPECT_TRUE(hand->getCards().isEmpty()); - ASSERT_EQ(table->getCards().size(), 2); - - CardState *moved = table->getCard(7); - ASSERT_NE(moved, nullptr); - EXPECT_EQ(moved->getGridPoint(), QPoint(3, TableZoneLogic::tableRowToGridY(1))); - EXPECT_EQ(moved->getPT(), QStringLiteral("3/3")); - EXPECT_TRUE(moved->getTapped()); - EXPECT_EQ(moved->getCounters().value(1), 2); - - // The untouched creature keeps its own position and state. - CardState *bears = table->getCard(9); - ASSERT_NE(bears, nullptr); - EXPECT_EQ(bears->getGridPoint(), QPoint(0, TableZoneLogic::tableRowToGridY(0))); - EXPECT_FALSE(bears->getTapped()); -} diff --git a/tests/local_client_lifetime_test.cpp b/tests/local_client_lifetime_test.cpp deleted file mode 100644 index 95a2b1380..000000000 --- a/tests/local_client_lifetime_test.cpp +++ /dev/null @@ -1,149 +0,0 @@ -/** - * @file local_client_lifetime_test.cpp - * @brief Tests that a LocalClient survives the death of the LocalServer it talks to. - * @ingroup Tests - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// The server library refers to this global; the real applications define it in main.cpp. -RNG_Abstract *rng = nullptr; - -namespace -{ - -// GTest's main() creates no QCoreApplication, which QObject::deleteLater() needs. -const QCoreApplication &application() -{ - static int argc = 1; - static char executable[] = "local_client_lifetime_test"; - static char *argv[] = {executable, nullptr}; - static const QCoreApplication app(argc, argv); - return app; -} - -/// Collects the warnings one logging category emits while this object is alive. -/// -/// The handler is process-global and Qt does not nest handlers, so only one capture can be -/// installed at a time; GTest running one case at a time is what makes that safe. -class CategoryWarningCapture -{ -public: - explicit CategoryWarningCapture(const char *_category) : category(_category) - { - previousHandler = qInstallMessageHandler(captureMessage); - s_active = this; - } - - ~CategoryWarningCapture() - { - qInstallMessageHandler(previousHandler); - s_active = nullptr; - } - - CategoryWarningCapture(const CategoryWarningCapture &) = delete; - CategoryWarningCapture &operator=(const CategoryWarningCapture &) = delete; - - const QStringList &warnings() const - { - return capturedWarnings; - } - -private: - static void captureMessage(QtMsgType type, const QMessageLogContext &context, const QString &message) - { - if (s_active != nullptr && type == QtWarningMsg && context.category != nullptr && - std::strcmp(s_active->category, context.category) == 0) { - s_active->capturedWarnings.append(message); - } - - // Forward everything else, so a failure elsewhere in the test is still visible. - if (previousHandler != nullptr) { - previousHandler(type, context, message); - return; - } - std::fprintf(stderr, "%s\n", qPrintable(qFormatLogMessage(type, context, message))); - } - - const char *category; - QStringList capturedWarnings; - - static CategoryWarningCapture *s_active; - static QtMessageHandler previousHandler; -}; - -CategoryWarningCapture *CategoryWarningCapture::s_active = nullptr; -QtMessageHandler CategoryWarningCapture::previousHandler = nullptr; - -class LocalClientLifetimeTest : public ::testing::Test -{ -protected: - void SetUp() override - { - (void)application(); - - server = std::make_unique(); - LocalServerInterface *lsi = server->newConnection(); - ASSERT_NE(lsi, nullptr); - // Building the client logs in and joins room 0 through that interface, exactly as - // MainWindow::startLocalGame() does. - client = std::make_unique(lsi, QStringLiteral("lifetime-test-player"), - QStringLiteral("lifetime-test-client-id")); - } - - void TearDown() override - { - // Either case may already have deleted one of these. - client.reset(); - server.reset(); - } - - std::unique_ptr server; - std::unique_ptr client; -}; - -TEST_F(LocalClientLifetimeTest, SendAfterServerDeletedIsDropped) -{ - server.reset(); // also destroys the LocalServerInterface the client was built on - - CategoryWarningCapture capture("local_client"); - // A default container stands in for a command still queued when the local game ended. - client->sendCommandContainer(CommandContainer()); - - ASSERT_EQ(capture.warnings().size(), 1); - EXPECT_TRUE(capture.warnings().first().contains(QStringLiteral("dropping command"))) - << capture.warnings().first().toStdString(); - - // The surviving client is intact: reading its state must not touch the deleted interface. - EXPECT_EQ(client->getUserName(), QStringLiteral("lifetime-test-player")); -} - -TEST_F(LocalClientLifetimeTest, DestroyingClientAfterServerIsSafe) -{ - server.reset(); - - CategoryWarningCapture capture("local_client"); - client.reset(); // must neither crash nor touch the destroyed interface - - // The destructor itself must have been silent. - EXPECT_TRUE(capture.warnings().isEmpty()); - - // A working capture proves the emptiness above is real rather than a broken handler. - qCWarning(LocalClientLog) << "local_client_lifetime_test capture self-check"; - EXPECT_EQ(capture.warnings().size(), 1); -} - -} // namespace diff --git a/tests/model_contract/CMakeLists.txt b/tests/model_contract/CMakeLists.txt deleted file mode 100644 index 3251c1443..000000000 --- a/tests/model_contract/CMakeLists.txt +++ /dev/null @@ -1,17 +0,0 @@ -cockatrice_add_test( - NAME - model_contract_test - SOURCES - ${VERSION_STRING_CPP} - model_contract_test.cpp - LIBS - libcockatrice_models - libcockatrice_card - libcockatrice_deck_list - libcockatrice_settings - INCLUDE_DIRS - ${CMAKE_CURRENT_SOURCE_DIR}/../support - ${CMAKE_CURRENT_SOURCE_DIR}/../carddatabase - DEFINITIONS - CARDDB_DATADIR="${CMAKE_SOURCE_DIR}/tests/carddatabase/data/" -) diff --git a/tests/model_contract/model_contract_test.cpp b/tests/model_contract/model_contract_test.cpp deleted file mode 100644 index aefa26f3d..000000000 --- a/tests/model_contract/model_contract_test.cpp +++ /dev/null @@ -1,137 +0,0 @@ -#include "libcockatrice/deck_list/deck_list.h" -#include "libcockatrice/deck_list/tree/inner_deck_list_node.h" -#include "libcockatrice/models/database/card_database_display_model.h" -#include "libcockatrice/models/database/card_database_model.h" -#include "libcockatrice/models/database/card_set/card_sets_model.h" -#include "libcockatrice/models/deck_list/deck_list_model.h" -#include "model_contract_checker.h" -#include "test_card_database_path_provider.h" - -#include "gtest/gtest.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ -QSharedPointer makeDeck() -{ - return QSharedPointer(new DeckList()); -} - -void loadCardDatabase(CardDatabase &database) -{ - database.loadCardDatabases(); - ASSERT_EQ(database.getLoadStatus(), Ok); -} - -TEST(ModelContract, DeckListModelKeepsContractWhileZonesAndCardsChange) -{ - const QSharedPointer deck = makeDeck(); - DeckListModel model(nullptr, deck); - - test_support::ModelContractChecker checker(&model); - - ASSERT_NE(deck->getTree()->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr); - model.rebuildTree(); - - CardInfoPtr bolt = CardInfo::newInstance("Lightning Bolt"); - const QModelIndex added = model.addCard(ExactCard(bolt), DECK_ZONE_MAIN); - ASSERT_TRUE(added.isValid()); - - CardInfoPtr bears = CardInfo::newInstance("Grizzly Bears"); - bears->setProperty(Mtg::MainCardType, "Creature"); - const QModelIndex grouped = model.addCard(ExactCard(bears), "Removal"); - ASSERT_TRUE(grouped.isValid()); - - ASSERT_TRUE(model.removeRows(grouped.row(), 1, grouped.parent())); -} - -TEST(ModelContract, DeckListModelEmitsCardAddedSignal) -{ - const QSharedPointer deck = makeDeck(); - DeckListModel model(nullptr, deck); - QSignalSpy spy(&model, &DeckListModel::cardAddedAt); - - const QModelIndex added = model.addCard(ExactCard(CardInfo::newInstance("Lightning Bolt")), DECK_ZONE_MAIN); - - ASSERT_TRUE(added.isValid()); - EXPECT_EQ(spy.count(), 1); -} - -TEST(ModelContract, CardDatabaseModelAndDisplayProxyKeepContract) -{ - CardDatabase database(nullptr, new NoopCardPreferenceProvider(), new TestCardDatabasePathProvider(), - new NoopCardSetPriorityController()); - ASSERT_NO_FATAL_FAILURE(loadCardDatabase(database)); - - CardDatabaseModel source(&database, false); - test_support::ModelContractChecker sourceChecker(&source); - - CardDatabaseDisplayModel display; - display.setSourceModel(&source); - test_support::ModelContractChecker displayChecker(&display); - - ASSERT_GT(source.rowCount(), 0); - EXPECT_EQ(display.rowCount(), source.rowCount()); - - // A card row is a leaf, so its index must not claim child columns. The - // tester does not check this, hence the explicit assertion. - const QModelIndex sourceCard = source.index(0, 0); - ASSERT_TRUE(sourceCard.isValid()); - EXPECT_EQ(source.columnCount(sourceCard), 0); - const QModelIndex displayCard = display.index(0, 0); - ASSERT_TRUE(displayCard.isValid()); - EXPECT_EQ(display.columnCount(displayCard), 0); - - QSignalSpy dirtySpy(&display, &CardDatabaseDisplayModel::modelDirty); - display.setCardName("Do"); - ASSERT_TRUE(dirtySpy.wait()); - EXPECT_GT(display.rowCount(), 0); - EXPECT_LT(display.rowCount(), source.rowCount()); -} - -TEST(ModelContract, SetsModelKeepsContract) -{ - CardDatabase database(nullptr, new NoopCardPreferenceProvider(), new TestCardDatabasePathProvider(), - new NoopCardSetPriorityController()); - ASSERT_NO_FATAL_FAILURE(loadCardDatabase(database)); - - SetsModel model(&database); - test_support::ModelContractChecker checker(&model); - - ASSERT_GT(model.rowCount(), 0); - ASSERT_GT(model.columnCount(), 0); -} - -TEST(ModelContract, SettingsSignalIsObservableWithQSignalSpy) -{ - QTemporaryDir tempDir; - ASSERT_TRUE(tempDir.isValid()); - DownloadSettings settings(tempDir.path() + "/", nullptr); - QSignalSpy spy(&settings, &DownloadSettings::downloadSpoilerStatusChanged); - - settings.setDownloadSpoilerStatus(true); - - ASSERT_EQ(spy.count(), 1); -} -} // namespace - -int main(int argc, char **argv) -{ - QCoreApplication app(argc, argv); - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/tests/password_hash_test.cpp b/tests/password_hash_test.cpp index 8e7eded72..99f2e1400 100644 --- a/tests/password_hash_test.cpp +++ b/tests/password_hash_test.cpp @@ -52,17 +52,6 @@ TEST(PasswordHashTest, DeriveKeyMatchesKnownVector) ASSERT_EQ(derived.toHex(), expected.toHex()); } -TEST(PasswordHashTest, DeriveKeyCoversTheWholeAcceptedRangeOfR) -{ - // costParamsAreSane accepts r up to 32. The memory budget handed to OpenSSL has to cover - // scrypt's XY array (256 * r bytes) on top of V and B, or derivation fails for r >= 17. - for (const int r : {17, 32}) { - const QByteArray derived = PasswordHasher::deriveKey("password", QByteArray("NaCl"), 1024, r, 1); - ASSERT_FALSE(derived.isEmpty()) << "deriveKey failed for accepted r=" << r; - ASSERT_EQ(derived.size(), SCRYPT_VERIFIER_LENGTH); - } -} - TEST(PasswordHashTest, PasswordVerifierRoundTrip) { const QString stored = PasswordHasher::generatePasswordVerifier("hunter2"); diff --git a/tests/server_forced_password_change_test.cpp b/tests/server_forced_password_change_test.cpp deleted file mode 100644 index 9717b610b..000000000 --- a/tests/server_forced_password_change_test.cpp +++ /dev/null @@ -1,234 +0,0 @@ -/** @file server_forced_password_change_test.cpp - * @brief Tests for the restricted session a forced password change leaves behind at login. - * @ingroup Tests - */ - -#include "libcockatrice/protocol/pb/response.pb.h" -#include "libcockatrice/protocol/pb/server_message.pb.h" -#include "server.h" -#include "server_database_interface.h" -#include "server_protocolhandler.h" - -#include -#include -#include -#include -#include -#include -#include - -RNG_Abstract *rng = nullptr; // referenced by the server_remote library - -namespace -{ - -class MockDatabaseInterface : public Server_DatabaseInterface -{ -public: - AuthenticationResult checkUserPassword(Server_ProtocolHandler *, - const QString &, - const QString &, - const QString &, - QString &, - int &, - bool) override - { - return PasswordChangeRequired; - } - ServerInfo_User getUserData(const QString &name, bool withId) override - { - ServerInfo_User user; - user.set_name(name.toStdString()); - user.set_user_level(ServerInfo_User::IsUser | ServerInfo_User::IsRegistered | ServerInfo_User::IsModerator); - if (withId) { - user.set_id(1); - } - return user; - } - int getNextReplayId() override - { - return 1; - } - int getNextGameId() override - { - return 1; - } - int getActiveUserCount(QString) override - { - return 0; - } -}; - -class FakeServer : public Server -{ -public: - FakeServer() - { - setDatabaseInterface(new MockDatabaseInterface()); - } -}; - -class TestHandler : public Server_ProtocolHandler -{ -public: - TestHandler(Server *_server, Server_DatabaseInterface *_db) : Server_ProtocolHandler(_server, _db) - { - } - - QString getAddress() const override - { - return {}; - } - QString getConnectionType() const override - { - return {}; - } - - Response::ResponseCode lastResponseCode = Response::RespNothing; - int lastExtendedSessionCommand = -1; - -protected: - Response::ResponseCode - processExtendedSessionCommand(int cmdType, const SessionCommand & /*cmd*/, ResponseContainer & /*rc*/) override - { - lastExtendedSessionCommand = cmdType; - return Response::RespOk; - } - - void transmitProtocolItem(const ServerMessage &item) override - { - if (item.message_type() == ServerMessage::RESPONSE) { - lastResponseCode = item.response().response_code(); - } - } -}; - -class ForcedPasswordChangeTest : public ::testing::Test -{ -protected: - FakeServer server; - TestHandler handler{&server, server.getDatabaseInterface()}; - - void login() - { - CommandContainer cont; - cont.set_cmd_id(1); - Command_Login cmd; - cmd.set_user_name("mod"); - cont.add_session_command()->MutableExtension(Command_Login::ext)->CopyFrom(cmd); - handler.processCommandContainer(cont); - } - - Response::ResponseCode sendPing() - { - CommandContainer cont; - cont.set_cmd_id(2); - cont.add_session_command()->MutableExtension(Command_Ping::ext); - handler.processCommandContainer(cont); - return handler.lastResponseCode; - } - - Response::ResponseCode sendSessionCommand() - { - CommandContainer cont; - cont.set_cmd_id(3); - cont.add_session_command()->MutableExtension(Command_ListRooms::ext); - handler.processCommandContainer(cont); - return handler.lastResponseCode; - } - - Response::ResponseCode sendRoomCommand() - { - CommandContainer cont; - cont.set_cmd_id(4); - cont.add_room_command()->MutableExtension(Command_RoomSay::ext)->set_message("hello"); - handler.processCommandContainer(cont); - return handler.lastResponseCode; - } - - Response::ResponseCode sendMixedSessionAndRoomCommand() - { - CommandContainer cont; - cont.set_cmd_id(5); - cont.add_session_command()->MutableExtension(Command_Ping::ext); - cont.add_room_command()->MutableExtension(Command_RoomSay::ext)->set_message("hello"); - handler.processCommandContainer(cont); - return handler.lastResponseCode; - } - - Response::ResponseCode sendAccountPassword() - { - CommandContainer cont; - cont.set_cmd_id(6); - Command_AccountPassword cmd; - cmd.set_old_password("temporary-password"); - cmd.set_new_password("replacement-password"); - cont.add_session_command()->MutableExtension(Command_AccountPassword::ext)->CopyFrom(cmd); - handler.processCommandContainer(cont); - return handler.lastResponseCode; - } -}; - -TEST_F(ForcedPasswordChangeTest, LoginReportsTheRequiredChange) -{ - login(); - - EXPECT_EQ(Response::RespPasswordChangeRequired, handler.lastResponseCode); - ASSERT_NE(nullptr, handler.getUserInfo()); - EXPECT_EQ("mod", QString::fromStdString(handler.getUserInfo()->name())); -} - -TEST_F(ForcedPasswordChangeTest, PendingSessionIsNotRegisteredAsAUser) -{ - login(); - // The session may change its password, but it must not take a user slot or appear in - // the session table before the client reconnects with the new password. - EXPECT_FALSE(handler.getUserInfo()->has_session_id()); - EXPECT_EQ(0, server.getUsersCount()); -} - -TEST_F(ForcedPasswordChangeTest, PendingModeratorIsNotReportedOnline) -{ - login(); - server.addClient(&handler); - - EXPECT_TRUE(server.getOnlineModeratorList().isEmpty()); -} - -TEST_F(ForcedPasswordChangeTest, OnlyPingAndPasswordChangeAreServed) -{ - login(); - - EXPECT_EQ(Response::RespOk, sendPing()); - EXPECT_EQ(Response::RespFunctionNotAllowed, sendSessionCommand()); - EXPECT_EQ(Response::RespFunctionNotAllowed, sendRoomCommand()); - // Dispatch prefers game/room over session, so a container carrying both must not slip past - // the restriction by carrying a session command. - EXPECT_EQ(Response::RespFunctionNotAllowed, sendMixedSessionAndRoomCommand()); -} - -TEST_F(ForcedPasswordChangeTest, AccountPasswordReachesTheCommand) -{ - login(); - // The forced change is useless if the gate refuses the command it needs. ACCOUNT_PASSWORD is - // served by the extended-command hook, so it must arrive there instead of being filtered. - EXPECT_EQ(Response::RespOk, sendAccountPassword()); - EXPECT_EQ(SessionCommand::ACCOUNT_PASSWORD, handler.lastExtendedSessionCommand); -} - -TEST_F(ForcedPasswordChangeTest, CompletedLoginIsStillReportedOnline) -{ - TestHandler loggedIn{&server, server.getDatabaseInterface()}; - ServerInfo_User user; - user.set_name("online-mod"); - user.set_user_level(ServerInfo_User::IsUser | ServerInfo_User::IsRegistered | ServerInfo_User::IsModerator); - user.set_session_id(7); - loggedIn.setUserInfo(user); - server.addClient(&loggedIn); - - const QList moderators = server.getOnlineModeratorList(); - ASSERT_EQ(1, moderators.size()); - EXPECT_EQ("online-mod", moderators.first()); -} - -} // namespace diff --git a/tests/server_game_join_test.cpp b/tests/server_game_join_test.cpp index 93895ab5c..8a2ae8c45 100644 --- a/tests/server_game_join_test.cpp +++ b/tests/server_game_join_test.cpp @@ -4,7 +4,6 @@ */ #include "game/game_config.h" -#include "game/role_info.h" #include "game/server_game.h" #include "libcockatrice/protocol/pb/response.pb.h" #include "server.h" @@ -120,13 +119,13 @@ TEST_F(GameJoinOverrideTest, StaffBypassPasswordRestriction) game = makeGame(true, false, false, true); // A plain user cannot override the password even with the override flag set. - EXPECT_EQ(game->checkJoin(&plainUser, "wrong", {}, true), Response::RespWrongPassword); + EXPECT_EQ(game->checkJoin(&plainUser, "wrong", false, true, false), Response::RespWrongPassword); // Moderators and judges may enter any game regardless of the password. - EXPECT_EQ(game->checkJoin(&moderator, "wrong", {}, true), Response::RespOk); - EXPECT_EQ(game->checkJoin(&judge, "wrong", {}, true), Response::RespOk); + EXPECT_EQ(game->checkJoin(&moderator, "wrong", false, true, false), Response::RespOk); + EXPECT_EQ(game->checkJoin(&judge, "wrong", false, true, false), Response::RespOk); // Without the override flag judges are still subject to the password. - EXPECT_EQ(game->checkJoin(&judge, "wrong", {.isJudge = true}, false), Response::RespWrongPassword); - EXPECT_EQ(game->checkJoin(&judge, "secret", {.isJudge = true}, false), Response::RespOk); + EXPECT_EQ(game->checkJoin(&judge, "wrong", false, false, true), Response::RespWrongPassword); + EXPECT_EQ(game->checkJoin(&judge, "secret", false, false, true), Response::RespOk); } TEST_F(GameJoinOverrideTest, StaffBypassRegisteredOnlyRestriction) @@ -134,9 +133,9 @@ TEST_F(GameJoinOverrideTest, StaffBypassRegisteredOnlyRestriction) game = makeGame(false, true, false, true); // Without the override flag the only-registered restriction still applies. - EXPECT_EQ(game->checkJoin(&unregisteredJudge, QString(), {}, false), Response::RespUserLevelTooLow); + EXPECT_EQ(game->checkJoin(&unregisteredJudge, QString(), false, false, false), Response::RespUserLevelTooLow); // An unregistered judge may enter when overriding restrictions. - EXPECT_EQ(game->checkJoin(&unregisteredJudge, QString(), {}, true), Response::RespOk); + EXPECT_EQ(game->checkJoin(&unregisteredJudge, QString(), false, true, false), Response::RespOk); } TEST_F(GameJoinOverrideTest, StaffBypassBuddiesOnlyRestriction) @@ -144,10 +143,10 @@ TEST_F(GameJoinOverrideTest, StaffBypassBuddiesOnlyRestriction) game = makeGame(false, false, true, true); // A plain user who is not on the creator's buddy list gets rejected. - EXPECT_EQ(game->checkJoin(&plainUser, QString(), {}, true), Response::RespOnlyBuddies); + EXPECT_EQ(game->checkJoin(&plainUser, QString(), false, true, false), Response::RespOnlyBuddies); // Moderators and judges bypass the buddies-only restriction. - EXPECT_EQ(game->checkJoin(&moderator, QString(), {}, true), Response::RespOk); - EXPECT_EQ(game->checkJoin(&judge, QString(), {}, true), Response::RespOk); + EXPECT_EQ(game->checkJoin(&moderator, QString(), false, true, false), Response::RespOk); + EXPECT_EQ(game->checkJoin(&judge, QString(), false, true, false), Response::RespOk); } TEST_F(GameJoinOverrideTest, StaffBypassSpectatorsNotAllowedRestriction) @@ -155,11 +154,11 @@ TEST_F(GameJoinOverrideTest, StaffBypassSpectatorsNotAllowedRestriction) game = makeGame(false, false, false, false); // A plain user cannot spectate when the game disallows spectators. - EXPECT_EQ(game->checkJoin(&plainUser, QString(), {.isSpectator = true}, false), Response::RespSpectatorsNotAllowed); + EXPECT_EQ(game->checkJoin(&plainUser, QString(), true, false, false), Response::RespSpectatorsNotAllowed); // Moderators and judges may spectate any game regardless of the password // and the spectator restriction. - EXPECT_EQ(game->checkJoin(&moderator, "wrong", {.isSpectator = true}, true), Response::RespOk); - EXPECT_EQ(game->checkJoin(&judge, "wrong", {.isSpectator = true}, true), Response::RespOk); + EXPECT_EQ(game->checkJoin(&moderator, "wrong", true, true, false), Response::RespOk); + EXPECT_EQ(game->checkJoin(&judge, "wrong", true, true, false), Response::RespOk); } TEST_F(GameJoinOverrideTest, JudgeOverrideDoesNotGrantJudgeJoinToPlainUser) @@ -167,8 +166,8 @@ TEST_F(GameJoinOverrideTest, JudgeOverrideDoesNotGrantJudgeJoinToPlainUser) game = makeGame(false, false, false, true); // joining with join_as_judge still requires the judge flag even when overriding. - EXPECT_EQ(game->checkJoin(&plainUser, QString(), {.isJudge = true}, true), Response::RespUserLevelTooLow); - EXPECT_EQ(game->checkJoin(&judge, QString(), {.isJudge = true}, true), Response::RespOk); + EXPECT_EQ(game->checkJoin(&plainUser, QString(), false, true, true), Response::RespUserLevelTooLow); + EXPECT_EQ(game->checkJoin(&judge, QString(), false, true, true), Response::RespOk); } } // namespace diff --git a/tests/settings/CMakeLists.txt b/tests/settings/CMakeLists.txt index 9dbda9d6b..98bed1f4b 100644 --- a/tests/settings/CMakeLists.txt +++ b/tests/settings/CMakeLists.txt @@ -1,7 +1,5 @@ cockatrice_add_test(NAME settings_manager_test SOURCES settings_manager_test.cpp LIBS libcockatrice_settings) -cockatrice_add_test(NAME servers_settings_test SOURCES servers_settings_test.cpp LIBS libcockatrice_settings) - cockatrice_add_test( NAME settings_defaults_test diff --git a/tests/settings/servers_settings_test.cpp b/tests/settings/servers_settings_test.cpp deleted file mode 100644 index 5d5953999..000000000 --- a/tests/settings/servers_settings_test.cpp +++ /dev/null @@ -1,228 +0,0 @@ -#include "gtest/gtest.h" -#include -#include -#include -#include - -namespace -{ -class ServersSettingsTest : public ::testing::Test -{ -protected: - QTemporaryDir tempDir; - QString settingsPath; - - void SetUp() override - { - settingsPath = tempDir.path() + "/"; - } - - QString settingsFile() const - { - return settingsPath + "servers.ini"; - } - - void writeLegacyServer(int index, - const QString &saveName, - const QString &host, - const QString &port, - const QString &username, - const QString &password, - bool savePassword, - const QString &site) - { - QSettings settings(settingsFile(), QSettings::IniFormat); - const QString prefix = QStringLiteral("server/server_details/"); - settings.setValue(prefix + QStringLiteral("saveName%1").arg(index), saveName); - settings.setValue(prefix + QStringLiteral("server%1").arg(index), host); - settings.setValue(prefix + QStringLiteral("port%1").arg(index), port); - settings.setValue(prefix + QStringLiteral("username%1").arg(index), username); - settings.setValue(prefix + QStringLiteral("password%1").arg(index), password); - settings.setValue(prefix + QStringLiteral("savePassword%1").arg(index), savePassword); - settings.setValue(prefix + QStringLiteral("site%1").arg(index), site); - } -}; - -TEST_F(ServersSettingsTest, AddServerStoresAllFields) -{ - ServersSettings settings(settingsPath, nullptr); - settings.addNewServer("Alpha", "alpha.example", "4748", "alice", "secret", true, "https://alpha.example"); - - const auto servers = settings.getServers(); - ASSERT_EQ(servers.size(), 1); - EXPECT_EQ(servers.first().saveName, QString("Alpha")); - EXPECT_EQ(servers.first().host, QString("alpha.example")); - EXPECT_EQ(servers.first().port, QString("4748")); - EXPECT_EQ(servers.first().username, QString("alice")); - EXPECT_EQ(servers.first().password, QString("secret")); - EXPECT_TRUE(servers.first().savePassword); - EXPECT_EQ(servers.first().site, QString("https://alpha.example")); -} - -TEST_F(ServersSettingsTest, AddExistingHostAndPortUpdatesInsteadOfDuplicating) -{ - ServersSettings settings(settingsPath, nullptr); - settings.addNewServer("Alpha", "alpha.example", "4748", "alice", "secret", true, "site-a"); - settings.addNewServer("Renamed", "alpha.example", "4748", "bob", "new-secret", true, "site-b"); - - const auto servers = settings.getServers(); - ASSERT_EQ(servers.size(), 1); - EXPECT_EQ(servers.first().saveName, QString("Renamed")); - EXPECT_EQ(servers.first().username, QString("bob")); - EXPECT_EQ(servers.first().password, QString("new-secret")); - EXPECT_EQ(servers.first().site, QString("site-b")); -} - -TEST_F(ServersSettingsTest, RemoveServerRemovesOnlyMatchingHostAndKeepsOrder) -{ - ServersSettings settings(settingsPath, nullptr); - settings.addNewServer("Alpha", "alpha.example", "4748", "alice", "a", true, ""); - settings.addNewServer("Beta", "beta.example", "4748", "bob", "b", true, ""); - settings.addNewServer("Gamma", "gamma.example", "4748", "carol", "c", true, ""); - - settings.removeServer("beta.example"); - - const auto servers = settings.getServers(); - ASSERT_EQ(servers.size(), 2); - EXPECT_EQ(servers.at(0).saveName, QString("Alpha")); - EXPECT_EQ(servers.at(1).saveName, QString("Gamma")); -} - -TEST_F(ServersSettingsTest, RemovingLastServerDropsItsStaleArraySlot) -{ - ServersSettings settings(settingsPath, nullptr); - settings.addNewServer("Alpha", "alpha.example", "4748", "alice", "a", true, ""); - settings.addNewServer("Beta", "beta.example", "4748", "bob", "b", true, ""); - settings.addNewServer("Gamma", "gamma.example", "4748", "carol", "gamma-secret", true, ""); - - settings.removeServer("gamma.example"); - - QSettings raw(settingsFile(), QSettings::IniFormat); - EXPECT_FALSE(raw.contains("server/server_details/servers/3/password")); - EXPECT_FALSE(raw.contains("server/server_details/servers/3/saveName")); - ASSERT_EQ(settings.getServers().size(), 2); -} - -TEST_F(ServersSettingsTest, UpdateWithoutLossKeepsCredentials) -{ - ServersSettings settings(settingsPath, nullptr); - settings.addNewServer("Alpha", "alpha.example", "4748", "alice", "secret", true, ""); - - EXPECT_TRUE(settings.updateExistingServerWithoutLoss("Renamed", "alpha.example", "1234", "site")); - - const auto server = settings.getServer(settings.findServerIndex("alpha.example", "1234")); - ASSERT_TRUE(server.has_value()); - EXPECT_EQ(server->saveName, QString("Renamed")); - EXPECT_EQ(server->username, QString("alice")); - EXPECT_EQ(server->password, QString("secret")); - EXPECT_TRUE(server->savePassword); - EXPECT_EQ(server->site, QString("site")); -} - -TEST_F(ServersSettingsTest, SetServerPasswordUpdatesOnlyTheNamedProfile) -{ - ServersSettings settings(settingsPath, nullptr); - settings.addNewServer("Alpha", "alpha.example", "4748", "alice", "old", true, ""); - settings.addNewServer("Beta", "beta.example", "4748", "bob", "keep", true, ""); - - settings.setServerPassword("Beta", "new"); - - EXPECT_EQ(settings.getServer(0)->password, QString("old")); - EXPECT_EQ(settings.getServer(1)->password, QString("new")); -} - -TEST_F(ServersSettingsTest, LegacyProfilesAreMigrated) -{ - { - QSettings settings(settingsFile(), QSettings::IniFormat); - settings.setValue("server/server_details/totalServers", 2); - } - writeLegacyServer(1, "Alpha", "alpha.example", "4748", "alice", "secret", true, "site-a"); - writeLegacyServer(2, "Beta", "beta.example", "4748", "bob", "other", false, "site-b"); - - ServersSettings settings(settingsPath, nullptr); - - const auto servers = settings.getServers(); - ASSERT_EQ(servers.size(), 2); - EXPECT_EQ(servers.at(0).saveName, QString("Alpha")); - EXPECT_EQ(servers.at(0).host, QString("alpha.example")); - EXPECT_TRUE(servers.at(0).savePassword); - EXPECT_EQ(servers.at(1).saveName, QString("Beta")); - EXPECT_FALSE(servers.at(1).savePassword); - - QSettings raw(settingsFile(), QSettings::IniFormat); - EXPECT_FALSE(raw.contains("server/server_details/totalServers")); - EXPECT_FALSE(raw.contains("server/server_details/saveName1")); - EXPECT_FALSE(raw.contains("server/server_details/server1")); -} - -TEST_F(ServersSettingsTest, MigrationRunsOnce) -{ - writeLegacyServer(1, "Alpha", "alpha.example", "4748", "alice", "secret", true, ""); - { - QSettings settings(settingsFile(), QSettings::IniFormat); - settings.setValue("server/server_details/totalServers", 1); - } - - { - ServersSettings first(settingsPath, nullptr); - ASSERT_EQ(first.getServers().size(), 1); - } - - ServersSettings second(settingsPath, nullptr); - ASSERT_EQ(second.getServers().size(), 1); -} - -TEST_F(ServersSettingsTest, LegacyProfileAddedAfterMigrationIsMerged) -{ - { - ServersSettings first(settingsPath, nullptr); - first.addNewServer("Alpha", "alpha.example", "4748", "alice", "secret", true, ""); - } - - // An older build writes its own profile back in the legacy format after the - // array migration already happened. - writeLegacyServer(1, "Beta", "beta.example", "4748", "bob", "other", true, "site-b"); - // A duplicate of a profile the array already holds must not be added twice. - writeLegacyServer(2, "Alpha duplicate", "alpha.example", "4748", "eve", "dupe", true, ""); - { - QSettings settings(settingsFile(), QSettings::IniFormat); - settings.setValue("server/server_details/totalServers", 2); - } - - ServersSettings settings(settingsPath, nullptr); - - const auto servers = settings.getServers(); - ASSERT_EQ(servers.size(), 2); - EXPECT_EQ(servers.at(0).saveName, QString("Alpha")); - EXPECT_EQ(servers.at(0).password, QString("secret")); - EXPECT_EQ(servers.at(1).saveName, QString("Beta")); - EXPECT_EQ(servers.at(1).host, QString("beta.example")); - EXPECT_EQ(servers.at(1).username, QString("bob")); - EXPECT_EQ(servers.at(1).password, QString("other")); - EXPECT_TRUE(servers.at(1).savePassword); - EXPECT_EQ(servers.at(1).site, QString("site-b")); - - QSettings raw(settingsFile(), QSettings::IniFormat); - EXPECT_FALSE(raw.contains("server/server_details/totalServers")); - EXPECT_FALSE(raw.contains("server/server_details/saveName1")); - EXPECT_FALSE(raw.contains("server/server_details/server2")); - - // The merge is persisted, so a further start adds nothing. - ServersSettings again(settingsPath, nullptr); - ASSERT_EQ(again.getServers().size(), 2); -} - -TEST_F(ServersSettingsTest, OutOfRangeIndexReturnsNullopt) -{ - ServersSettings settings(settingsPath, nullptr); - EXPECT_FALSE(settings.getServer(-1).has_value()); - EXPECT_FALSE(settings.getServer(0).has_value()); -} -} // namespace - -int main(int argc, char **argv) -{ - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/tests/support/model_contract_checker.h b/tests/support/model_contract_checker.h deleted file mode 100644 index 9b493ff6e..000000000 --- a/tests/support/model_contract_checker.h +++ /dev/null @@ -1,35 +0,0 @@ -#ifndef COCKATRICE_TEST_MODEL_CONTRACT_CHECKER_H -#define COCKATRICE_TEST_MODEL_CONTRACT_CHECKER_H - -#include -#include -#include - -namespace test_support -{ -/** - * @brief Keeps a QAbstractItemModelTester alive for the lifetime of a scope. - * - * The tester only observes the model while it exists, so construct this before - * the mutations a test wants to validate and keep it in scope until the end. - * Fatal reporting mode aborts on the first contract violation; a warning would - * let the test pass while printing a line nobody reads in CI. - */ -class ModelContractChecker -{ -public: - explicit ModelContractChecker(QAbstractItemModel *model) - : tester( - std::make_unique(model, QAbstractItemModelTester::FailureReportingMode::Fatal)) - { - } - - ModelContractChecker(const ModelContractChecker &) = delete; - ModelContractChecker &operator=(const ModelContractChecker &) = delete; - -private: - std::unique_ptr tester; -}; -} // namespace test_support - -#endif // COCKATRICE_TEST_MODEL_CONTRACT_CHECKER_H