diff --git a/.ci/Arch/Dockerfile b/.ci/Arch/Dockerfile index b08e568f3..f37315262 100644 --- a/.ci/Arch/Dockerfile +++ b/.ci/Arch/Dockerfile @@ -8,7 +8,6 @@ RUN pacman --sync --refresh --sysupgrade --needed --noconfirm \ gtest \ mariadb-libs \ ninja \ - openssl \ protobuf \ qt6-base \ qt6-declarative \ diff --git a/.ci/Debian12/Dockerfile b/.ci/Debian12/Dockerfile index e3df94ab5..0fa227d6f 100644 --- a/.ci/Debian12/Dockerfile +++ b/.ci/Debian12/Dockerfile @@ -15,7 +15,6 @@ RUN apt-get update && \ libprotobuf-dev \ libqt6multimedia6 \ libqt6sql6-mysql \ - libssl-dev \ ninja-build \ protobuf-compiler \ qt6-image-formats-plugins \ diff --git a/.ci/Debian13/Dockerfile b/.ci/Debian13/Dockerfile index 60e490c98..13e8b35c7 100644 --- a/.ci/Debian13/Dockerfile +++ b/.ci/Debian13/Dockerfile @@ -16,7 +16,6 @@ RUN apt-get update && \ libprotobuf-dev \ libqt6multimedia6 \ libqt6sql6-mysql \ - libssl-dev \ ninja-build \ protobuf-compiler \ qt6-image-formats-plugins \ diff --git a/.ci/Fedora43/Dockerfile b/.ci/Fedora43/Dockerfile index 4005bbf67..68e894543 100644 --- a/.ci/Fedora43/Dockerfile +++ b/.ci/Fedora43/Dockerfile @@ -7,7 +7,6 @@ RUN dnf install -y \ git \ mariadb-devel \ ninja-build \ - openssl-devel \ protobuf-devel \ qt6-{qtdeclarative,qtshadertools,qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \ qt6-qtimageformats \ diff --git a/.ci/Fedora44/Dockerfile b/.ci/Fedora44/Dockerfile index e0224cdc6..ffd7c1b9b 100644 --- a/.ci/Fedora44/Dockerfile +++ b/.ci/Fedora44/Dockerfile @@ -7,7 +7,6 @@ RUN dnf install -y \ git \ mariadb-devel \ ninja-build \ - openssl-devel \ protobuf-devel \ qt6-{qtdeclarative,qtshadertools,qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \ qt6-qtimageformats \ diff --git a/.ci/Servatrice_Debian12/Dockerfile b/.ci/Servatrice_Debian12/Dockerfile index 321aa7c0f..21f6a036e 100644 --- a/.ci/Servatrice_Debian12/Dockerfile +++ b/.ci/Servatrice_Debian12/Dockerfile @@ -12,7 +12,6 @@ RUN apt-get update && \ libmariadb-dev-compat \ libprotobuf-dev \ libqt6sql6-mysql \ - libssl-dev \ ninja-build \ protobuf-compiler \ qt6-tools-dev \ diff --git a/.ci/Ubuntu24.04/Dockerfile b/.ci/Ubuntu24.04/Dockerfile index 10adc5e64..12320c276 100644 --- a/.ci/Ubuntu24.04/Dockerfile +++ b/.ci/Ubuntu24.04/Dockerfile @@ -15,7 +15,6 @@ RUN apt-get update && \ libprotobuf-dev \ libqt6multimedia6 \ libqt6sql6-mysql \ - libssl-dev \ ninja-build \ protobuf-compiler \ qt6-image-formats-plugins \ diff --git a/.ci/Ubuntu26.04/Dockerfile b/.ci/Ubuntu26.04/Dockerfile index 1b6cf825f..ce3d9cd6c 100644 --- a/.ci/Ubuntu26.04/Dockerfile +++ b/.ci/Ubuntu26.04/Dockerfile @@ -16,7 +16,6 @@ RUN apt-get update && \ libprotobuf-dev \ libqt6multimedia6 \ libqt6sql6-mysql \ - libssl-dev \ ninja-build \ protobuf-compiler \ qt6-image-formats-plugins \ diff --git a/.ci/compile.sh b/.ci/compile.sh index bd8c900c8..8a16d3243 100755 --- a/.ci/compile.sh +++ b/.ci/compile.sh @@ -149,9 +149,6 @@ if [[ $MAKE_TEST ]]; then fi if [[ $USE_CCACHE ]]; then flags+=("-DUSE_CCACHE=1") - # PCH-aware caching is required or ccache refuses to cache any TU that - # consumes a precompiled header, silently recompiling everything on every run. - ccache --set-config sloppiness=pch_defines,time_macros if [[ $CCACHE_SIZE ]]; then # note, this setting persists after running the script ccache --max-size "$CCACHE_SIZE" diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index 6ca634389..04037a74e 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -152,7 +152,7 @@ jobs: env: CACHE: ${{ github.workspace }}/.cache/${{ matrix.distro }}${{ matrix.version }} # directory for caching docker image and ccache CCACHE_EVICTION_AGE: 7d - CCACHE_SIZE: 600M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy + CCACHE_SIZE: 550M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy CMAKE_GENERATOR: 'Ninja' NAME: ${{ matrix.distro }}${{ matrix.version }} @@ -176,12 +176,8 @@ jobs: shell: bash run: | source .ci/docker.sh - args=() - [[ $GITHUB_REF == "refs/heads/master" ]] && args+=(--evict-ccache "$CCACHE_EVICTION_AGE") - args+=(--ccache "$CCACHE_SIZE") - args+=(--cmake-generator "$CMAKE_GENERATOR") - - RUN --server --debug --test "${args[@]}" + RUN --server --debug --test --ccache "$CCACHE_SIZE" \ + --cmake-generator "$CMAKE_GENERATOR" - name: "Build release package" id: build @@ -342,7 +338,7 @@ jobs: timeout-minutes: 100 env: CCACHE_DIR: ${{ github.workspace }}/.cache/ - CCACHE_SIZE: 600M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy + CCACHE_SIZE: 550M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy steps: - name: "Checkout" diff --git a/CMakeLists.txt b/CMakeLists.txt index 0da073464..bac46c2bc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,11 +5,11 @@ # This file sets all the variables shared between the projects # like the installation path, compilation flags etc.. -# 3.16 required for Qt6 and target_precompile_headers() -cmake_minimum_required(VERSION 3.16) +# cmake 3.16 is required if using qt6 +cmake_minimum_required(VERSION 3.10) # Use compiler cache (ccache) -option(USE_CCACHE "Cache the build results with ccache" ON) +option(USE_CCACHE "Cache the build results with ccache" OFF) # Treat warnings as errors (Debug builds only) option(WARNING_AS_ERROR "Treat warnings as errors in debug builds" ON) # Check for translation updates @@ -39,24 +39,13 @@ else() ) endif() -# ccache does not support MSVC and must not auto-engage on Windows -# (it is installed unintentionally on the Windows CI runner). -# NOTE: this keys off the target OS, so a mingw/Ninja configuration on Windows -# also opts out of ccache even though the GNUCXX branch below supports it. -if(USE_CCACHE AND NOT WIN32) +if(USE_CCACHE) find_program(CCACHE_PROGRAM ccache) if(CCACHE_PROGRAM) # Support Unix Makefiles and Ninja set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CCACHE_PROGRAM}") - # PCH-aware caching, matching .ci/compile.sh: without this ccache refuses - # to cache any TU that consumes a precompiled header, so every PCH-backed - # target recompiles from scratch on each build. - execute_process(COMMAND ${CCACHE_PROGRAM} --set-config sloppiness=pch_defines,time_macros) message(STATUS "Found CCache ${CCACHE_PROGRAM}") endif() -elseif(USE_CCACHE AND WIN32) - # An explicit opt-in must not disappear silently on Windows. - message(STATUS "ccache disabled: not supported for the MSVC toolchain on Windows") endif() if(WIN32 OR USE_VCPKG) @@ -195,9 +184,6 @@ elseif(CMAKE_COMPILER_IS_GNUCXX) set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${FLAG}") endif() endforeach() - - # Reduce compiler I/O by using pipes between stages instead of temp files - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pipe") else() # other: osx/llvm, bsd/llvm set(CMAKE_CXX_FLAGS_RELEASE "-O2") @@ -206,9 +192,6 @@ else() else() set(CMAKE_CXX_FLAGS_DEBUG "-g -O0 -Wall -Wextra") endif() - - # Reduce compiler I/O by using pipes between stages instead of temp files - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pipe") endif() # GNU systems need to define the Mersenne exponent for the RNG to compile w/o warning @@ -256,6 +239,11 @@ if(WIN32) find_package(OpenSSL REQUIRED) if(OPENSSL_FOUND) include_directories(${OPENSSL_INCLUDE_DIRS}) + else() + message( + WARNING + "Could not find OpenSSL runtime libraries. They are not required for compiling, but needs to be available at runtime." + ) endif() endif() diff --git a/Dockerfile b/Dockerfile index 7d3deb5fb..382309d47 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,6 @@ RUN apt-get update \ libmariadb-dev-compat \ libprotobuf-dev \ libqt6sql6-mysql \ - libssl-dev \ qt6-websockets-dev \ protobuf-compiler \ qt6-tools-dev \ @@ -43,7 +42,6 @@ RUN apt-get update \ libprotobuf32t64 \ libqt6sql6-mysql \ libqt6websockets6 \ - libssl3 \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* diff --git a/README.md b/README.md index 5935bb540..f22df461f 100644 --- a/README.md +++ b/README.md @@ -149,15 +149,15 @@ You can then
The following flags (with their non-default values) can be passed to `cmake`: -| Flag | Description | -| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | -| `-DWITH_SERVER=1` | Build Servatrice server | -| `-DWITH_CLIENT=0` | Don't build Cockatrice client | -| `-DWITH_ORACLE=0` | Don't build Oracle card database tool | -| `-DCMAKE_BUILD_TYPE=Debug` | Compile in debug mode
Enables extra logging output, debug symbols, and much more verbose compiler warnings | -| `-DWARNING_AS_ERROR=0` | Don't treat compilation warnings as errors in debug mode | -| `-DUPDATE_TRANSLATIONS=1` | Configure `make` to update the translation .ts files for new strings in the source code
**Note:** `make clean` will remove the .ts files | -| `-DTEST=1` | Enable regression tests
**Note:** `make test` to run tests, *googletest* will be downloaded if not available | +| Flag | Description | +| --- | --- | +| `-DWITH_SERVER=1` | Build Servatrice server | +| `-DWITH_CLIENT=0` | Don't build Cockatrice client | +| `-DWITH_ORACLE=0` | Don't build Oracle card database tool | +| `-DCMAKE_BUILD_TYPE=Debug` | Compile in debug mode
Enables extra logging output, debug symbols, and much more verbose compiler warnings | +| `-DWARNING_AS_ERROR=0` | Don't treat compilation warnings as errors in debug mode | +| `-DUPDATE_TRANSLATIONS=1` | Configure `make` to update the translation .ts files for new strings in the source code
**Note:** `make clean` will remove the .ts files | +| `-DTEST=1` | Enable regression tests
**Note:** `make test` to run tests, *googletest* will be downloaded if not available | # Run diff --git a/cmake/pch/qtcore_pch.h b/cmake/pch/qtcore_pch.h deleted file mode 100644 index cc3dd12ee..000000000 --- a/cmake/pch/qtcore_pch.h +++ /dev/null @@ -1,24 +0,0 @@ -/** @file qtcore_pch.h - * @brief Precompiled header for all Qt targets (Qt Core only). - * - * Safe for every target that links Qt Core, including the headless - * Servatrice binary. Keep this header free of any widget/gui types. - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include diff --git a/cmake/pch/qtwidgets_pch.h b/cmake/pch/qtwidgets_pch.h deleted file mode 100644 index 2c63f450e..000000000 --- a/cmake/pch/qtwidgets_pch.h +++ /dev/null @@ -1,30 +0,0 @@ -/** @file qtwidgets_pch.h - * @brief Precompiled header for GUI targets (Cockatrice client, Oracle). - * - * Includes the Qt Core precompiled header plus the heavy Gui, Widgets and - * Network layers that virtually every client translation unit re-parses. - * Do not use on Servatrice (headless, QT_DONT_USE_QTGUI). - */ - -#include "qtcore_pch.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 63ccc4e9c..a171bb32e 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -166,7 +166,6 @@ set(cockatrice_SOURCES src/interface/widgets/cards/additional_info/mana_cost_widget.cpp src/interface/widgets/cards/additional_info/mana_symbol_widget.cpp src/interface/widgets/cards/art_crop_attribution.cpp - src/interface/widgets/cards/card_art_utils.cpp src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp src/interface/widgets/cards/card_group_display_widgets/flat_card_group_display_widget.cpp src/interface/widgets/cards/card_group_display_widgets/overlapped_card_group_display_widget.cpp @@ -215,7 +214,6 @@ set(cockatrice_SOURCES src/interface/widgets/deck_editor/deck_editor_printing_selector_dock_widget.cpp src/interface/widgets/deck_editor/deck_list_style_proxy.cpp src/interface/widgets/deck_editor/deck_state_manager.cpp - src/interface/widgets/deck_editor/deck_zone_dialog.cpp src/interface/widgets/deck_editor/printing_disabled_info_widget.cpp src/interface/widgets/general/background_sources.cpp src/interface/widgets/general/display/background_plate_widget.cpp @@ -519,8 +517,6 @@ qt6_add_executable( MANUAL_FINALIZATION ) -target_precompile_headers(cockatrice PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtwidgets_pch.h") - qt6_add_shaders( cockatrice "onboarding_shaders" diff --git a/cockatrice/resources/help/search.md b/cockatrice/resources/help/search.md index fd0a12507..0c8bdb450 100644 --- a/cockatrice/resources/help/search.md +++ b/cockatrice/resources/help/search.md @@ -52,7 +52,6 @@ In this list of examples below, each entry has an explanation and can be clicked
Edition:
[set:lea](#set:lea) (Cards that appear in Alpha, which has the set code LEA)
[e:lea OR e:leb](#e:lea OR e:leb) (Cards that appear in Alpha or Beta)
-
[e<8ED](#e<8ED) (Cards that appear before 8th edition)
Negate:
[c:wu -c:m](#c:wu -c:m) (Any card that is white or blue, but not multicolored)
diff --git a/cockatrice/src/game/board/counter_state.cpp b/cockatrice/src/game/board/counter_state.cpp index 0970e4272..6da18b662 100644 --- a/cockatrice/src/game/board/counter_state.cpp +++ b/cockatrice/src/game/board/counter_state.cpp @@ -13,12 +13,12 @@ CounterState *CounterState::fromProto(const ServerInfo_Counter &counter, QObject convertColorToQColor(counter.counter_color()), counter.radius(), counter.count(), parent); } -void CounterState::setValue(int newValue, bool skipDamageAnimation) +void CounterState::setValue(int newValue) { if (newValue == value) { return; } int old = value; value = newValue; - emit valueChanged(old, newValue, skipDamageAnimation); + emit valueChanged(old, newValue); } \ No newline at end of file diff --git a/cockatrice/src/game/board/counter_state.h b/cockatrice/src/game/board/counter_state.h index 4c7b34473..0f2f16b55 100644 --- a/cockatrice/src/game/board/counter_state.h +++ b/cockatrice/src/game/board/counter_state.h @@ -35,23 +35,10 @@ public: return value; } - /** - * @brief Set the counter value. - * @param newValue The new value. - * @param skipDamageAnimation When true, valueChanged is emitted with skipDamageAnimation=true, letting views - * suppress damage-related feedback (e.g. battlefield shimmer, life counter flash) for values set during replay - * rewinds. - */ - void setValue(int newValue, bool skipDamageAnimation = false); + void setValue(int newValue); signals: - /** - * @brief Emitted whenever the value changes. - * @param oldValue The previous value. - * @param newValue The new value. - * @param skipDamageAnimation True when the change should not trigger damage/life-change feedback in views. - */ - void valueChanged(int oldValue, int newValue, bool skipDamageAnimation); + void valueChanged(int oldValue, int newValue); private: int id; diff --git a/cockatrice/src/game/game_event_handler.cpp b/cockatrice/src/game/game_event_handler.cpp index f146cdbb4..bc68d4d7c 100644 --- a/cockatrice/src/game/game_event_handler.cpp +++ b/cockatrice/src/game/game_event_handler.cpp @@ -430,13 +430,12 @@ void GameEventHandler::eventJoin(const Event_Join &event, int /*eventPlayerId*/, QString playerName = QString::fromStdString(playerInfo.user_info().name()); emit addPlayerToAutoCompleteList(playerName); - PlayerManager *playerManager = game->getPlayerManager(); - if (playerManager->getPlayers().contains(playerId) || playerManager->getSpectators().contains(playerId)) { + if (game->getPlayerManager()->getPlayers().contains(playerId)) { return; } if (playerInfo.spectator()) { - playerManager->addSpectator(playerId, playerInfo); + game->getPlayerManager()->addSpectator(playerId, playerInfo); emit logJoinSpectator(playerName); emit spectatorJoined(playerInfo); } else { diff --git a/cockatrice/src/game/player/event_processing_options.h b/cockatrice/src/game/player/event_processing_options.h index 06238d77e..4c7663789 100644 --- a/cockatrice/src/game/player/event_processing_options.h +++ b/cockatrice/src/game/player/event_processing_options.h @@ -13,8 +13,7 @@ enum EventProcessingOption { SKIP_REVEAL_WINDOW = 0x0001, - SKIP_TAP_ANIMATION = 0x0002, - SKIP_DAMAGE_ANIMATION = 0x0004 + SKIP_TAP_ANIMATION = 0x0002 }; // Wrap it in a QFlags typedef diff --git a/cockatrice/src/game/player/player_event_handler.cpp b/cockatrice/src/game/player/player_event_handler.cpp index 277b8b1d4..bc48298f7 100644 --- a/cockatrice/src/game/player/player_event_handler.cpp +++ b/cockatrice/src/game/player/player_event_handler.cpp @@ -262,15 +262,14 @@ void PlayerEventHandler::eventCreateCounter(const Event_CreateCounter &event) player->addCounter(event.counter_info()); } -void PlayerEventHandler::eventSetCounter(const Event_SetCounter &event, EventProcessingOptions options) +void PlayerEventHandler::eventSetCounter(const Event_SetCounter &event) { CounterState *ctr = player->getCounters().value(event.counter_id(), nullptr); if (!ctr) { return; } int oldValue = ctr->getValue(); - const bool skipDamageAnimation = options.testFlag(SKIP_DAMAGE_ANIMATION); - ctr->setValue(event.value(), skipDamageAnimation); + ctr->setValue(event.value()); emit logSetCounter(player, ctr->getName(), event.value(), oldValue); } @@ -626,7 +625,7 @@ void PlayerEventHandler::processGameEvent(GameEvent::GameEventType type, eventCreateCounter(event.GetExtension(Event_CreateCounter::ext)); break; case GameEvent::SET_COUNTER: - eventSetCounter(event.GetExtension(Event_SetCounter::ext), options); + eventSetCounter(event.GetExtension(Event_SetCounter::ext)); break; case GameEvent::DEL_COUNTER: eventDelCounter(event.GetExtension(Event_DelCounter::ext)); diff --git a/cockatrice/src/game/player/player_event_handler.h b/cockatrice/src/game/player/player_event_handler.h index 300cacd08..48ad85e88 100644 --- a/cockatrice/src/game/player/player_event_handler.h +++ b/cockatrice/src/game/player/player_event_handler.h @@ -153,7 +153,7 @@ public: void eventCreateCounter(const Event_CreateCounter &event); /// Set a player-level counter value. - void eventSetCounter(const Event_SetCounter &event, EventProcessingOptions options); + void eventSetCounter(const Event_SetCounter &event); /// Delete a player-level counter. void eventDelCounter(const Event_DelCounter &event); diff --git a/cockatrice/src/game/player/player_logic.cpp b/cockatrice/src/game/player/player_logic.cpp index 143df5c57..45ba09aac 100644 --- a/cockatrice/src/game/player/player_logic.cpp +++ b/cockatrice/src/game/player/player_logic.cpp @@ -175,15 +175,7 @@ void PlayerLogic::processPlayerInfo(const ServerInfo_Player &info) const ServerInfo_Card &cardInfo = zoneInfo.card_list(j); auto *card = new CardItem(this); card->processCardInfo(cardInfo); - // Zones without coordinates (hand, piles, stack) preserve the order - // they arrive in on the server in the positions of their cards list. - // The x coordinate of such cards is always 0, so inserting at it - // would reverse the list on reconnect. Append instead. - if (zoneInfo.with_coords()) { - zone->addCard(card, false, cardInfo.x(), cardInfo.y()); - } else { - zone->addCard(card, false, -1); - } + zone->addCard(card, false, cardInfo.x(), cardInfo.y()); } } if (zoneInfo.has_always_reveal_top_card()) { diff --git a/cockatrice/src/game/player/player_manager.cpp b/cockatrice/src/game/player/player_manager.cpp index 8486efbeb..6772d3ff1 100644 --- a/cockatrice/src/game/player/player_manager.cpp +++ b/cockatrice/src/game/player/player_manager.cpp @@ -75,14 +75,6 @@ PlayerLogic *PlayerManager::getPlayer(int playerId) const return player; } -void PlayerManager::clearSpectators() -{ - const QList spectatorIds = spectators.keys(); - for (int spectatorId : spectatorIds) { - removeSpectator(spectatorId); - } -} - void PlayerManager::onPlayerConceded(int playerId, bool conceded) { // Everything else cares about this diff --git a/cockatrice/src/game/player/player_manager.h b/cockatrice/src/game/player/player_manager.h index 504e65396..2f8b87af8 100644 --- a/cockatrice/src/game/player/player_manager.h +++ b/cockatrice/src/game/player/player_manager.h @@ -100,9 +100,6 @@ public: emit spectatorRemoved(spectatorId, spectatorInfo); } - /** @brief Remove all spectators, emitting the removal signal for each. */ - void clearSpectators(); - [[nodiscard]] AbstractGame *getGame() const { return game; diff --git a/cockatrice/src/game_graphics/board/abstract_counter.cpp b/cockatrice/src/game_graphics/board/abstract_counter.cpp index 4ba04804f..e63117e13 100644 --- a/cockatrice/src/game_graphics/board/abstract_counter.cpp +++ b/cockatrice/src/game_graphics/board/abstract_counter.cpp @@ -29,9 +29,9 @@ AbstractCounter::AbstractCounter(CounterState *state, { setAcceptHoverEvents(true); - connect(state, &CounterState::valueChanged, this, [this](int oldValue, int newValue, bool skipDamageAnimation) { + connect(state, &CounterState::valueChanged, this, [this](int oldValue, int newValue) { value = newValue; - onValueChanged(oldValue, newValue, skipDamageAnimation); + onValueChanged(oldValue, newValue); update(); }); @@ -230,7 +230,7 @@ void AbstractCounterDialog::changeValue(int diff) setTextValue(QString::number(curValue)); } -void AbstractCounter::onValueChanged(int /*oldValue*/, int /*newValue*/, bool /*skipDamageAnimation*/) +void AbstractCounter::onValueChanged(int /*oldValue*/, int /*newValue*/) { // Default: no feedback. Subclasses such as PlayerCounter override this to // flash the counter on meaningful changes (life gain/loss). diff --git a/cockatrice/src/game_graphics/board/abstract_counter.h b/cockatrice/src/game_graphics/board/abstract_counter.h index 67b5b4074..9ddcc6d58 100644 --- a/cockatrice/src/game_graphics/board/abstract_counter.h +++ b/cockatrice/src/game_graphics/board/abstract_counter.h @@ -39,9 +39,8 @@ protected: * @brief Hook for subclasses that need per-value-change feedback (e.g. life-total flash). * * Called whenever the counter's value changes, before the item repaints. - * @param skipDamageAnimation True when damage-related feedback should be suppressed (replay rewinds). */ - virtual void onValueChanged(int oldValue, int newValue, bool skipDamageAnimation); + virtual void onValueChanged(int oldValue, int newValue); void mousePressEvent(QGraphicsSceneMouseEvent *event) override; void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override; diff --git a/cockatrice/src/game_graphics/board/card_item.cpp b/cockatrice/src/game_graphics/board/card_item.cpp index c2dc455cc..c40c8c214 100644 --- a/cockatrice/src/game_graphics/board/card_item.cpp +++ b/cockatrice/src/game_graphics/board/card_item.cpp @@ -316,7 +316,7 @@ void CardItem::drawAttachArrow() for (const auto &item : scene()->selectedItems()) { CardItem *card = qgraphicsitem_cast(item); - if (card == nullptr || card == this) { + if (card == nullptr) { continue; } if (card->getZone() != state->getZone()) { diff --git a/cockatrice/src/game_graphics/deckview/deck_view.cpp b/cockatrice/src/game_graphics/deckview/deck_view.cpp index 1acd02a75..1278737a0 100644 --- a/cockatrice/src/game_graphics/deckview/deck_view.cpp +++ b/cockatrice/src/game_graphics/deckview/deck_view.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include DeckViewCardDragItem::DeckViewCardDragItem(DeckViewCard *_item, @@ -380,10 +381,12 @@ void DeckViewScene::rebuildTree() addItem(container); } - // Cards in custom zones nested under a board are regular board cards in-game. - // They are collected recursively (like every other consumer) and reported with - // the top-level board zone as their origin, so that sideboard plans keep working. - for (auto *currentCard : deck->getCardNodes({currentZone->getName()})) { + for (int j = 0; j < currentZone->size(); j++) { + auto *currentCard = dynamic_cast(currentZone->at(j)); + if (!currentCard) { + continue; + } + for (int k = 0; k < currentCard->getNumber(); ++k) { auto *newCard = new DeckViewCard(container, currentCard->toCardRef(), currentZone->getName()); container->addCard(newCard); diff --git a/cockatrice/src/game_graphics/game_scene.cpp b/cockatrice/src/game_graphics/game_scene.cpp index 17af7618b..87af4c73c 100644 --- a/cockatrice/src/game_graphics/game_scene.cpp +++ b/cockatrice/src/game_graphics/game_scene.cpp @@ -44,16 +44,11 @@ GameScene::GameScene(PhasesToolbar *_phasesToolbar, QObject *parent) GameScene::~GameScene() { - // Sever all destroyed->removeAnimatedItem connections before the members below - // are destroyed: the base QGraphicsScene destructor destroys the remaining items, - // and their destroyed() signals must not reach slots that reference members that - // no longer exist. The connection handle overload is used because the string-based - // disconnect(nullptr, nullptr, this, nullptr) is invalid (the sender must never be - // nullptr) and would otherwise fail to sever these pointer-to-member connections. - for (auto it = animationItemConnections.constBegin(); it != animationItemConnections.constEnd(); ++it) { - QObject::disconnect(*it); - } - animationItemConnections.clear(); + // Sever all incoming connections (animated item destroy-tracking) before the + // members below are destroyed: the base QGraphicsScene destructor destroys the + // remaining items, and their destroyed() signals must not reach slots that + // reference members that no longer exist. + QObject::disconnect(nullptr, nullptr, this, nullptr); delete animationTimer; animationTimer = nullptr; @@ -221,12 +216,7 @@ void GameScene::removePlayer(PlayerLogic *player) clearArrowsForPlayer(player->getPlayerInfo()->getId()); - // Closing a view removes it from zoneViews synchronously, so iterate over a - // copy: otherwise a player with several open views (e.g. library and hand) - // only has the first one closed here and the remaining views are left - // pointing at a player that is about to be deleted. - const QList zoneViewCopy = zoneViews; - for (ZoneViewWidget *zone : zoneViewCopy) { + for (ZoneViewWidget *zone : zoneViews) { if (zone->getPlayer() == player) { zone->close(); } @@ -669,10 +659,7 @@ CardItem *GameScene::findTopmostCardInZone(const QList &items, */ void GameScene::toggleZoneView(PlayerLogic *player, const QString &zoneName, int numberCards, bool isReversed) { - // Closing a view removes it from zoneViews synchronously, so iterate over a - // copy to make sure every already-open matching view is closed. - const QList zoneViewCopy = zoneViews; - for (auto *view : zoneViewCopy) { + for (auto &view : zoneViews) { ZoneViewZone *temp = view->getZone(); if (temp->getLogic()->getName() == zoneName && temp->getLogic()->getPlayer() == player && qobject_cast(temp->getLogic())->getNumberCards() == numberCards) { @@ -790,15 +777,8 @@ void GameScene::registerAnimationItem(IAnimatedItem *item) if (!object) { return; } - // Guard against duplicate connections using the connection map, not - // animatedItems: the animation timer removes entries from animatedItems when an - // animation completes, but the destroyed->removeAnimatedItem connection must - // persist until the object is destroyed. Relying on animatedItems here would let - // a re-registered item (e.g. a life counter that flashes repeatedly) accumulate - // duplicate destroyed connections, the older ones of which would survive teardown. - if (!animationItemConnections.contains(object)) { - animationItemConnections.insert(object, - connect(object, &QObject::destroyed, this, &GameScene::removeAnimatedItem)); + if (!animatedItems.contains(object)) { + connect(object, &QObject::destroyed, this, &GameScene::removeAnimatedItem); } animatedItems.insert(object, item); if (animationTimer && !animationTimer->isActive()) { @@ -817,7 +797,6 @@ void GameScene::unregisterAnimationItem(IAnimatedItem *item) void GameScene::removeAnimatedItem(QObject *item) { animatedItems.remove(item); - animationItemConnections.remove(item); if (animationTimer && animatedItems.isEmpty()) { animationTimer->stop(); } diff --git a/cockatrice/src/game_graphics/game_scene.h b/cockatrice/src/game_graphics/game_scene.h index 859d7a6eb..c12696189 100644 --- a/cockatrice/src/game_graphics/game_scene.h +++ b/cockatrice/src/game_graphics/game_scene.h @@ -54,11 +54,9 @@ private: QPointer hoveredCard; ///< Currently hovered card QBasicTimer *animationTimer; ///< Timer for scene animations QHash animatedItems; ///< Items currently animating - QHash - animationItemConnections; ///< destroyed->removeAnimatedItem handles per animated item - int playerRotation; ///< Rotation offset for player layout - bool rearranging = false; ///< Guard against re-entrant rearrange - bool needsReArrange = false; ///< Pending rearrange requested during a pass + int playerRotation; ///< Rotation offset for player layout + bool rearranging = false; ///< Guard against re-entrant rearrange + bool needsReArrange = false; ///< Pending rearrange requested during a pass /** * @brief Updates which card is currently hovered based on scene coordinates. diff --git a/cockatrice/src/game_graphics/player/menu/tally_menu.cpp b/cockatrice/src/game_graphics/player/menu/tally_menu.cpp index 08cb6cac9..7eb3945b3 100644 --- a/cockatrice/src/game_graphics/player/menu/tally_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/tally_menu.cpp @@ -12,13 +12,11 @@ TallyMenu::TallyMenu() aTallyNone = createTallyAction(TallyType::None); aTallySubtypes = createTallyAction(TallyType::Subtypes); aTallyTotalPower = createTallyAction(TallyType::TotalPower); - aTallyTotalToughness = createTallyAction(TallyType::TotalToughness); addAction(aTallyNone); addSeparator(); addAction(aTallySubtypes); addAction(aTallyTotalPower); - addAction(aTallyTotalToughness); retranslateUi(); } @@ -56,5 +54,4 @@ void TallyMenu::retranslateUi() aTallyNone->setText(tr("None")); aTallySubtypes->setText(tr("Subtypes")); aTallyTotalPower->setText(tr("Total Power")); - aTallyTotalToughness->setText(tr("Total Toughness")); } diff --git a/cockatrice/src/game_graphics/player/menu/tally_menu.h b/cockatrice/src/game_graphics/player/menu/tally_menu.h index 11802fd20..acd1daf67 100644 --- a/cockatrice/src/game_graphics/player/menu/tally_menu.h +++ b/cockatrice/src/game_graphics/player/menu/tally_menu.h @@ -24,7 +24,6 @@ private: QAction *aTallyNone = nullptr; QAction *aTallySubtypes = nullptr; QAction *aTallyTotalPower = nullptr; - QAction *aTallyTotalToughness = nullptr; QAction *createTallyAction(TallyType tallyType); }; diff --git a/cockatrice/src/game_graphics/player/player_graphics_item.cpp b/cockatrice/src/game_graphics/player/player_graphics_item.cpp index 122ab83be..8bf2703e1 100644 --- a/cockatrice/src/game_graphics/player/player_graphics_item.cpp +++ b/cockatrice/src/game_graphics/player/player_graphics_item.cpp @@ -3,7 +3,6 @@ #include "../../game/player/player_actions.h" #include "../../interface/card_picture_loader/card_picture_loader.h" #include "../../interface/widgets/cards/art_crop_attribution.h" -#include "../../interface/widgets/cards/card_art_utils.h" #include "../../interface/widgets/playmat/playmat_utils.h" #include "../../interface/widgets/tabs/tab_game.h" #include "../board/abstract_card_item.h" @@ -252,8 +251,8 @@ void PlayerGraphicsItem::onCounterAdded(CounterState *state) AbstractCounter *widget; if (state->getName() == "life") { widget = playerTarget->addCounter(state); - connect(state, &CounterState::valueChanged, this, [this](int oldValue, int newValue, bool skipDamageAnimation) { - if (newValue < oldValue && !skipDamageAnimation) { + connect(state, &CounterState::valueChanged, this, [this](int oldValue, int newValue) { + if (newValue < oldValue) { tableZoneGraphicsItem->triggerDamageShimmer(); } }); @@ -443,7 +442,7 @@ void PlayerGraphicsItem::updatePlaymat() hasPlaymat = true; emit playmatChanged(true); } - playmatPixmap = CardArtUtils::rotateSidewaysLayoutArt(fullRes, card); + playmatPixmap = fullRes; update(); } diff --git a/cockatrice/src/game_graphics/player/player_list_widget.cpp b/cockatrice/src/game_graphics/player/player_list_widget.cpp index 13a077af8..4268e1019 100644 --- a/cockatrice/src/game_graphics/player/player_list_widget.cpp +++ b/cockatrice/src/game_graphics/player/player_list_widget.cpp @@ -92,11 +92,6 @@ void PlayerListWidget::retranslateUi() void PlayerListWidget::addPlayer(const ServerInfo_PlayerProperties &player) { - if (players.contains(player.player_id())) { - updatePlayerProperties(player); - return; - } - QTreeWidgetItem *newPlayer = new PlayerListTWI; players.insert(player.player_id(), newPlayer); updatePlayerProperties(player); @@ -181,17 +176,6 @@ void PlayerListWidget::removePlayer(int playerId) delete takeTopLevelItem(indexOfTopLevelItem(player)); } -void PlayerListWidget::clearSpectators() -{ - const QList playerIds = players.keys(); - for (int playerId : playerIds) { - QTreeWidgetItem *player = players.value(playerId, 0); - if (player && !player->data(1, Qt::UserRole).toBool()) { - removePlayer(playerId); - } - } -} - void PlayerListWidget::setActivePlayer(int playerId) { QMapIterator i(players); diff --git a/cockatrice/src/game_graphics/player/player_list_widget.h b/cockatrice/src/game_graphics/player/player_list_widget.h index f2f0be5fd..a53cfa989 100644 --- a/cockatrice/src/game_graphics/player/player_list_widget.h +++ b/cockatrice/src/game_graphics/player/player_list_widget.h @@ -66,7 +66,6 @@ public slots: void addPlayer(const ServerInfo_PlayerProperties &player); void removePlayer(int playerId); void updatePlayerProperties(const ServerInfo_PlayerProperties &prop, int playerId = -1); - void clearSpectators(); }; #endif diff --git a/cockatrice/src/game_graphics/player/player_target.cpp b/cockatrice/src/game_graphics/player/player_target.cpp index d6c28370d..910ee9c17 100644 --- a/cockatrice/src/game_graphics/player/player_target.cpp +++ b/cockatrice/src/game_graphics/player/player_target.cpp @@ -69,7 +69,7 @@ void PlayerCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * /* } } -void PlayerCounter::onValueChanged(int oldValue, int newValue, bool skipDamageAnimation) +void PlayerCounter::onValueChanged(int oldValue, int newValue) { flashDelta = newValue - oldValue; if (flashDelta == 0) { @@ -81,11 +81,6 @@ void PlayerCounter::onValueChanged(int oldValue, int newValue, bool skipDamageAn return; } - if (skipDamageAnimation) { - flashAlpha = 0.0; - return; - } - flashAlpha = 1.0; flashClock.start(); if (scene()) { @@ -137,18 +132,8 @@ void PlayerTarget::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*o QRectF translatedRect = painter->combinedTransform().mapRect(avatarBoundingRect); QSize translatedSize = translatedRect.size().toSize(); QPixmap cachedPixmap; - // The key must cover everything the generated pawn depends on: the rendered - // size, the user level, and the pixmap being drawn. fullPixmap.cacheKey() is - // 0 for every null pixmap, so the default-pawn branch additionally needs the - // pawn's privlevel (lowercased, matching UserLevelPixmapGenerator) and colors - // in the key — otherwise two players without a custom avatar (and the same - // user level) would share one cached pawn. const QString cacheKey = "avatar" + QString::number(translatedSize.width()) + "_" + - QString::number(translatedSize.height()) + "_" + QString::number(info->user_level()) + - "_" + QString::number(fullPixmap.cacheKey()) + "_" + - QString::fromStdString(info->privlevel()).toLower() + "_" + - QString::fromStdString(info->pawn_colors().left_side()) + "_" + - QString::fromStdString(info->pawn_colors().right_side()); + QString::number(info->user_level()) + "_" + QString::number(fullPixmap.cacheKey()); if (!QPixmapCache::find(cacheKey, &cachedPixmap)) { cachedPixmap = QPixmap(translatedSize.width(), translatedSize.height()); diff --git a/cockatrice/src/game_graphics/player/player_target.h b/cockatrice/src/game_graphics/player/player_target.h index 1d06c6274..af0e9c8b7 100644 --- a/cockatrice/src/game_graphics/player/player_target.h +++ b/cockatrice/src/game_graphics/player/player_target.h @@ -21,7 +21,7 @@ class PlayerCounter : public AbstractCounter, public IAnimatedItem { Q_OBJECT protected: - void onValueChanged(int oldValue, int newValue, bool skipDamageAnimation) override; + void onValueChanged(int oldValue, int newValue) override; private: static constexpr qreal flashDurationMs = 450.0; diff --git a/cockatrice/src/game_graphics/tally/stats_tally.cpp b/cockatrice/src/game_graphics/tally/stats_tally.cpp index 7e05c3fb1..e7a6621fa 100644 --- a/cockatrice/src/game_graphics/tally/stats_tally.cpp +++ b/cockatrice/src/game_graphics/tally/stats_tally.cpp @@ -34,31 +34,3 @@ QList StatsTally::computeTotalPower(const QList &cards) QString name = QCoreApplication::translate("StatsTally", "Total Power"); return {TallyRow{name, QString::number(total)}}; } - -static int sumToughness(const QList &cards) -{ - int total = 0; - for (auto card : cards) { - QVariantList parsed = CardItem::parsePT(card->getPT()); - if (parsed.size() == 2) { - int toughness = parsed.at(1).toInt(); // toInt will default to 0 if it's not an int - total += qMax(toughness, 0); - } - } - return total; -} - -QList StatsTally::computeTotalToughness(const QList &cards) -{ - // don't bother if none of the cards have pt - bool hasPT = - std::any_of(cards.cbegin(), cards.cend(), [](const CardItem *card) { return !card->getPT().isEmpty(); }); - if (!hasPT) { - return {}; - } - - int total = sumToughness(cards); - - QString name = QCoreApplication::translate("StatsTally", "Total Toughness"); - return {TallyRow{name, QString::number(total)}}; -} diff --git a/cockatrice/src/game_graphics/tally/stats_tally.h b/cockatrice/src/game_graphics/tally/stats_tally.h index e499587eb..4c3d93b56 100644 --- a/cockatrice/src/game_graphics/tally/stats_tally.h +++ b/cockatrice/src/game_graphics/tally/stats_tally.h @@ -16,14 +16,6 @@ namespace StatsTally */ QList computeTotalPower(const QList &cards); -/** - * @brief Sums the toughness of all selected cards - * - * @param cards The list of selected card items to analyze. - * @return A single row containing the total, or an empty list if none of the cards have pt - */ -QList computeTotalToughness(const QList &cards); - } // namespace StatsTally #endif // COCKATRICE_STATS_TALLY_H diff --git a/cockatrice/src/game_graphics/tally/tally.cpp b/cockatrice/src/game_graphics/tally/tally.cpp index 21806ee84..aa2cae024 100644 --- a/cockatrice/src/game_graphics/tally/tally.cpp +++ b/cockatrice/src/game_graphics/tally/tally.cpp @@ -21,8 +21,6 @@ QList Tally::compute(const QList &cards, const TallyType t return SubtypeTally::countSubtypes(cards); case TallyType::TotalPower: return StatsTally::computeTotalPower(cards); - case TallyType::TotalToughness: - return StatsTally::computeTotalToughness(cards); } return {}; } diff --git a/cockatrice/src/game_graphics/tally/tally.h b/cockatrice/src/game_graphics/tally/tally.h index 84c54918f..97406cddb 100644 --- a/cockatrice/src/game_graphics/tally/tally.h +++ b/cockatrice/src/game_graphics/tally/tally.h @@ -21,8 +21,7 @@ enum class TallyType None, Subtypes, TotalPower, - TotalToughness, - MaxValue = TotalToughness // sentinel value + MaxValue = TotalPower // sentinel value }; namespace Tally diff --git a/cockatrice/src/game_graphics/zones/hand_zone.cpp b/cockatrice/src/game_graphics/zones/hand_zone.cpp index 1a8f7a910..b52a4955a 100644 --- a/cockatrice/src/game_graphics/zones/hand_zone.cpp +++ b/cockatrice/src/game_graphics/zones/hand_zone.cpp @@ -41,8 +41,7 @@ void HandZone::handleDropEvent(const QList &dragItems, } } } else { - bool sameZone = startZone == getLogic(); - x = calcDropIndexFromY(dropPoint.y(), !sameZone); + x = calcDropIndexFromY(dropPoint.y()); } Command_MoveCard cmd; diff --git a/cockatrice/src/game_graphics/zones/select_zone.cpp b/cockatrice/src/game_graphics/zones/select_zone.cpp index 470c70fcf..c58c41b92 100644 --- a/cockatrice/src/game_graphics/zones/select_zone.cpp +++ b/cockatrice/src/game_graphics/zones/select_zone.cpp @@ -83,7 +83,7 @@ SelectZone::StackLayoutParams SelectZone::buildStackParams(qreal minOffset) cons return {cardCount, boundingRect().height(), cardHeight, offset, minOffset}; } -int SelectZone::calcDropIndexFromY(qreal dropY, bool allowCountExpand, qreal minOffset) const +int SelectZone::calcDropIndexFromY(qreal dropY, qreal minOffset) const { const auto &cards = getLogic()->getCards(); if (cards.isEmpty()) { @@ -94,8 +94,7 @@ int SelectZone::calcDropIndexFromY(qreal dropY, bool allowCountExpand, qreal min if (effectiveOffset <= 0.0) { return 0; } - int max = allowCountExpand ? params.cardCount : params.cardCount - 1; - return qBound(0, qRound((dropY - start) / effectiveOffset), max); + return qBound(0, qRound((dropY - start) / effectiveOffset), params.cardCount - 1); } void SelectZone::restoreStaleEscapedCards() diff --git a/cockatrice/src/game_graphics/zones/select_zone.h b/cockatrice/src/game_graphics/zones/select_zone.h index b5d3ca37a..7408f29b6 100644 --- a/cockatrice/src/game_graphics/zones/select_zone.h +++ b/cockatrice/src/game_graphics/zones/select_zone.h @@ -104,12 +104,8 @@ protected: /** * @brief Computes the card index at a given y-coordinate within the zone's vertical layout. * Returns 0 if the zone has no cards or the offset is zero. - * - * @param dropY The y-coordinate that the card was dropped at - * @param allowCountExpand If false, clamps the index at the number of cards minus 1 - * @param minOffset Minimum offset to preserve */ - int calcDropIndexFromY(qreal dropY, bool allowCountExpand, qreal minOffset = 0.0) const; + int calcDropIndexFromY(qreal dropY, qreal minOffset = 0.0) const; /** * @brief Positions cards vertically with alternating left/right x-offsets. diff --git a/cockatrice/src/game_graphics/zones/stack_zone.cpp b/cockatrice/src/game_graphics/zones/stack_zone.cpp index ff62097c7..e9b14f13d 100644 --- a/cockatrice/src/game_graphics/zones/stack_zone.cpp +++ b/cockatrice/src/game_graphics/zones/stack_zone.cpp @@ -57,14 +57,18 @@ void StackZone::handleDropEvent(const QList &dragItems, return; } - bool sameZone = startZone == getLogic(); - int index = calcDropIndexFromY(dropPoint.y(), !sameZone, MIN_CARD_VISIBLE); - if (sameZone) { + const auto &cards = getLogic()->getCards(); + int index; + if (startZone == getLogic()) { + // Reordering within the zone: use drop position + index = calcDropIndexFromY(dropPoint.y(), MIN_CARD_VISIBLE); // Same-zone no-op: don't move a card onto itself - const auto &cards = getLogic()->getCards(); if (!cards.isEmpty() && cards.at(index)->getId() == dragItems.at(0)->getId()) { return; } + } else { + // Coming from another zone: append at end (top of stack, rendered on top) + index = static_cast(cards.size()); } Command_MoveCard cmd; 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 7daafb610..2f46e7941 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp @@ -138,8 +138,7 @@ void CardPictureLoader::getPixmap(QPixmap &pixmap, const ExactCard &card, QSize QPixmap bigPixmap; if (QPixmapCache::find(key, &bigPixmap)) { if (bigPixmap.isNull()) { - // Leave the pixmap null so callers fall back to a solid color - // instead of showing the card back. + getCardBackLoadingFailedPixmap(pixmap, size); QDateTime failedAtTime = getInstance().failedAt.value(key); if (!failedAtTime.isValid() || failedAtTime.addSecs(RETRY_FAILED_CARDS_SECS) < QDateTime::currentDateTime()) { diff --git a/cockatrice/src/interface/deck_loader/deck_loader.cpp b/cockatrice/src/interface/deck_loader/deck_loader.cpp index f03339da8..39a0c1071 100644 --- a/cockatrice/src/interface/deck_loader/deck_loader.cpp +++ b/cockatrice/src/interface/deck_loader/deck_loader.cpp @@ -375,32 +375,15 @@ void DeckLoader::saveToStream_DeckHeader(QTextStream &out, const DeckList &deckL void DeckLoader::saveToStream_DeckZone(QTextStream &out, const InnerDecklistNode *zoneNode, bool addComments, - bool addSetNameAndNumber, - const QString &boardZoneName) + bool addSetNameAndNumber) { - // Nested sub-zones keep their owning board's identity: the top-level call - // passes no board, so the zone's own name is used; recursive calls carry the - // owning board down so the sideboard marker survives sub-zone nesting. - const QString owningBoardZoneName = boardZoneName.isEmpty() ? zoneNode->getName() : boardZoneName; - // group cards by card type and count the subtotals QMultiMap cardsByType; QMap cardTotalByType; int cardTotal = 0; - QList subZones; for (int j = 0; j < zoneNode->size(); j++) { auto *card = dynamic_cast(zoneNode->at(j)); - if (!card) { - // Cards collected in nested sub-zones are exported by recursion so - // they don't end up invisible in the plain text output. They are - // deferred until after this zone's own header and cards so they read - // as part of this zone's block. - if (auto *subZone = dynamic_cast(zoneNode->at(j))) { - subZones.append(subZone); - } - continue; - } CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(card->getName()); QString cardType = info ? info->getMainCardType() : "unknown"; @@ -428,30 +411,25 @@ void DeckLoader::saveToStream_DeckZone(QTextStream &out, QList cards = cardsByType.values(cardType); - saveToStream_DeckZoneCards(out, cards, addComments, addSetNameAndNumber, owningBoardZoneName); + saveToStream_DeckZoneCards(out, zoneNode, cards, addComments, addSetNameAndNumber); if (addComments) { out << "\n"; } } - - // Nested sub-zones come last, after the parent's own header and cards. - for (const auto *subZone : subZones) { - saveToStream_DeckZone(out, subZone, addComments, addSetNameAndNumber, owningBoardZoneName); - } } void DeckLoader::saveToStream_DeckZoneCards(QTextStream &out, + const InnerDecklistNode *zoneNode, QList cards, bool addComments, - bool addSetNameAndNumber, - const QString &boardZoneName) + bool addSetNameAndNumber) { // QMultiMap sorts values in reverse order for (int i = cards.size() - 1; i >= 0; --i) { DecklistCardNode *card = cards[i]; - if (boardZoneName == DECK_ZONE_SIDE && addComments) { + if (zoneNode->getName() == DECK_ZONE_SIDE && addComments) { out << "SB: "; } @@ -532,26 +510,9 @@ bool DeckLoader::convertToCockatriceFormat(LoadedDeck &deck) void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode *node) { - if (!node || node->isEmpty()) { - return; - } - const int totalColumns = 2; - // Dispatch children by type instead of trusting a whole-node height: a deck - // node may hold direct cards and nested zones side by side (custom zones), - // and an empty node would previously crash on at(0). - QVector cards; - QVector subZones; - for (int i = 0; i < node->size(); i++) { - if (auto *card = dynamic_cast(node->at(i))) { - cards.append(card); - } else if (auto *zone = dynamic_cast(node->at(i))) { - subZones.append(zone); - } - } - - if (!cards.isEmpty()) { + if (node->height() == 1) { QTextBlockFormat blockFormat; QTextCharFormat charFormat; charFormat.setFontPointSize(11); @@ -562,9 +523,9 @@ void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode tableFormat.setCellPadding(0); tableFormat.setCellSpacing(0); tableFormat.setBorder(0); - QTextTable *table = cursor->insertTable(cards.size() + 1, totalColumns, tableFormat); - for (int i = 0; i < cards.size(); i++) { - const AbstractDecklistCardNode *card = cards[i]; + QTextTable *table = cursor->insertTable(node->size() + 1, totalColumns, tableFormat); + for (int i = 0; i < node->size(); i++) { + auto *card = dynamic_cast(node->at(i)); QTextCharFormat cellCharFormat; cellCharFormat.setFontPointSize(9); @@ -579,13 +540,7 @@ void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode cellCursor = cell.firstCursorPosition(); cellCursor.insertText(card->getName()); } - } - - for (const InnerDecklistNode *subZone : subZones) { - if (subZone->isEmpty()) { - continue; - } - + } else if (node->height() == 2) { QTextBlockFormat blockFormat; QTextCharFormat charFormat; charFormat.setFontPointSize(14); @@ -604,8 +559,10 @@ void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode tableFormat.setColumnWidthConstraints(constraints); QTextTable *table = cursor->insertTable(1, totalColumns, tableFormat); - QTextCursor cellCursor = table->cellAt(0, 0).firstCursorPosition(); - printDeckListNode(&cellCursor, subZone); + for (int i = 0; i < node->size(); i++) { + QTextCursor cellCursor = table->cellAt(0, (i * totalColumns) / node->size()).lastCursorPosition(); + printDeckListNode(&cellCursor, dynamic_cast(node->at(i))); + } } cursor->movePosition(QTextCursor::End); diff --git a/cockatrice/src/interface/deck_loader/deck_loader.h b/cockatrice/src/interface/deck_loader/deck_loader.h index b851c6895..ac23e1ee0 100644 --- a/cockatrice/src/interface/deck_loader/deck_loader.h +++ b/cockatrice/src/interface/deck_loader/deck_loader.h @@ -159,13 +159,12 @@ private: static void saveToStream_DeckZone(QTextStream &out, const InnerDecklistNode *zoneNode, bool addComments = true, - bool addSetNameAndNumber = true, - const QString &boardZoneName = QString()); + bool addSetNameAndNumber = true); static void saveToStream_DeckZoneCards(QTextStream &out, + const InnerDecklistNode *zoneNode, QList cards, bool addComments = true, - bool addSetNameAndNumber = true, - const QString &boardZoneName = QString()); + bool addSetNameAndNumber = true); }; #endif diff --git a/cockatrice/src/interface/pixel_map_generator.cpp b/cockatrice/src/interface/pixel_map_generator.cpp index 674f3dd4c..d7d67c6bf 100644 --- a/cockatrice/src/interface/pixel_map_generator.cpp +++ b/cockatrice/src/interface/pixel_map_generator.cpp @@ -14,7 +14,7 @@ #define DEFAULT_COLOR_MODERATOR_LEFT "#ffffff"; #define DEFAULT_COLOR_MODERATOR_RIGHT "#000000"; #define DEFAULT_COLOR_ADMIN "#ff2701"; -#define DEFAULT_COLOR_DEVELOPER "#B8B8B8" +#define DEFAULT_COLOR_DEVELOPER "#800020" /** * Clamps an svg render size so that rendering does not exceed a multiple of the requested size. diff --git a/cockatrice/src/interface/widgets/cards/card_art_utils.cpp b/cockatrice/src/interface/widgets/cards/card_art_utils.cpp deleted file mode 100644 index b26b73593..000000000 --- a/cockatrice/src/interface/widgets/cards/card_art_utils.cpp +++ /dev/null @@ -1,18 +0,0 @@ -#include "card_art_utils.h" - -#include -#include - -namespace CardArtUtils -{ -QPixmap rotateSidewaysLayoutArt(const QPixmap &art, const ExactCard &card) -{ - if (!card.getInfo().getUiAttributes().landscapeOrientation) { - return art; - } - - QTransform transform; - transform.rotate(90); - return art.transformed(transform, Qt::SmoothTransformation); -} -} // namespace CardArtUtils \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/cards/card_art_utils.h b/cockatrice/src/interface/widgets/cards/card_art_utils.h deleted file mode 100644 index 5c331a12c..000000000 --- a/cockatrice/src/interface/widgets/cards/card_art_utils.h +++ /dev/null @@ -1,25 +0,0 @@ -#ifndef CARD_ART_UTILS_H -#define CARD_ART_UTILS_H - -#include - -class ExactCard; - -namespace CardArtUtils -{ -/** - * @brief Rotates a card's art upright when its layout shows sideways. - * - * Sideways-layout cards (planes, sieges/battles, split cards) store their - * landscape artwork rotated 90° inside a portrait frame. Art-crop displays, - * playmat art, and the card-info picture must show such art upright before - * sampling or painting. Portrait cards are returned unchanged. - * - * @param art The card pixmap to orient. - * @param card The card describing the art orientation. - * @return @p art rotated 90° clockwise for sideways-layout cards, else @p art. - */ -QPixmap rotateSidewaysLayoutArt(const QPixmap &art, const ExactCard &card); -} // namespace CardArtUtils - -#endif // CARD_ART_UTILS_H \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp b/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp index bfbdd7e42..3f36e559c 100644 --- a/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp @@ -174,18 +174,16 @@ void CardGroupDisplayWidget::updateCardDisplays() QModelIndex sourceIndex = proxy.mapToSource(proxyIndex); // 4. persist the source index - addCardWidgets(QPersistentModelIndex(sourceIndex)); - } -} + QPersistentModelIndex persistent(sourceIndex); -void CardGroupDisplayWidget::addCardWidgets(const QPersistentModelIndex &persistent) -{ - // Get the card amount - int cardAmount = persistent.sibling(persistent.row(), DeckListModelColumns::CARD_AMOUNT).data(Qt::EditRole).toInt(); + // Get the card amount + int cardAmount = + sourceIndex.sibling(sourceIndex.row(), DeckListModelColumns::CARD_AMOUNT).data(Qt::EditRole).toInt(); - // Create multiple widgets for the card count - for (int copy = 0; copy < cardAmount; ++copy) { - addToLayout(constructWidgetForIndex(persistent)); + // Create multiple widgets for the card count + for (int copy = 0; copy < cardAmount; ++copy) { + addToLayout(constructWidgetForIndex(persistent)); + } } } diff --git a/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.h b/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.h index a3bf70981..2308ccf8d 100644 --- a/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.h +++ b/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.h @@ -35,7 +35,6 @@ public: void onSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected); void refreshSelectionForIndex(const QPersistentModelIndex &persistent); void clearAllDisplayWidgets(); - void addCardWidgets(const QPersistentModelIndex &persistent); DeckListModel *deckListModel; QItemSelectionModel *selectionModel; 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 de622bdc8..79ae087d7 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp @@ -5,7 +5,6 @@ #include "../../../interface/card_picture_loader/card_picture_loader.h" #include "../../../interface/widgets/tabs/tab_supervisor.h" #include "../../window_main.h" -#include "card_art_utils.h" #include #include @@ -194,7 +193,12 @@ void CardInfoPictureWidget::paintEvent(QPaintEvent *event) QPixmap transformedPixmap = resizedPixmap; // Default pixmap if (SettingsCache::instance().cardsDisplay().getAutoRotateSidewaysLayoutCards()) { - transformedPixmap = CardArtUtils::rotateSidewaysLayoutArt(resizedPixmap, exactCard); + if (exactCard.getInfo().getUiAttributes().landscapeOrientation) { + // Rotate pixmap 90 degrees to the left + QTransform transform; + transform.rotate(90); + transformedPixmap = resizedPixmap.transformed(transform, Qt::SmoothTransformation); + } } // Handle DPI scaling diff --git a/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.cpp b/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.cpp index b00d9db1e..eaf3a67b0 100644 --- a/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.cpp @@ -5,7 +5,6 @@ #include "libcockatrice/card/database/card_database_manager.h" #include -#include #include DeckCardZoneDisplayWidget::DeckCardZoneDisplayWidget(QWidget *parent, @@ -52,6 +51,11 @@ DeckCardZoneDisplayWidget::DeckCardZoneDisplayWidget(QWidget *parent, // User Interaction // ===================================================================================================================== +void DeckCardZoneDisplayWidget::onClick(QMouseEvent *event, const ExactCard &card) +{ + emit cardClicked(event, card, zoneName); +} + void DeckCardZoneDisplayWidget::onHover(const ExactCard &card) { emit cardHovered(card); @@ -91,18 +95,12 @@ void DeckCardZoneDisplayWidget::constructAppropriateWidget(QPersistentModelIndex } auto categoryName = index.sibling(index.row(), DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); - // Cards in a custom zone belong to that zone, not the board zone, so that - // increment/decrement/swap actions target the custom zone. - const bool isCustomZone = index.data(DeckRoles::IsCustomZoneRole).toBool(); - const QString effectiveZoneName = isCustomZone ? categoryName : zoneName; - const auto routeCardClick = [this, effectiveZoneName](QMouseEvent *event, const ExactCard &card) { - emit cardClicked(event, card, effectiveZoneName); - }; if (displayType == DisplayType::Overlap) { auto *displayWidget = new OverlappedCardGroupDisplayWidget( - cardGroupContainer, deckListModel, selectionModel, index, effectiveZoneName, categoryName, - activeGroupCriteria, activeSortCriteria, subBannerOpacity, cardSizeWidget); - connect(displayWidget, &OverlappedCardGroupDisplayWidget::cardClicked, this, routeCardClick); + cardGroupContainer, deckListModel, selectionModel, index, zoneName, categoryName, activeGroupCriteria, + activeSortCriteria, subBannerOpacity, cardSizeWidget); + connect(displayWidget, &OverlappedCardGroupDisplayWidget::cardClicked, this, + &DeckCardZoneDisplayWidget::onClick); connect(displayWidget, &OverlappedCardGroupDisplayWidget::cardHovered, this, &DeckCardZoneDisplayWidget::onHover); connect(displayWidget, &CardGroupDisplayWidget::cleanupRequested, this, @@ -113,9 +111,9 @@ void DeckCardZoneDisplayWidget::constructAppropriateWidget(QPersistentModelIndex indexToWidgetMap.insert(index, displayWidget); } else if (displayType == DisplayType::Flat) { auto *displayWidget = new FlatCardGroupDisplayWidget(cardGroupContainer, deckListModel, selectionModel, index, - effectiveZoneName, categoryName, activeGroupCriteria, + zoneName, categoryName, activeGroupCriteria, activeSortCriteria, subBannerOpacity, cardSizeWidget); - connect(displayWidget, &FlatCardGroupDisplayWidget::cardClicked, this, routeCardClick); + connect(displayWidget, &FlatCardGroupDisplayWidget::cardClicked, this, &DeckCardZoneDisplayWidget::onClick); connect(displayWidget, &FlatCardGroupDisplayWidget::cardHovered, this, &DeckCardZoneDisplayWidget::onHover); connect(displayWidget, &CardGroupDisplayWidget::cleanupRequested, this, &DeckCardZoneDisplayWidget::cleanupInvalidCardGroup); @@ -128,18 +126,24 @@ void DeckCardZoneDisplayWidget::constructAppropriateWidget(QPersistentModelIndex void DeckCardZoneDisplayWidget::displayCards() { - if (!trackedIndex.isValid()) { - return; - } + QSortFilterProxyModel proxy; + proxy.setSourceModel(deckListModel); + proxy.setSortRole(Qt::EditRole); + proxy.sort(DeckListModelColumns::CARD_NAME, Qt::AscendingOrder); - // Iterate the direct children of the tracked zone, keeping the tree view's row - // order (criteria groups first, then custom zones, both in the model's sort order). - QList rows; - for (int i = 0; i < deckListModel->rowCount(trackedIndex); ++i) { - rows.append(QPersistentModelIndex(deckListModel->index(i, 0, trackedIndex))); - } + // 1. trackedIndex is a source index → map it to proxy space + QModelIndex proxyParent = proxy.mapFromSource(trackedIndex); + + // 2. iterate children under the proxy parent + for (int i = 0; i < proxy.rowCount(proxyParent); ++i) { + QModelIndex proxyIndex = proxy.index(i, 0, proxyParent); + + // 3. map back to source + QModelIndex sourceIndex = proxy.mapToSource(proxyIndex); + + // 4. persist the source index + QPersistentModelIndex persistent(sourceIndex); - for (const QPersistentModelIndex &persistent : rows) { constructAppropriateWidget(persistent); } } diff --git a/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.h b/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.h index 53f3fa7cf..b426fca30 100644 --- a/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.h +++ b/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.h @@ -42,6 +42,7 @@ public: void addCardsToOverlapWidget(); public slots: + void onClick(QMouseEvent *event, const ExactCard &card); void onHover(const ExactCard &card); void cleanupInvalidCardGroup(CardGroupDisplayWidget *displayWidget); void constructAppropriateWidget(QPersistentModelIndex index); diff --git a/cockatrice/src/interface/widgets/deck_editor/card_database_view.cpp b/cockatrice/src/interface/widgets/deck_editor/card_database_view.cpp index 00388a3cd..7c782b074 100644 --- a/cockatrice/src/interface/widgets/deck_editor/card_database_view.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/card_database_view.cpp @@ -90,13 +90,6 @@ void CardDatabaseView::decrementCard(const QString &zoneName) emit cardDecremented(currentCardName(), zoneName); } -void CardDatabaseView::setZoneMenuProvider(const std::function>()> &provider, - const std::function &newZoneHandler) -{ - zoneMenuProvider = provider; - this->newZoneHandler = newZoneHandler; -} - void CardDatabaseView::updateCard(const QModelIndex ¤t, const QModelIndex & /*previous*/) { if (!current.isValid()) { @@ -149,50 +142,6 @@ void CardDatabaseView::openCustomMenu(QPoint point) [this, card] { emit cardAdded(card->getName(), DECK_ZONE_SIDE); }); connect(selectPrinting, &QAction::triggered, this, &CardDatabaseView::selectPrintingClicked); - if (zoneMenuProvider) { - QMenu *addToZoneMenu = menu.addMenu(tr("Add to Zone")); - const auto zoneBoards = zoneMenuProvider(); - for (const QString &boardName : InnerDecklistNode::boardZoneNames()) { - // Boards with zones nest their children so no two menu entries - // share a visible name: "Maindeck ▸ { Maindeck (whole board), … }". - const QStringList customZones = [&zoneBoards, boardName] { - for (const auto &zoneBoard : zoneBoards) { - if (zoneBoard.first == boardName) { - return zoneBoard.second; - } - } - return QStringList(); - }(); - if (customZones.isEmpty()) { - QAction *action = addToZoneMenu->addAction(InnerDecklistNode::visibleNameFromName(boardName)); - connect(action, &QAction::triggered, this, - [this, card, boardName] { emit cardAdded(card->getName(), boardName); }); - } else { - QMenu *boardSubmenu = addToZoneMenu->addMenu(InnerDecklistNode::visibleNameFromName(boardName)); - QAction *wholeBoardAction = boardSubmenu->addAction(InnerDecklistNode::visibleNameFromName(boardName)); - connect(wholeBoardAction, &QAction::triggered, this, - [this, card, boardName] { emit cardAdded(card->getName(), boardName); }); - for (const QString &zoneName : customZones) { - QAction *action = boardSubmenu->addAction(zoneName); - connect(action, &QAction::triggered, this, - [this, card, zoneName] { emit cardAdded(card->getName(), zoneName); }); - } - } - } - - if (newZoneHandler) { - addToZoneMenu->addSeparator(); - - QAction *newZoneAction = addToZoneMenu->addAction(tr("Create &new zone...")); - connect(newZoneAction, &QAction::triggered, this, [this, card] { - const QString zoneName = newZoneHandler(); - if (!zoneName.isEmpty()) { - emit cardAdded(card->getName(), zoneName); - } - }); - } - } - if (canBeCommander(*card)) { QAction *edhRecCommander = menu.addAction(tr("Show on EDHRec (Commander)")); connect(edhRecCommander, &QAction::triggered, this, [this, card] { emit edhrecClicked(card, true); }); diff --git a/cockatrice/src/interface/widgets/deck_editor/card_database_view.h b/cockatrice/src/interface/widgets/deck_editor/card_database_view.h index 668444199..175ec12b9 100644 --- a/cockatrice/src/interface/widgets/deck_editor/card_database_view.h +++ b/cockatrice/src/interface/widgets/deck_editor/card_database_view.h @@ -4,7 +4,6 @@ #include "../../key_signals.h" #include -#include #include class CardDatabaseModel; @@ -20,13 +19,6 @@ class CardDatabaseView : public QTreeView KeySignals searchKeySignals; CardDatabaseDisplayModel *databaseDisplayModel; - /// Provides the custom zones available in the current deck, grouped by board zone. - /// The list contains (board zone name, custom zone names) pairs for every board. - std::function>()> zoneMenuProvider; - /// Handler invoked when the user picks "New zone..." from the add-to-zone menu. - /// Returns the name of the created zone, or an empty string if creation was cancelled. - std::function newZoneHandler; - public: explicit CardDatabaseView(QWidget *parent, CardDatabaseDisplayModel *model); @@ -41,17 +33,6 @@ public: return &searchKeySignals; } - /** - * @brief Sets the provider used to populate the "Add to zone" submenu of the context menu. - * If no provider is set, the submenu is not shown. - * - * @param provider Returns the custom zones of the current deck, grouped by board zone - * @param newZoneHandler Creates a new custom zone and returns its name, or an empty string - * if creation was cancelled. The menu entry is hidden when not provided. - */ - void setZoneMenuProvider(const std::function>()> &provider, - const std::function &newZoneHandler); - signals: void cardChanged(const QString &cardName); diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp index 6269f0323..2a491de4f 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp @@ -1,12 +1,5 @@ #include "deck_editor_card_database_dock_widget.h" -#include "../../../interface/widgets/tabs/abstract_tab_deck_editor.h" -#include "card_database_view.h" -#include "deck_state_manager.h" -#include "deck_zone_dialog.h" - -#include - DeckEditorCardDatabaseDockWidget::DeckEditorCardDatabaseDockWidget(AbstractTabDeckEditor *parent) : QDockWidget(parent) { setObjectName("databaseDisplayDock"); @@ -22,27 +15,6 @@ void DeckEditorCardDatabaseDockWidget::createDatabaseDisplayDock(AbstractTabDeck { databaseDisplayWidget = new DeckEditorDatabaseDisplayWidget(this, deckEditor->databaseModel); - databaseDisplayWidget->getDatabaseView()->setZoneMenuProvider( - [deckEditor]() -> QList> { - QList> result; - auto *deckListModel = deckEditor->deckStateManager->getModel(); - for (const QString &boardName : InnerDecklistNode::boardZoneNames()) { - result.append({boardName, deckListModel->getCustomZoneNames(boardName)}); - } - return result; - }, - [this, deckEditor]() -> QString { - QString boardName; - const QString zoneName = - DeckZoneDialog::promptForNewZone(this, {}, &boardName, [deckEditor](const QString &candidate) { - return deckEditor->deckStateManager->validateNewZoneName(candidate); - }); - if (!zoneName.isEmpty()) { - deckEditor->deckStateManager->createCustomZone(boardName, zoneName); - } - return zoneName; - }); - auto *frame = new QVBoxLayout; frame->setObjectName("databaseDisplayFrame"); frame->addWidget(databaseDisplayWidget); diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp index e2175a358..14defc8e9 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp @@ -7,18 +7,15 @@ #include "../tabs/api/commander_spellbook/commander_bracket_widget.h" #include "deck_list_style_proxy.h" #include "deck_state_manager.h" -#include "deck_zone_dialog.h" #include #include #include #include -#include #include #include #include #include -#include #include #include #include @@ -775,213 +772,14 @@ void DeckEditorDeckDockWidget::offsetCountAtIndex(const QModelIndex &idx, bool i void DeckEditorDeckDockWidget::decklistCustomMenu(QPoint point) { - const QModelIndex sourceIndex = proxy->mapToSource(deckView->indexAt(point)); - QMenu menu; - const bool isCustomZoneRow = sourceIndex.isValid() && sourceIndex.data(DeckRoles::IsCustomZoneRole).toBool(); - const bool isBoardZoneRow = sourceIndex.isValid() && !isCustomZoneRow && !sourceIndex.parent().isValid(); - const bool isCardRow = - sourceIndex.isValid() && !isCustomZoneRow && !isBoardZoneRow && !getModel()->hasChildren(sourceIndex); - - // Walk the row up to its top-level node to find the hosting board. Cards in - // the tokens board cannot be moved (moveCardToZone bails for it), so the - // move menu is skipped for them. - QString currentBoardName; - QModelIndex board = sourceIndex.parent(); - while (board.isValid() && board.parent().isValid()) { - board = board.parent(); - } - if (board.isValid()) { - currentBoardName = board.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); - } - - if (isCardRow) { - if (currentBoardName != DECK_ZONE_TOKENS) { - addMoveToZoneMenu(&menu, sourceIndex, currentBoardName); - menu.addSeparator(); - } - } else if (isCustomZoneRow) { - const QString zoneName = - sourceIndex.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); - - QAction *renameAction = menu.addAction(tr("&Rename zone...")); - connect(renameAction, &QAction::triggered, this, [this, zoneName] { - // The unchanged name must not validate as a duplicate. - const QString newName = - DeckZoneDialog::promptForRename(this, zoneName, [this, zoneName](const QString &candidate) { - return candidate == zoneName ? QString() : deckStateManager->validateNewZoneName(candidate); - }); - if (!newName.isEmpty() && newName != zoneName) { - deckStateManager->renameCustomZone(zoneName, newName); - } - }); - - QMenu *boardMenu = menu.addMenu(tr("Change &board")); - addChangeBoardMenu(boardMenu, zoneName); - - QAction *deleteAction = menu.addAction(tr("&Delete zone")); - const bool zoneHasCards = getModel()->hasChildren(sourceIndex); - deleteAction->setEnabled(!zoneHasCards); - if (zoneHasCards) { - deleteAction->setToolTip(tr("Move or remove all cards first.")); - menu.setToolTipsVisible(true); - } - connect(deleteAction, &QAction::triggered, this, [this, zoneName] { - const auto result = - QMessageBox::warning(this, tr("Delete zone"), tr("Delete the zone \"%1\"?").arg(zoneName), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - if (result == QMessageBox::Yes) { - deckStateManager->removeCustomZone(zoneName); - } - }); - menu.addSeparator(); - } else if (isBoardZoneRow) { - const QString boardName = - sourceIndex.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); - // Tokens cannot host custom zones, so only offer the action on real boards. - const bool canHostCustomZones = - boardName == DECK_ZONE_MAIN || boardName == DECK_ZONE_SIDE || boardName == DECK_ZONE_MAYBEBOARD; - if (canHostCustomZones) { - addNewZoneAction(&menu, boardName); - menu.addSeparator(); - } - } else if (!sourceIndex.isValid()) { - addNewZoneAction(&menu); - menu.addSeparator(); - } - QAction *selectPrinting = menu.addAction(tr("Select Printing")); connect(selectPrinting, &QAction::triggered, deckEditor, &AbstractTabDeckEditor::showPrintingSelector); menu.exec(deckView->mapToGlobal(point)); } -void DeckEditorDeckDockWidget::addMoveToZoneMenu(QMenu *menu, - const QModelIndex &sourceCardIndex, - const QString ¤tBoardName) -{ - // The card's current *zone*, derived with the same ancestor walk as - // DeckStateManager::moveCardToZone (nearest custom-zone ancestor, else the - // top-level board/zone): a card inside "Removal" under the maindeck lives in - // "Removal", not "main". Comparing against that instead of the board keeps - // the enabled state and the same-zone no-op consistent with the move logic. - QString currentZoneName; - for (QModelIndex ancestor = sourceCardIndex.parent(); ancestor.isValid(); ancestor = ancestor.parent()) { - if (ancestor.data(DeckRoles::IsCustomZoneRole).toBool() || !ancestor.parent().isValid()) { - currentZoneName = ancestor.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); - break; - } - } - - const auto addMoveAction = [this, sourceCardIndex](QMenu *targetMenu, const QString &targetZoneName, - const QString &label, bool enabled) { - QAction *action = targetMenu->addAction(label); - action->setEnabled(enabled); - if (enabled) { - connect(action, &QAction::triggered, this, [this, sourceCardIndex, targetZoneName] { - deckStateManager->moveCardToZone(sourceCardIndex, targetZoneName); - }); - } - }; - - const auto tree = deckStateManager->getDeckListShared()->getTree(); - - QMenu *moveMenu = menu->addMenu(tr("Move to &zone")); - - for (const QString &boardName : InnerDecklistNode::boardZoneNames()) { - const QString boardLabel = InnerDecklistNode::visibleNameFromName(boardName); - const auto customZones = tree->getCustomZones(boardName); - - // Boards with zones nest their children so no two menu entries share a - // visible name: "Maindeck ▸ { Maindeck (whole board), Removal, … }". - // The board the card already lives on is marked instead of offered. - if (!customZones.isEmpty()) { - QMenu *boardSubmenu = moveMenu->addMenu(boardLabel); - addMoveAction(boardSubmenu, boardName, boardLabel, boardName != currentZoneName); - for (const auto *customZone : customZones) { - addMoveAction(boardSubmenu, customZone->getName(), customZone->getName(), - customZone->getName() != currentZoneName); - } - } else { - addMoveAction(moveMenu, boardName, boardLabel, boardName != currentZoneName); - } - } - - moveMenu->addSeparator(); - - QAction *newZoneAction = moveMenu->addAction(tr("Create new zone and move &here...")); - connect(newZoneAction, &QAction::triggered, this, [this, sourceCardIndex, currentBoardName, currentZoneName] { - // Resolve the card's identity before creating the zone: - // createNewCustomZone rebuilds the model tree, so sourceCardIndex's - // internal pointer is freed by the time it would be used. - const QString cardName = - sourceCardIndex.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); - const QString providerId = - sourceCardIndex.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data(Qt::DisplayRole).toString(); - const QString collectorNumber = sourceCardIndex.siblingAtColumn(DeckListModelColumns::CARD_COLLECTOR_NUMBER) - .data(Qt::DisplayRole) - .toString(); - - const QString zoneName = createNewCustomZone(currentBoardName); - if (!zoneName.isEmpty()) { - // Re-find the card: the old index is no longer safe since rows were - // rebuilt. Mirror DeckStateManager::decrementCard's re-find pattern. - const QModelIndex refreshed = getModel()->findCard(cardName, currentZoneName, providerId, collectorNumber); - if (refreshed.isValid()) { - deckStateManager->moveCardToZone(refreshed, zoneName); - } - } - }); -} - -void DeckEditorDeckDockWidget::addChangeBoardMenu(QMenu *menu, const QString &zoneName) -{ - const auto tree = deckStateManager->getDeckListShared()->getTree(); - for (const QString &boardName : InnerDecklistNode::boardZoneNames()) { - QAction *action = menu->addAction(InnerDecklistNode::visibleNameFromName(boardName)); - - // The board currently holding the zone is marked instead of offered. - // Duplicate names cannot come up through the editor, so this doubles as - // the uniqueness guard for imported decks. - bool holdsTheZone = false; - for (const auto *customZone : tree->getCustomZones(boardName)) { - if (customZone->getName() == zoneName) { - holdsTheZone = true; - break; - } - } - if (holdsTheZone) { - action->setCheckable(true); - action->setChecked(true); - continue; - } - - connect(action, &QAction::triggered, this, - [this, zoneName, boardName] { deckStateManager->moveCustomZone(zoneName, boardName); }); - } -} - -void DeckEditorDeckDockWidget::addNewZoneAction(QMenu *menu, const QString &initialBoardName) -{ - QAction *newZoneAction = menu->addAction(tr("Create &new zone...")); - connect(newZoneAction, &QAction::triggered, this, - [this, initialBoardName] { createNewCustomZone(initialBoardName); }); -} - -QString DeckEditorDeckDockWidget::createNewCustomZone(const QString &initialBoardName) -{ - QString boardName; - const QString zoneName = - DeckZoneDialog::promptForNewZone(this, initialBoardName, &boardName, [this](const QString &candidate) { - return deckStateManager->validateNewZoneName(candidate); - }); - if (!zoneName.isEmpty()) { - deckStateManager->createCustomZone(boardName, zoneName); - } - return zoneName; -} - void DeckEditorDeckDockWidget::refreshShortcuts() { ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts(); diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h index 1e5f4e677..9db01e2e5 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h @@ -19,7 +19,6 @@ #include #include #include -#include #include #include #include @@ -103,11 +102,6 @@ private: [[nodiscard]] QModelIndexList getSelectedCardNodeSourceIndices() const; void offsetCountAtIndex(const QModelIndex &idx, bool isIncrement); - void addMoveToZoneMenu(QMenu *menu, const QModelIndex &sourceCardIndex, const QString ¤tBoardName); - void addChangeBoardMenu(QMenu *menu, const QString &zoneName); - QString createNewCustomZone(const QString &initialBoardName = {}); - void addNewZoneAction(QMenu *menu, const QString &initialBoardName = {}); - private slots: void decklistCustomMenu(QPoint point); void updateCard(QModelIndex, const QModelIndex ¤t); diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp index e563729a4..eda741728 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp @@ -2,7 +2,6 @@ #include #include -#include DeckStateManager::DeckStateManager(QObject *parent) : QObject(parent), deckList(QSharedPointer(new DeckList)), @@ -308,170 +307,6 @@ bool DeckStateManager::decrementCountAtIndex(const QModelIndex &idx) return offsetCountAtIndex(idx, -1); } -bool DeckStateManager::moveCardToZone(const QModelIndex &idx, const QString &targetZoneName) -{ - if (!idx.isValid()) { - return false; - } - - // Only actual card rows can be moved. Group or zone rows report an - // aggregate amount and must never be deleted by this operation. - if (!idx.data(DeckRoles::IsCardRole).toBool()) { - return false; - } - - QString cardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); - QString providerId = idx.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data(Qt::DisplayRole).toString(); - int copies = idx.siblingAtColumn(DeckListModelColumns::CARD_AMOUNT).data(Qt::EditRole).toInt(); - - if (copies <= 0) { - return false; - } - - // Tokens only live in the tokens zone and cannot be moved into decks. - CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(cardName); - if (info && info->getIsToken()) { - return false; - } - - // Determine the zone the card currently lives in: the enclosing custom - // zone, or the nearest top-level zone (board zone or legacy zone). - QString currentZoneName; - for (QModelIndex ancestor = idx.parent(); ancestor.isValid(); ancestor = ancestor.parent()) { - bool isCustomZone = ancestor.data(DeckRoles::IsCustomZoneRole).toBool(); - if (isCustomZone || !ancestor.parent().isValid()) { - currentZoneName = ancestor.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); - break; - } - } - - if (currentZoneName == targetZoneName) { - return false; - } - - QString reason = tr("Moved %1 × \"%2\" (%3) to %4") - .arg(copies) - .arg(cardName) - .arg(providerId) - .arg(InnerDecklistNode::visibleNameFromName(targetZoneName)); - - return modifyDeck(reason, [&idx, &cardName, &providerId, &targetZoneName, copies](auto model) { - if (!model->removeRow(idx.row(), idx.parent())) { - return false; - } - - if (ExactCard card = CardDatabaseManager::query()->getCard({cardName, providerId})) { - for (int i = 0; i < copies; ++i) { - model->addCard(card, targetZoneName); - } - } else { - for (int i = 0; i < copies; ++i) { - model->addPreferredPrintingCard(cardName, targetZoneName, true); - } - } - - return true; - }); -} - -bool DeckStateManager::createCustomZone(const QString &boardZoneName, const QString &zoneName) -{ - const QString trimmedZoneName = zoneName.trimmed(); - if (trimmedZoneName.isEmpty()) { - return false; - } - - QString reason = - tr("Created zone \"%1\" in %2").arg(trimmedZoneName, InnerDecklistNode::visibleNameFromName(boardZoneName)); - - return modifyTree(reason, [&boardZoneName, &trimmedZoneName](DecklistNodeTree *tree) { - return tree->addCustomZone(boardZoneName, trimmedZoneName) != nullptr; - }); -} - -bool DeckStateManager::renameCustomZone(const QString &oldZoneName, const QString &newZoneName) -{ - const QString trimmedNewZoneName = newZoneName.trimmed(); - if (trimmedNewZoneName.isEmpty() || oldZoneName == trimmedNewZoneName) { - return false; - } - - QString reason = tr("Renamed zone \"%1\" to \"%2\"").arg(oldZoneName, trimmedNewZoneName); - - return modifyTree(reason, [&oldZoneName, &trimmedNewZoneName](DecklistNodeTree *tree) { - return tree->renameCustomZone(oldZoneName, trimmedNewZoneName); - }); -} - -bool DeckStateManager::moveCustomZone(const QString &zoneName, const QString &newBoardZoneName) -{ - const auto *tree = deckList->getTree(); - - // Locate the zone through the tree's own lookup, which walks every top-level - // zone (not just the standard boards) and covers the same-board no-op below. - const auto *zone = tree->findCustomZoneByName(zoneName); - if (!zone) { - return false; - } - - // Same-board moves are no-ops and must not pollute the history. - const QString currentBoardName = zone->getParent() ? zone->getParent()->getName() : QString(); - if (currentBoardName == newBoardZoneName) { - return true; - } - - // Zone names are deck-unique among zones created through this manager, so a - // same-named zone on the target board can only come from an imported deck. - // Refuse the move instead of silently stacking same-named zones. - for (const auto *targetZone : tree->getCustomZones(newBoardZoneName)) { - if (targetZone->getName() == zoneName) { - return false; - } - } - - QString reason = - tr("Moved zone \"%1\" to %2").arg(zoneName, InnerDecklistNode::visibleNameFromName(newBoardZoneName)); - - return modifyTree(reason, [&zoneName, &newBoardZoneName](DecklistNodeTree *tree) { - return tree->moveCustomZone(zoneName, newBoardZoneName); - }); -} - -bool DeckStateManager::removeCustomZone(const QString &zoneName) -{ - QString reason = tr("Deleted zone \"%1\"").arg(zoneName); - - return modifyTree(reason, [&zoneName](DecklistNodeTree *tree) { return tree->removeCustomZone(zoneName); }); -} - -QString DeckStateManager::validateNewZoneName(const QString &zoneName) const -{ - if (zoneName.trimmed().isEmpty()) { - return tr("Enter a zone name."); - } - - const QString trimmedZoneName = zoneName.trimmed(); - - // The standard zone names are reserved even before they exist. - if (trimmedZoneName == DECK_ZONE_MAIN || trimmedZoneName == DECK_ZONE_SIDE || - trimmedZoneName == DECK_ZONE_MAYBEBOARD || trimmedZoneName == DECK_ZONE_TOKENS) { - return tr("This name is reserved."); - } - - const auto *tree = deckList->getTree(); - - // Reuse the tree's own uniqueness contract: any top-level zone and any - // custom zone on *every* board claims the name (hasZoneName also reserves - // the standard board names, which we already rejected with a dedicated - // message above). Scanning only the standard boards here would miss a - // custom zone an imported deck carries under `tokens`. - if (tree->hasZoneName(trimmedZoneName)) { - return tr("A zone with this name already exists."); - } - - return {}; -} - bool DeckStateManager::offsetCountAtIndex(const QModelIndex &idx, int offset) { if (!idx.isValid()) { @@ -532,25 +367,6 @@ void DeckStateManager::requestHistorySave(const QString &reason) historyManager->save(deckList->createMemento(reason)); } -bool DeckStateManager::modifyTree(const QString &reason, const std::function &operation) -{ - DeckListMemento memento = deckList->createMemento(reason); - bool success = operation(deckList->getTree()); - - if (success) { - historyManager->save(memento); - deckListModel->rebuildTree(); - deckList->refreshDeckHash(); - emit deckListModel->deckHashChanged(); - // removeCustomZone can drop whole card sets the model never notified - // about (rebuildTree emits no cardNodesChanged), so tell the consumers. - emit deckListModel->cardNodesChanged(); - doCardModified(); - } - - return success; -} - /** * @brief Handles updating state and emitting signals whenever the cards are modified */ diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h index 2c8b34a39..b9c99903e 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h @@ -5,7 +5,6 @@ #include "deck_list_model.h" #include -#include #include class DeckListHistoryManager; @@ -237,68 +236,6 @@ public: */ bool decrementCountAtIndex(const QModelIndex &idx); - /** - * @brief Moves all copies of the card at the given index to the given zone. - * No-ops if the index is invalid, not a card node, the card is a token, or the - * card is already in the target zone. - * Saves the operation to history if successful. - * - * @param idx The model index of the card to move - * @param targetZoneName The zone to move the card to (board zone or custom zone name) - * @return Whether the operation was successfully performed - */ - bool moveCardToZone(const QModelIndex &idx, const QString &targetZoneName); - - /** - * @brief Creates a new custom zone nested under a board zone. - * Saves the operation to history if successful. - * - * @param boardZoneName The board zone to nest the custom zone under - * @param zoneName The name of the new custom zone. Gets trimmed and must be - * unique across the deck. - * @return Whether the zone was created - */ - bool createCustomZone(const QString &boardZoneName, const QString &zoneName); - - /** - * @brief Renames a custom zone. - * Saves the operation to history if successful. - * - * @param oldZoneName The current name of the custom zone - * @param newZoneName The new name. Gets trimmed and must be unique across the deck. - * @return Whether the rename succeeded - */ - bool renameCustomZone(const QString &oldZoneName, const QString &newZoneName); - - /** - * @brief Moves a custom zone (and its cards) to a different board zone. - * Same-board moves succeed without creating a history entry. - * Saves the operation to history if successful. - * - * @param zoneName The custom zone to move - * @param newBoardZoneName The board zone to move the custom zone under - * @return Whether the move succeeded - */ - bool moveCustomZone(const QString &zoneName, const QString &newBoardZoneName); - - /** - * @brief Removes a custom zone and all its cards. - * Saves the operation to history if successful. - * - * @param zoneName The custom zone to remove - * @return Whether the zone was removed - */ - bool removeCustomZone(const QString &zoneName); - - /** - * @brief Checks whether a candidate name is usable for a new custom zone. - * - * @param zoneName The candidate name - * @return An empty string when the name is usable, otherwise a user-facing - * error message describing the problem - */ - [[nodiscard]] QString validateNewZoneName(const QString &zoneName) const; - /** * Undoes n steps of the history, setting the decklist state and updating the current step in the historyManager. * @param steps Number of steps to undo. @@ -320,7 +257,6 @@ public slots: private: bool offsetCountAtIndex(const QModelIndex &idx, int offset); - bool modifyTree(const QString &reason, const std::function &operation); void doCardModified(); void doMetadataModified(); diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_zone_dialog.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_zone_dialog.cpp deleted file mode 100644 index 9a0be2570..000000000 --- a/cockatrice/src/interface/widgets/deck_editor/deck_zone_dialog.cpp +++ /dev/null @@ -1,145 +0,0 @@ -#include "deck_zone_dialog.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -DeckZoneDialog::DeckZoneDialog(QWidget *parent, - const QString &initialBoardName, - const std::function &_nameValidator, - bool _allowBoardSelection) - : QDialog(parent), nameValidator(_nameValidator), allowBoardSelection(_allowBoardSelection) -{ - nameLabel = new QLabel(this); - nameEdit = new QLineEdit(this); - nameEdit->setMaxLength(MAX_NAME_LENGTH); - - errorLabel = new QLabel(this); - errorLabel->hide(); - - boardLabel = new QLabel(this); - boardCombo = new QComboBox(this); - for (const QString &boardName : InnerDecklistNode::boardZoneNames()) { - // Use the icon overload explicitly so `boardName` lands in the user data role - // (visible text is applied below in retranslateUi). The two-argument form - // addItem({}, boardName) would be ambiguous and resolve to the icon overload - // with empty user data, yielding empty entries and an empty getBoardName(). - boardCombo->addItem({}, {}, boardName); - } - if (!initialBoardName.isEmpty()) { - int idx = boardCombo->findData(initialBoardName); - if (idx != -1) { - boardCombo->setCurrentIndex(idx); - } - } - - buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); - buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); - connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); - connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); - - auto *layout = new QVBoxLayout(this); - layout->addWidget(nameLabel); - layout->addWidget(nameEdit); - layout->addWidget(errorLabel); - if (allowBoardSelection) { - layout->addWidget(boardLabel); - layout->addWidget(boardCombo); - } else { - boardLabel->hide(); - boardCombo->hide(); - } - layout->addWidget(buttonBox); - - retranslateUi(); - - connect(nameEdit, &QLineEdit::textChanged, this, [this] { validateName(); }); - validateName(); - - nameEdit->setFocus(); -} - -QString DeckZoneDialog::getZoneName() const -{ - return nameEdit->text().trimmed(); -} - -QString DeckZoneDialog::getBoardName() const -{ - return boardCombo->currentData().toString(); -} - -void DeckZoneDialog::setZoneName(const QString &zoneName) -{ - nameEdit->setText(zoneName); - nameEdit->selectAll(); -} - -void DeckZoneDialog::changeEvent(QEvent *event) -{ - QDialog::changeEvent(event); - - if (event->type() == QEvent::LanguageChange) { - retranslateUi(); - } -} - -void DeckZoneDialog::retranslateUi() -{ - setWindowTitle(allowBoardSelection ? tr("New zone") : tr("Rename zone")); - - nameLabel->setText(tr("Zone &name:")); - nameLabel->setBuddy(nameEdit); - - boardLabel->setText(tr("&Parent zone:")); - boardLabel->setBuddy(boardCombo); - - for (int i = 0; i < boardCombo->count(); i++) { - boardCombo->setItemText(i, InnerDecklistNode::visibleNameFromName(boardCombo->itemData(i).toString())); - } -} - -void DeckZoneDialog::validateName() -{ - const QString zoneName = nameEdit->text().trimmed(); - QString error; - if (zoneName.isEmpty()) { - error = tr("Enter a zone name."); - } else if (nameValidator) { - error = nameValidator(zoneName); - } - - errorLabel->setText(error); - errorLabel->setVisible(!error.isEmpty()); - buttonBox->button(QDialogButtonBox::Ok)->setEnabled(error.isEmpty()); -} - -QString DeckZoneDialog::promptForNewZone(QWidget *parent, - const QString &initialBoardName, - QString *chosenBoardName, - const std::function &nameValidator) -{ - DeckZoneDialog dialog(parent, initialBoardName, nameValidator); - if (dialog.exec() != QDialog::Accepted) { - return {}; - } - - if (chosenBoardName) { - *chosenBoardName = dialog.getBoardName(); - } - return dialog.getZoneName(); -} - -QString DeckZoneDialog::promptForRename(QWidget *parent, - const QString ¤tZoneName, - const std::function &nameValidator) -{ - DeckZoneDialog dialog(parent, {}, nameValidator, false); - dialog.setZoneName(currentZoneName); - return dialog.exec() == QDialog::Accepted ? dialog.getZoneName() : QString(); -} diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_zone_dialog.h b/cockatrice/src/interface/widgets/deck_editor/deck_zone_dialog.h deleted file mode 100644 index 6f55617a8..000000000 --- a/cockatrice/src/interface/widgets/deck_editor/deck_zone_dialog.h +++ /dev/null @@ -1,123 +0,0 @@ -/** - * @file deck_zone_dialog.h - * @ingroup DeckEditorWidgets - * @brief Shared dialog for creating custom deck zones. - */ - -#ifndef DECK_ZONE_DIALOG_H -#define DECK_ZONE_DIALOG_H - -#include -#include -#include -#include - -class QComboBox; -class QDialogButtonBox; -class QLabel; -class QLineEdit; -class QWidget; - -/** - * @brief Modal dialog asking for the name and parent zone of a new custom deck zone. - * - * Menus construct the dialog transiently around exec(), so validation state only - * ever reflects the name currently typed. - */ -class DeckZoneDialog : public QDialog -{ - Q_OBJECT - -public: - /** - * @brief Constructs the dialog and runs the initial validation pass. - * - * @param parent The parent widget for the dialog - * @param initialBoardName The board zone to preselect in the combo. Unknown names - * fall back to main. - * @param _nameValidator Given the trimmed candidate name, returns an empty string - * when it is usable, otherwise a user-facing error message. May be empty. - * @param _allowBoardSelection When false the parent-zone combo is hidden and the - * dialog acts as a rename prompt for an existing zone. - */ - explicit DeckZoneDialog(QWidget *parent = nullptr, - const QString &initialBoardName = {}, - const std::function &_nameValidator = {}, - bool _allowBoardSelection = true); - - /** - * @brief The trimmed zone name entered by the user. - */ - [[nodiscard]] QString getZoneName() const; - - /** - * @brief The internal name of the board zone selected in the combo. - */ - [[nodiscard]] QString getBoardName() const; - - /** - * @brief Prefills the name field, e.g. with the current name when renaming. - * - * @param zoneName The text to put into the name field, selected for quick editing - */ - void setZoneName(const QString &zoneName); - - /** - * @brief Prompts the user for a new custom zone name and the board zone to nest it under. - * - * Convenience wrapper that runs DeckZoneDialog modally. - * - * @param parent The parent widget for the dialog - * @param initialBoardName The board zone to preselect in the dialog. Unknown names fall - * back to main. - * @param chosenBoardName (out) The internal name of the board zone the user chose - * @param nameValidator Optional validator forwarded to the dialog - * @return The trimmed zone name, or an empty string if the user cancelled - */ - static QString promptForNewZone(QWidget *parent, - const QString &initialBoardName, - QString *chosenBoardName, - const std::function &nameValidator = {}); - - /** - * @brief Prompts the user for a new name for an existing custom zone. - * - * Same inline validation as promptForNewZone, but without a parent-zone picker. - * - * @param parent The parent widget for the dialog - * @param currentZoneName The current name, prefilled for editing - * @param nameValidator Validator deciding whether a candidate name is usable. It sees - * the current name too, so callers wanting to allow unchanged names must - * special-case that themselves. - * @return The trimmed new name, or an empty string if the user cancelled - */ - static QString promptForRename(QWidget *parent, - const QString ¤tZoneName, - const std::function &nameValidator = {}); - -protected: - void changeEvent(QEvent *event) override; - -private: - /** - * @brief Sets every user-visible string. Runs on construction and on runtime - * language changes. - */ - void retranslateUi(); - - /** - * @brief Validates the current input, toggling Ok and the inline error label. - */ - void validateName(); - - QLabel *nameLabel; - QLineEdit *nameEdit; - QLabel *errorLabel; - QLabel *boardLabel; - QComboBox *boardCombo; - QDialogButtonBox *buttonBox; - std::function nameValidator; - bool allowBoardSelection; -}; - -#endif // DECK_ZONE_DIALOG_H diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp index 4698b011f..883cfcd03 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp @@ -132,7 +132,7 @@ void DlgSettings::setupUi() pagesWidget->addWidget(makeScrollable(userInterfacePage)); pagesWidget->addWidget(makeScrollable(deckEditorPage)); pagesWidget->addWidget(makeScrollable(storagePage)); - pagesWidget->addWidget(makeScrollable(messagesPage)); + pagesWidget->addWidget(messagesPage); pagesWidget->addWidget(soundPage); pagesWidget->addWidget(shortcutsPage); diff --git a/cockatrice/src/interface/widgets/onboarding/first_run_wizard.cpp b/cockatrice/src/interface/widgets/onboarding/first_run_wizard.cpp index 4af02fe4f..618ac6f26 100644 --- a/cockatrice/src/interface/widgets/onboarding/first_run_wizard.cpp +++ b/cockatrice/src/interface/widgets/onboarding/first_run_wizard.cpp @@ -182,13 +182,6 @@ void FirstRunWizard::onCardDatabaseUpdateFinished(bool success) } } -void FirstRunWizard::onCardDatabaseUpdateProgress(const QString &stage, qint64 done, qint64 total) -{ - if (cardDatabasePage) { - cardDatabasePage->onUpdateProgress(stage, done, total); - } -} - void FirstRunWizard::finish() { accept(); diff --git a/cockatrice/src/interface/widgets/onboarding/first_run_wizard.h b/cockatrice/src/interface/widgets/onboarding/first_run_wizard.h index 21d7b6e06..2c186ef95 100644 --- a/cockatrice/src/interface/widgets/onboarding/first_run_wizard.h +++ b/cockatrice/src/interface/widgets/onboarding/first_run_wizard.h @@ -37,9 +37,6 @@ public slots: /** @brief Forwarded from MainWindow once the background card database update process exits. */ void onCardDatabaseUpdateFinished(bool success); - /** @brief Forwarded from MainWindow while the background card database update process runs. */ - void onCardDatabaseUpdateProgress(const QString &stage, qint64 done, qint64 total); - protected: void closeEvent(QCloseEvent *event) override; void changeEvent(QEvent *event) override; diff --git a/cockatrice/src/interface/widgets/onboarding/pages/card_database_setup_page.cpp b/cockatrice/src/interface/widgets/onboarding/pages/card_database_setup_page.cpp index 50e8ff63d..12116de7a 100644 --- a/cockatrice/src/interface/widgets/onboarding/pages/card_database_setup_page.cpp +++ b/cockatrice/src/interface/widgets/onboarding/pages/card_database_setup_page.cpp @@ -15,7 +15,6 @@ #include #include #include -#include #include #include @@ -180,25 +179,6 @@ void CardDatabaseSetupPage::onUpdateFinished(bool success) } } -void CardDatabaseSetupPage::onUpdateProgress(const QString &stage, qint64 done, qint64 total) -{ - if (state != State::Running) { - return; - } - progressBar->setRange(0, total > 0 ? static_cast(qMin(total, INT_MAX)) : 0); - progressBar->setValue(static_cast(qMin(done, INT_MAX))); - if (total > 0) { - const int percent = static_cast((100.0 * done) / total); - if (stage == QLatin1String("download")) { - statusLabel->setText(tr("Downloading the card database (%1%)…").arg(percent)); - } else if (stage == QLatin1String("scan")) { - statusLabel->setText(tr("Parsing the card database (%1%)…").arg(percent)); - } else if (stage == QLatin1String("import")) { - statusLabel->setText(tr("Importing cards (%1%)…").arg(percent)); - } - } -} - QString CardDatabaseSetupPage::nextButtonText() const { return state == State::NotStarted ? tr("Download") : QString(); diff --git a/cockatrice/src/interface/widgets/onboarding/pages/card_database_setup_page.h b/cockatrice/src/interface/widgets/onboarding/pages/card_database_setup_page.h index 870e759ea..0461d11d5 100644 --- a/cockatrice/src/interface/widgets/onboarding/pages/card_database_setup_page.h +++ b/cockatrice/src/interface/widgets/onboarding/pages/card_database_setup_page.h @@ -30,7 +30,6 @@ public: void retranslateUi() override; void onUpdateFinished(bool success); - void onUpdateProgress(const QString &stage, qint64 done, qint64 total); signals: void updateRequested(); diff --git a/cockatrice/src/interface/widgets/playmat/playmat_settings_dialog.cpp b/cockatrice/src/interface/widgets/playmat/playmat_settings_dialog.cpp index 9459c5ea9..57706cf93 100644 --- a/cockatrice/src/interface/widgets/playmat/playmat_settings_dialog.cpp +++ b/cockatrice/src/interface/widgets/playmat/playmat_settings_dialog.cpp @@ -2,7 +2,6 @@ #include "../../card_picture_loader/card_picture_loader.h" #include "../cards/art_crop_attribution.h" -#include "../cards/card_art_utils.h" #include "../utility/completer_utils.h" #include "card_database_display_model.h" #include "card_database_model.h" @@ -277,7 +276,7 @@ void PlaymatSettingsDialog::reloadPreview() return; } - currentPixmap = CardArtUtils::rotateSidewaysLayoutArt(fullRes, card); + currentPixmap = fullRes; preview->setPixmap(currentPixmap); preview->setParams(currentParams); preview->setAttribution(buildArtAttribution(card)); diff --git a/cockatrice/src/interface/widgets/replay/replay_manager.cpp b/cockatrice/src/interface/widgets/replay/replay_manager.cpp index c51b96b6c..a2c1e0ff0 100644 --- a/cockatrice/src/interface/widgets/replay/replay_manager.cpp +++ b/cockatrice/src/interface/widgets/replay/replay_manager.cpp @@ -142,10 +142,8 @@ void ReplayManager::processNewEvents(PlaybackMode playbackMode) } // backwards skip => always skip tap animation - // backwards skip => always skip damage animation (battlefield shimmer / life counter flash) if (playbackMode == BACKWARD_SKIP) { options |= SKIP_TAP_ANIMATION; - options |= SKIP_DAMAGE_ANIMATION; } emit eventReplayed(replay->event_list(currentEvent), options); diff --git a/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp index 2ba745715..3a1876fa1 100644 --- a/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp @@ -1,7 +1,6 @@ #include "user_card_art_provider.h" #include "../../../card_picture_loader/card_picture_loader.h" -#include "../../cards/card_art_utils.h" #include #include @@ -53,25 +52,16 @@ void UserCardArtProvider::requestCardArt(const QString &userName, const QString processQueue(); } -QPixmap UserCardArtProvider::cropCardArt(const QPixmap &fullRes, const ExactCard &card) +QPixmap UserCardArtProvider::cropCardArt(const QPixmap &fullRes) { - QPixmap source = fullRes; - - // Sideways-layout cards (plane, siege/battle, split) store their landscape - // artwork rotated 90° inside a portrait frame. Rotate it upright first so - // the crop below lands on the horizontal art, mirroring the way - // CardInfoPictureWidget displays these cards. - const bool landscape = card.getInfo().getUiAttributes().landscapeOrientation; - source = CardArtUtils::rotateSidewaysLayoutArt(source, card); - - const QSize sz = source.size(); + const QSize sz = fullRes.size(); const int marginX = sz.width() * 0.07; - const int topMargin = landscape ? sz.height() * 0.05 : sz.height() * 0.11; - const int bottomMargin = landscape ? sz.height() * 0.42 : sz.height() * 0.45; + const int topMargin = sz.height() * 0.11; + const int bottomMargin = sz.height() * 0.45; - const QRect artRect(marginX, topMargin, sz.width() - 2 * marginX, sz.height() - topMargin - bottomMargin); + const QRect foilRect(marginX, topMargin, sz.width() - 2 * marginX, sz.height() - topMargin - bottomMargin); - return source.copy(artRect.intersected(source.rect())); + return fullRes.copy(foilRect.intersected(fullRes.rect())); } void UserCardArtProvider::insertIntoCache(const QString &key, const QPixmap &pixmap) @@ -121,7 +111,7 @@ void UserCardArtProvider::processQueue() // Synchronous hit (already loaded/on disk) if (!fullRes.isNull()) { - insertIntoCache(key, cropCardArt(fullRes, card)); + insertIntoCache(key, cropCardArt(fullRes)); pending.remove(key); emit cardArtUpdated(userName); @@ -145,7 +135,7 @@ void UserCardArtProvider::processQueue() CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040)); if (!fullRes.isNull()) { - self->insertIntoCache(key, self->cropCardArt(fullRes, card)); + self->insertIntoCache(key, self->cropCardArt(fullRes)); } self->pending.remove(key); diff --git a/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h index e8283a891..2592237c4 100644 --- a/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h +++ b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h @@ -6,7 +6,6 @@ #include #include #include -#include class UserCardArtProvider : public QObject { @@ -17,7 +16,7 @@ public: void requestCardArt(const QString &userName, const QString &cardName, const QString &providerId); const QMap &cache() const; - static QPixmap cropCardArt(const QPixmap &fullRes, const ExactCard &card); + static QPixmap cropCardArt(const QPixmap &fullRes); signals: void cardArtUpdated(const QString &userName); diff --git a/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp index d49e3d540..532112964 100644 --- a/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp @@ -560,7 +560,7 @@ void UserCardArtSettingsDialog::reloadPreview() return; } - currentPixmap = UserCardArtProvider::cropCardArt(fullRes, card); + currentPixmap = UserCardArtProvider::cropCardArt(fullRes); preview->setPixmap(currentPixmap); preview->setParams(currentParams); diff --git a/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp b/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp index 0d2267a63..72e7c41b2 100644 --- a/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp @@ -452,7 +452,7 @@ void UserContextMenu::showContextMenu(const QPoint &pos, } } aDetails->setEnabled(true); - aChat->setEnabled(anotherUser && online && !userListProxy->isUserIgnored(userName)); + aChat->setEnabled(anotherUser && online); aShowGames->setEnabled(online); aReport->setEnabled(anotherUser); aAddToBuddyList->setEnabled(anotherUser); @@ -625,15 +625,7 @@ void UserContextMenu::execAddToIgnore(const QString &userName) Command_AddToList cmd; cmd.set_list("ignore"); cmd.set_user_name(userName.toStdString()); - PendingCommand *pend = client->prepareSessionCommand(cmd); - connect(pend, &PendingCommand::finished, this, - [this, userName](const Response &response, const CommandContainer &, const QVariant &) { - if (response.response_code() == Response::RespOk) { - QMessageBox::information(static_cast(parent()), tr("Ignore list"), - tr("%1 has been added to your ignore list.").arg(userName)); - } - }); - client->sendCommand(pend); + client->sendCommand(client->prepareSessionCommand(cmd)); } void UserContextMenu::execRemoveFromIgnore(const QString &userName) diff --git a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp index c8494f095..881c54167 100644 --- a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp @@ -154,48 +154,6 @@ AppearanceSettingsPage::AppearanceSettingsPage() homeTabGroupBox = new QGroupBox; homeTabGroupBox->setLayout(homeTabGrid); - // Playmat settings - playmatVisibilityCombo.addItem(tr("Show all playmats"), PlaymatVisibilityAll); - playmatVisibilityCombo.addItem(tr("Show own playmat only"), PlaymatVisibilityOwnOnly); - playmatVisibilityCombo.addItem(tr("Don't use playmats"), PlaymatVisibilityNone); - int visIdx = playmatVisibilityCombo.findData(settings.userInterface().getPlaymatVisibility()); - if (visIdx >= 0) { - playmatVisibilityCombo.setCurrentIndex(visIdx); - } - connect(&playmatVisibilityCombo, qOverload(&QComboBox::currentIndexChanged), this, [this](int index) { - SettingsCache::instance().userInterface().setPlaymatVisibility(playmatVisibilityCombo.itemData(index).toInt()); - }); - playmatVisibilityLabel.setBuddy(&playmatVisibilityCombo); - - // Playmat mode: Override / Fallback / Deck-only - playmatModeCombo.addItem(tr("Override deck playmat"), PlaymatModeOverrideDeck); - playmatModeCombo.addItem(tr("Fallback if deck has none"), PlaymatModeFallback); - playmatModeCombo.addItem(tr("Deck only, ignore collection"), PlaymatModeDeckOnly); - int modeIdx = playmatModeCombo.findData(settings.userInterface().getPlaymatMode()); - if (modeIdx >= 0) { - playmatModeCombo.setCurrentIndex(modeIdx); - } - connect(&playmatModeCombo, qOverload(&QComboBox::currentIndexChanged), this, [this](int index) { - SettingsCache::instance().userInterface().setPlaymatMode(playmatModeCombo.itemData(index).toInt()); - }); - playmatModeLabel.setBuddy(&playmatModeCombo); - - // User-level playmat settings: fallback collection. - connect(&playmatDefaultEditButton, &QPushButton::clicked, this, - &AppearanceSettingsPage::openPlaymatCollectionDialog); - - auto *playmatGrid = new QGridLayout; - playmatGrid->addWidget(&playmatVisibilityLabel, 0, 0, 1, 1); - playmatGrid->addWidget(&playmatVisibilityCombo, 0, 1, 1, 1); - playmatGrid->addWidget(&playmatModeLabel, 1, 0, 1, 1); - playmatGrid->addWidget(&playmatModeCombo, 1, 1, 1, 1); - playmatGrid->addWidget(&playmatDefaultLabel, 2, 0, 1, 1); - playmatGrid->addWidget(&playmatDefaultEditButton, 2, 1, 1, 1); - - playmatGroupBox = new QGroupBox; - playmatGroupBox->setLayout(playmatGrid); - - // Styling settings styleUserListCheckBox.setChecked(settings.appearance().getStyleUserList()); connect(&styleUserListCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.appearance(), &AppearanceSettings::setStyleUserList); @@ -301,6 +259,7 @@ AppearanceSettingsPage::AppearanceSettingsPage() cardLayoutGroupBox->setLayout(cardLayoutGrid); // Card counter colors + auto *cardCounterColorsLayout = new QGridLayout; cardCounterColorsLayout->setColumnStretch(1, 1); cardCounterColorsLayout->setColumnStretch(3, 1); @@ -380,6 +339,47 @@ AppearanceSettingsPage::AppearanceSettingsPage() tableGroupBox = new QGroupBox; tableGroupBox->setLayout(tableGrid); + // Playmat settings + playmatVisibilityCombo.addItem(tr("Show all playmats"), PlaymatVisibilityAll); + playmatVisibilityCombo.addItem(tr("Show own playmat only"), PlaymatVisibilityOwnOnly); + playmatVisibilityCombo.addItem(tr("Don't use playmats"), PlaymatVisibilityNone); + int visIdx = playmatVisibilityCombo.findData(settings.userInterface().getPlaymatVisibility()); + if (visIdx >= 0) { + playmatVisibilityCombo.setCurrentIndex(visIdx); + } + connect(&playmatVisibilityCombo, qOverload(&QComboBox::currentIndexChanged), this, [this](int index) { + SettingsCache::instance().userInterface().setPlaymatVisibility(playmatVisibilityCombo.itemData(index).toInt()); + }); + playmatVisibilityLabel.setBuddy(&playmatVisibilityCombo); + + // Playmat mode: Override / Fallback / Deck-only + playmatModeCombo.addItem(tr("Override deck playmat"), PlaymatModeOverrideDeck); + playmatModeCombo.addItem(tr("Fallback if deck has none"), PlaymatModeFallback); + playmatModeCombo.addItem(tr("Deck only, ignore collection"), PlaymatModeDeckOnly); + int modeIdx = playmatModeCombo.findData(settings.userInterface().getPlaymatMode()); + if (modeIdx >= 0) { + playmatModeCombo.setCurrentIndex(modeIdx); + } + connect(&playmatModeCombo, qOverload(&QComboBox::currentIndexChanged), this, [this](int index) { + SettingsCache::instance().userInterface().setPlaymatMode(playmatModeCombo.itemData(index).toInt()); + }); + playmatModeLabel.setBuddy(&playmatModeCombo); + + // User-level playmat settings: fallback collection. + connect(&playmatDefaultEditButton, &QPushButton::clicked, this, + &AppearanceSettingsPage::openPlaymatCollectionDialog); + + auto *playmatGrid = new QGridLayout; + playmatGrid->addWidget(&playmatVisibilityLabel, 0, 0, 1, 1); + playmatGrid->addWidget(&playmatVisibilityCombo, 0, 1, 1, 1); + playmatGrid->addWidget(&playmatModeLabel, 1, 0, 1, 1); + playmatGrid->addWidget(&playmatModeCombo, 1, 1, 1, 1); + playmatGrid->addWidget(&playmatDefaultLabel, 2, 0, 1, 1); + playmatGrid->addWidget(&playmatDefaultEditButton, 2, 1, 1, 1); + + playmatGroupBox = new QGroupBox; + playmatGroupBox->setLayout(playmatGrid); + // putting it all together auto *mainLayout = new QVBoxLayout; mainLayout->addWidget(themeGroupBox); @@ -512,12 +512,6 @@ void AppearanceSettingsPage::retranslateUi() homeTabButtonColorSourceBox.setToolTip( tr("Automatic: extract from background if present, otherwise use theme default")); - playmatGroupBox->setTitle(tr("Playmat settings")); - playmatVisibilityLabel.setText(tr("Playmat visibility:")); - playmatModeLabel.setText(tr("Default collection behavior:")); - playmatDefaultLabel.setText(tr("Default playmat collection:")); - playmatDefaultEditButton.setText(tr("Edit...")); - stylingGroupBox->setTitle(tr("Styling settings")); styleUserListCheckBox.setText(tr("Style user list")); @@ -560,4 +554,9 @@ void AppearanceSettingsPage::retranslateUi() tableGroupBox->setTitle(tr("Table grid layout")); invertVerticalCoordinateCheckBox.setText(tr("Invert vertical coordinate")); minPlayersForMultiColumnLayoutLabel.setText(tr("Minimum player count for multi-column layout:")); + playmatGroupBox->setTitle(tr("Playmat settings")); + playmatVisibilityLabel.setText(tr("Playmat visibility:")); + playmatModeLabel.setText(tr("Default collection behavior:")); + playmatDefaultLabel.setText(tr("Default playmat collection:")); + playmatDefaultEditButton.setText(tr("Edit...")); } diff --git a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h index 8db71ff8f..6b0369694 100644 --- a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h @@ -44,55 +44,46 @@ private: QLabel homeTabButtonColorSourceLabel; QComboBox homeTabButtonColorSourceBox; - QLabel playmatVisibilityLabel; - QComboBox playmatVisibilityCombo; - QLabel playmatModeLabel; - QComboBox playmatModeCombo; - QLabel playmatDefaultLabel; - QPushButton playmatDefaultEditButton; - QCheckBox styleUserListCheckBox; - QCheckBox showShortcutsCheckBox; QCheckBox showGameSelectorFilterToolbarCheckBox; - + QLabel minPlayersForMultiColumnLayoutLabel; + QLabel maxFontSizeForCardsLabel; QCheckBox overrideAllCardArtWithPersonalPreferenceCheckBox; QCheckBox bumpSetsWithCardsInDeckToTopCheckBox; - QCheckBox displayCardNamesCheckBox; QCheckBox autoRotateSidewaysLayoutCardsCheckBox; QCheckBox cardScalingCheckBox; QCheckBox roundCardCornersCheckBox; - QLabel maxFontSizeForCardsLabel; - QSpinBox maxFontSizeForCardsEdit; - QLabel verticalCardOverlapPercentLabel; QSpinBox verticalCardOverlapPercentBox; QLabel cardViewInitialRowsMaxLabel; QSpinBox cardViewInitialRowsMaxBox; QLabel cardViewExpandedRowsMaxLabel; QSpinBox cardViewExpandedRowsMaxBox; - - QList cardCounterNames; - QCheckBox horizontalHandCheckBox; QCheckBox leftJustifiedHandCheckBox; - QCheckBox invertVerticalCoordinateCheckBox; - QLabel minPlayersForMultiColumnLayoutLabel; - QSpinBox minPlayersForMultiColumnLayoutEdit; - + QLabel playmatVisibilityLabel; + QComboBox playmatVisibilityCombo; + QLabel playmatModeLabel; + QComboBox playmatModeCombo; + QLabel playmatDefaultLabel; + QPushButton playmatDefaultEditButton; QGroupBox *themeGroupBox; QGroupBox *homeTabGroupBox; - QGroupBox *playmatGroupBox; QGroupBox *stylingGroupBox; QGroupBox *menuGroupBox; QGroupBox *printingsGroupBox; QGroupBox *cardsGroupBox; QGroupBox *cardLayoutGroupBox; - QGroupBox *cardCountersGroupBox; QGroupBox *handGroupBox; + QGroupBox *playmatGroupBox; QGroupBox *tableGroupBox; + QGroupBox *cardCountersGroupBox; + QList cardCounterNames; + QSpinBox minPlayersForMultiColumnLayoutEdit; + QSpinBox maxFontSizeForCardsEdit; public: AppearanceSettingsPage(); diff --git a/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp index 62b06fb60..a293660f9 100644 --- a/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp @@ -425,28 +425,29 @@ void GeneralSettingsPage::updateStartupServerControlsVisibility() void GeneralSettingsPage::retranslateUi() { - const auto &settings = SettingsCache::instance(); - languageGroupBox->setTitle(tr("Language settings")); languageLabel.setText(tr("Language:")); - advertiseTranslationPageLabel.setText( - QString("%2").arg(WIKI_TRANSLATION_FAQ).arg(tr("How to help with translations"))); versionGroupBox->setTitle(tr("Version settings")); + cardDatabaseGroupBox->setTitle(tr("Card database")); + startupGroupBox->setTitle(tr("Startup settings")); + + if (SettingsCache::instance().getIsPortableBuild()) { + pathsGroupBox->setTitle(tr("Paths (editing disabled in portable mode)")); + } else { + pathsGroupBox->setTitle(tr("Paths")); + } + advertiseTranslationPageLabel.setText( + QString("%2").arg(WIKI_TRANSLATION_FAQ).arg(tr("How to help with translations"))); + deckPathLabel.setText(tr("Decks directory:")); + filtersPathLabel.setText(tr("Filters directory:")); + replaysPathLabel.setText(tr("Replays directory:")); + picsPathLabel.setText(tr("Pictures directory:")); + cardDatabasePathLabel.setText(tr("Card database:")); + customCardDatabasePathLabel.setText(tr("Custom database directory:")); + tokenDatabasePathLabel.setText(tr("Token database:")); updateReleaseChannelLabel.setText(tr("Update channel")); startupUpdateCheckCheckBox.setText(tr("Check for client updates on startup")); - updateNotificationCheckBox.setText(tr("Notify if a feature supported by the server is missing in my client")); - newVersionOracleCheckBox.setText(tr("Automatically run Oracle when running a new version of Cockatrice")); - - // We can't change the strings after they're put into the QComboBox, so this is our workaround - int oldIndex = updateReleaseChannelBox.currentIndex(); - updateReleaseChannelBox.clear(); - for (ReleaseChannel *chan : settings.getUpdateReleaseChannels()) { - updateReleaseChannelBox.addItem(tr(chan->getName().toUtf8())); - } - updateReleaseChannelBox.setCurrentIndex(oldIndex); - - cardDatabaseGroupBox->setTitle(tr("Card database")); startupCardUpdateCheckBehaviorLabel.setText(tr("Check for card database updates on startup")); startupCardUpdateCheckBehaviorSelector.setItemText(startupCardUpdateCheckBehaviorIndexNone, tr("Don't check")); startupCardUpdateCheckBehaviorSelector.setItemText(startupCardUpdateCheckBehaviorIndexPrompt, @@ -455,13 +456,8 @@ void GeneralSettingsPage::retranslateUi() tr("Always update in the background")); cardUpdateCheckIntervalLabel.setText(tr("Check for card database updates every")); cardUpdateCheckIntervalSpinBox.setSuffix(tr(" days")); - - QDate lastCheckDate = settings.updates().getLastCardUpdateCheck(); - int daysAgo = lastCheckDate.daysTo(QDate::currentDate()); - lastCardUpdateCheckDateLabel.setText( - tr("Last update check on %1 (%2 days ago)").arg(lastCheckDate.toString()).arg(daysAgo)); - - startupGroupBox->setTitle(tr("Startup settings")); + updateNotificationCheckBox.setText(tr("Notify if a feature supported by the server is missing in my client")); + newVersionOracleCheckBox.setText(tr("Automatically run Oracle when running a new version of Cockatrice")); showTipsOnStartup.setText(tr("Show tips on startup")); startupTabLabel.setText(tr("Startup tab:")); startupTabSelector.setItemText(StartupTab::StartupTabHome, tr("Home")); @@ -477,18 +473,21 @@ void GeneralSettingsPage::retranslateUi() startupServerLabel.setText(tr("Server:")); startupRoomLabel.setText(tr("Room:")); startupRoomNameEdit->setPlaceholderText(tr("Room name")); - - if (settings.getIsPortableBuild()) { - pathsGroupBox->setTitle(tr("Paths (editing disabled in portable mode)")); - } else { - pathsGroupBox->setTitle(tr("Paths")); - } - deckPathLabel.setText(tr("Decks directory:")); - filtersPathLabel.setText(tr("Filters directory:")); - replaysPathLabel.setText(tr("Replays directory:")); - picsPathLabel.setText(tr("Pictures directory:")); - cardDatabasePathLabel.setText(tr("Card database:")); - customCardDatabasePathLabel.setText(tr("Custom database directory:")); - tokenDatabasePathLabel.setText(tr("Token database:")); resetAllPathsButton->setText(tr("Reset all paths")); -} + + const auto &settings = SettingsCache::instance(); + + QDate lastCheckDate = settings.updates().getLastCardUpdateCheck(); + int daysAgo = lastCheckDate.daysTo(QDate::currentDate()); + + lastCardUpdateCheckDateLabel.setText( + tr("Last update check on %1 (%2 days ago)").arg(lastCheckDate.toString()).arg(daysAgo)); + + // We can't change the strings after they're put into the QComboBox, so this is our workaround + int oldIndex = updateReleaseChannelBox.currentIndex(); + updateReleaseChannelBox.clear(); + for (ReleaseChannel *chan : settings.getUpdateReleaseChannels()) { + updateReleaseChannelBox.addItem(tr(chan->getName().toUtf8())); + } + updateReleaseChannelBox.setCurrentIndex(oldIndex); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/settings_page/general_settings_page.h b/cockatrice/src/interface/widgets/settings_page/general_settings_page.h index e0c1a47bf..8dd7e8798 100644 --- a/cockatrice/src/interface/widgets/settings_page/general_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/general_settings_page.h @@ -42,37 +42,6 @@ private: QGroupBox *startupGroupBox; QGroupBox *pathsGroupBox; - QLabel languageLabel; - QComboBox languageBox; - QLabel advertiseTranslationPageLabel; - - QLabel updateReleaseChannelLabel; - QComboBox updateReleaseChannelBox; - QCheckBox startupUpdateCheckCheckBox; - QCheckBox updateNotificationCheckBox; - QCheckBox newVersionOracleCheckBox; - - QLabel startupCardUpdateCheckBehaviorLabel; - QComboBox startupCardUpdateCheckBehaviorSelector; - QLabel cardUpdateCheckIntervalLabel; - QSpinBox cardUpdateCheckIntervalSpinBox; - QLabel lastCardUpdateCheckDateLabel; - - QCheckBox showTipsOnStartup; - QLabel startupTabLabel; - QComboBox startupTabSelector; - QLabel startupServerLabel; - QComboBox startupServerSelector; - QLabel startupRoomLabel; - QLineEdit *startupRoomNameEdit; - - QLabel deckPathLabel; - QLabel filtersPathLabel; - QLabel replaysPathLabel; - QLabel picsPathLabel; - QLabel cardDatabasePathLabel; - QLabel customCardDatabasePathLabel; - QLabel tokenDatabasePathLabel; QLineEdit *deckPathEdit; QLineEdit *filtersPathEdit; QLineEdit *replaysPathEdit; @@ -82,6 +51,33 @@ private: QLineEdit *tokenDatabasePathEdit; QPushButton *resetAllPathsButton; QLabel *allPathsResetLabel; + QComboBox languageBox; + QCheckBox startupUpdateCheckCheckBox; + QLabel startupCardUpdateCheckBehaviorLabel; + QComboBox startupCardUpdateCheckBehaviorSelector; + QLabel cardUpdateCheckIntervalLabel; + QSpinBox cardUpdateCheckIntervalSpinBox; + QLabel lastCardUpdateCheckDateLabel; + QCheckBox updateNotificationCheckBox; + QCheckBox newVersionOracleCheckBox; + QComboBox updateReleaseChannelBox; + QLabel languageLabel; + QLabel deckPathLabel; + QLabel filtersPathLabel; + QLabel replaysPathLabel; + QLabel picsPathLabel; + QLabel cardDatabasePathLabel; + QLabel customCardDatabasePathLabel; + QLabel tokenDatabasePathLabel; + QLabel updateReleaseChannelLabel; + QLabel advertiseTranslationPageLabel; + QCheckBox showTipsOnStartup; + QLabel startupTabLabel; + QComboBox startupTabSelector; + QLabel startupServerLabel; + QComboBox startupServerSelector; + QLabel startupRoomLabel; + QLineEdit *startupRoomNameEdit; }; #endif // COCKATRICE_GENERAL_SETTINGS_PAGE_H diff --git a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp index c161030d8..e4f24ab73 100644 --- a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp @@ -59,10 +59,6 @@ MessagesSettingsPage::MessagesSettingsPage() connect(&roomHistory, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().chat(), &ChatSettings::setRoomHistory); - ignoreAllPrivateMessagesCheckBox.setChecked(SettingsCache::instance().chat().getIgnoreAllPrivateMessages()); - connect(&ignoreAllPrivateMessagesCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().chat(), - &ChatSettings::setIgnoreAllPrivateMessages); - customAlertString = new QLineEdit(); customAlertString->setText(SettingsCache::instance().chat().getHighlightWords()); connect(customAlertString, &QLineEdit::textChanged, &SettingsCache::instance().chat(), @@ -80,7 +76,6 @@ MessagesSettingsPage::MessagesSettingsPage() chatGrid->addWidget(&messagePopups, 5, 0); chatGrid->addWidget(&mentionPopups, 6, 0); chatGrid->addWidget(&roomHistory, 7, 0); - chatGrid->addWidget(&ignoreAllPrivateMessagesCheckBox, 8, 0); chatGroupBox = new QGroupBox; chatGroupBox->setLayout(chatGrid); @@ -261,7 +256,6 @@ void MessagesSettingsPage::retranslateUi() messagePopups.setText(tr("Enable desktop notifications for private messages")); mentionPopups.setText(tr("Enable desktop notification for mentions")); roomHistory.setText(tr("Enable room message history on join")); - ignoreAllPrivateMessagesCheckBox.setText(tr("Ignore all private messages")); hexLabel.setText(tr("(Color is hexadecimal)")); hexHighlightLabel.setText(tr("(Color is hexadecimal)")); customAlertStringLabel.setText(tr("Separate words with a space, alphanumeric characters only")); diff --git a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.h b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.h index 436ebbad9..e98ae0592 100644 --- a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.h @@ -40,7 +40,6 @@ private: QCheckBox messagePopups; QCheckBox mentionPopups; QCheckBox roomHistory; - QCheckBox ignoreAllPrivateMessagesCheckBox; QGroupBox *chatGroupBox; QGroupBox *highlightGroupBox; QGroupBox *messageGroupBox; diff --git a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp index 2c6e062da..182e75aac 100644 --- a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp @@ -20,7 +20,26 @@ enum visualDeckStoragePromptForConversionIndex UserInterfaceSettingsPage::UserInterfaceSettingsPage() { - // general settings + // general settings and notification settings + notificationsEnabledCheckBox.setChecked(SettingsCache::instance().userInterface().getNotificationsEnabled()); + connect(¬ificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), + &InterfaceSettings::setNotificationsEnabled); + connect(¬ificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, this, + &UserInterfaceSettingsPage::setNotificationEnabled); + + specNotificationsEnabledCheckBox.setChecked( + SettingsCache::instance().userInterface().getSpectatorNotificationsEnabled()); + specNotificationsEnabledCheckBox.setEnabled(SettingsCache::instance().userInterface().getNotificationsEnabled()); + connect(&specNotificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), + &InterfaceSettings::setSpectatorNotificationsEnabled); + + buddyConnectNotificationsEnabledCheckBox.setChecked( + SettingsCache::instance().userInterface().getBuddyConnectNotificationsEnabled()); + buddyConnectNotificationsEnabledCheckBox.setEnabled( + SettingsCache::instance().userInterface().getNotificationsEnabled()); + connect(&buddyConnectNotificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, + &SettingsCache::instance().userInterface(), &InterfaceSettings::setBuddyConnectNotificationsEnabled); + doubleClickToPlayCheckBox.setChecked(SettingsCache::instance().userInterface().getDoubleClickToPlay()); connect(&doubleClickToPlayCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), &InterfaceSettings::setDoubleClickToPlay); @@ -84,26 +103,6 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() generalGroupBox = new QGroupBox; generalGroupBox->setLayout(generalGrid); - // notification settings - notificationsEnabledCheckBox.setChecked(SettingsCache::instance().userInterface().getNotificationsEnabled()); - connect(¬ificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), - &InterfaceSettings::setNotificationsEnabled); - connect(¬ificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, this, - &UserInterfaceSettingsPage::setNotificationEnabled); - - specNotificationsEnabledCheckBox.setChecked( - SettingsCache::instance().userInterface().getSpectatorNotificationsEnabled()); - specNotificationsEnabledCheckBox.setEnabled(SettingsCache::instance().userInterface().getNotificationsEnabled()); - connect(&specNotificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), - &InterfaceSettings::setSpectatorNotificationsEnabled); - - buddyConnectNotificationsEnabledCheckBox.setChecked( - SettingsCache::instance().userInterface().getBuddyConnectNotificationsEnabled()); - buddyConnectNotificationsEnabledCheckBox.setEnabled( - SettingsCache::instance().userInterface().getNotificationsEnabled()); - connect(&buddyConnectNotificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, - &SettingsCache::instance().userInterface(), &InterfaceSettings::setBuddyConnectNotificationsEnabled); - auto *notificationsGrid = new QGridLayout; notificationsGrid->addWidget(¬ificationsEnabledCheckBox, 0, 0); notificationsGrid->addWidget(&specNotificationsEnabledCheckBox, 1, 0); @@ -356,7 +355,6 @@ void UserInterfaceSettingsPage::retranslateUi() notificationsEnabledCheckBox.setText(tr("Enable notifications in taskbar")); specNotificationsEnabledCheckBox.setText(tr("Notify in the taskbar for game events while you are spectating")); buddyConnectNotificationsEnabledCheckBox.setText(tr("Notify in the taskbar when users in your buddy list connect")); - animationGroupBox->setTitle(tr("Animation settings")); enableAllAnimationsButton.setText(tr("&Enable all animations")); disableAllAnimationsButton.setText(tr("&Disable all animations")); @@ -364,7 +362,6 @@ void UserInterfaceSettingsPage::retranslateUi() arrowDrawAnimationCheckBox.setText(tr("&Arrow draw animation")); lifeCounterAnimationsCheckBox.setText(tr("Life counter flash")); battlefieldFlashCheckBox.setText(tr("Battlefield flash on damage")); - deckEditorGroupBox->setTitle(tr("Deck editor/storage settings")); openDeckInNewTabCheckBox.setText(tr("Open deck in new tab by default")); visualDeckStorageInGameCheckBox.setText(tr("Use visual deck storage in game lobby")); @@ -400,8 +397,8 @@ void UserInterfaceSettingsPage::retranslateUi() 0, CommanderBracketNames::CommanderSpellbookBracketNames); commanderSpellbookIntegrationBracketNamingSelector.setItemText( 1, CommanderBracketNames::OfficialCommanderBracketNames); - commanderSpellbookIntegrationUseOfficialBracketNamesExplainer.setToolTip(CommanderBracketNames::Explainer); + commanderSpellbookIntegrationUseOfficialBracketNamesExplainer.setToolTip(CommanderBracketNames::Explainer); replayGroupBox->setTitle(tr("Replay settings")); rewindBufferingMsLabel.setText(tr("Buffer time for backwards skip via shortcut:")); rewindBufferingMsBox.setSuffix(" ms"); diff --git a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h index e8a30fb1f..0dc4cf4e8 100644 --- a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h @@ -23,6 +23,9 @@ private slots: void updateCommanderSpellbookUiState(); private: + QCheckBox notificationsEnabledCheckBox; + QCheckBox specNotificationsEnabledCheckBox; + QCheckBox buddyConnectNotificationsEnabledCheckBox; QCheckBox doubleClickToPlayCheckBox; QCheckBox clickPlaysAllSelectedCheckBox; QCheckBox playToStackCheckBox; @@ -34,18 +37,12 @@ private: QCheckBox showTotalSelectionCountCheckBox; QCheckBox useTearOffMenusCheckBox; QCheckBox keepGameChatFocusCheckBox; - - QCheckBox notificationsEnabledCheckBox; - QCheckBox specNotificationsEnabledCheckBox; - QCheckBox buddyConnectNotificationsEnabledCheckBox; - QPushButton enableAllAnimationsButton; QPushButton disableAllAnimationsButton; QCheckBox tapAnimationCheckBox; QCheckBox arrowDrawAnimationCheckBox; QCheckBox lifeCounterAnimationsCheckBox; QCheckBox battlefieldFlashCheckBox; - QCheckBox openDeckInNewTabCheckBox; QLabel visualDeckStoragePromptForConversionLabel; QComboBox visualDeckStoragePromptForConversionSelector; @@ -60,10 +57,8 @@ private: QLabel commanderSpellbookIntegrationUseOfficialBracketNamesLabel; QToolButton commanderSpellbookIntegrationUseOfficialBracketNamesExplainer; QComboBox commanderSpellbookIntegrationBracketNamingSelector; - QLabel rewindBufferingMsLabel; QSpinBox rewindBufferingMsBox; - QGroupBox *generalGroupBox; QGroupBox *notificationsGroupBox; QGroupBox *animationGroupBox; diff --git a/cockatrice/src/interface/widgets/tabs/tab_account.cpp b/cockatrice/src/interface/widgets/tabs/tab_account.cpp index dbcf50966..410a48d40 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_account.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_account.cpp @@ -137,11 +137,6 @@ void TabAccount::retranslateUi() buddyList->retranslateUi(); ignoreList->retranslateUi(); userInfoBox->retranslateUi(); - - buddyList->setToolTip(tr("Buddies are marked with a star in chat, a sound plays when they join or leave the " - "server, and they can be invited to buddy-only games.")); - ignoreList->setToolTip(tr("Ignored users' chat messages are hidden from you, and they cannot send you private " - "messages or join your games.")); } void TabAccount::processListUsersResponse(const Response &response) diff --git a/cockatrice/src/interface/widgets/tabs/tab_developer.cpp b/cockatrice/src/interface/widgets/tabs/tab_developer.cpp index 39652ef7d..5456b86b0 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_developer.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_developer.cpp @@ -6,15 +6,12 @@ #include "tab_developer.h" -#include #include #include #include #include #include -#include #include -#include #include #include #include @@ -22,8 +19,6 @@ #include #include -static constexpr int DEFAULT_AUTO_REFRESH_INTERVAL_SECS = 30; - TabDeveloper::TabDeveloper(TabSupervisor *_tabSupervisor, AbstractClient *_client) : Tab(_tabSupervisor), client(_client) { @@ -50,28 +45,12 @@ TabDeveloper::TabDeveloper(TabSupervisor *_tabSupervisor, AbstractClient *_clien statusLabel = new QLabel; - autoRefreshCheckBox = new QCheckBox; - autoRefreshCheckBox->setChecked(false); - - refreshIntervalSpinBox = new QSpinBox; - refreshIntervalSpinBox->setRange(5, 3600); - refreshIntervalSpinBox->setValue(DEFAULT_AUTO_REFRESH_INTERVAL_SECS); - refreshIntervalSpinBox->setEnabled(false); - - autoRefreshTimer = new QTimer(this); - connect(autoRefreshTimer, &QTimer::timeout, this, &TabDeveloper::refreshClicked); - connect(autoRefreshCheckBox, &QCheckBox::toggled, this, &TabDeveloper::autoRefreshToggled); - connect(refreshIntervalSpinBox, QOverload::of(&QSpinBox::valueChanged), this, - &TabDeveloper::refreshIntervalChanged); - refreshButton = new QPushButton; refreshButton->setAutoDefault(true); connect(refreshButton, &QPushButton::clicked, this, &TabDeveloper::refreshClicked); auto *buttonLayout = new QHBoxLayout; buttonLayout->addWidget(statusLabel, 1, Qt::AlignLeft); - buttonLayout->addWidget(autoRefreshCheckBox, 0, Qt::AlignRight); - buttonLayout->addWidget(refreshIntervalSpinBox, 0, Qt::AlignRight); buttonLayout->addWidget(refreshButton, 0, Qt::AlignRight); auto *tableLayout = new QHBoxLayout; @@ -91,10 +70,6 @@ TabDeveloper::TabDeveloper(TabSupervisor *_tabSupervisor, AbstractClient *_clien void TabDeveloper::retranslateUi() { - autoRefreshCheckBox->setText(tr("Auto-refresh")); - autoRefreshCheckBox->setToolTip(tr("Automatically request fresh server statistics at a fixed interval.")); - refreshIntervalSpinBox->setSuffix(tr(" s")); - refreshIntervalSpinBox->setToolTip(tr("Seconds between automatic refreshes.")); refreshButton->setText(tr("Refresh server stats")); statsTable->setHorizontalHeaderLabels(QString(tr("Statistic;Value")).split(";")); commandTable->setHorizontalHeaderLabels(QString(tr("Command;Count;Total ms;Avg ms")).split(";")); @@ -151,37 +126,14 @@ void TabDeveloper::appendSeparatorRow(const QString §ionTitle) void TabDeveloper::refreshClicked() { - if (requestPending) { - return; - } - requestPending = true; Command_GetServerStats cmd; PendingCommand *pend = client->prepareDeveloperCommand(cmd); connect(pend, &PendingCommand::finished, this, &TabDeveloper::serverStatsResponse); client->sendCommand(pend); } -void TabDeveloper::autoRefreshToggled(bool checked) -{ - refreshIntervalSpinBox->setEnabled(checked); - if (checked) { - refreshIntervalChanged(); - refreshClicked(); - } else { - autoRefreshTimer->stop(); - } -} - -void TabDeveloper::refreshIntervalChanged() -{ - if (autoRefreshCheckBox->isChecked()) { - autoRefreshTimer->start(refreshIntervalSpinBox->value() * 1000); - } -} - void TabDeveloper::serverStatsResponse(const Response &resp) { - requestPending = false; if (resp.response_code() != Response::RespOk) { statusLabel->setText(tr("No server statistics available yet.")); return; diff --git a/cockatrice/src/interface/widgets/tabs/tab_developer.h b/cockatrice/src/interface/widgets/tabs/tab_developer.h index 727a56bcb..fa142ae03 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_developer.h +++ b/cockatrice/src/interface/widgets/tabs/tab_developer.h @@ -10,12 +10,9 @@ #include "tab.h" class AbstractClient; -class QCheckBox; class QLabel; class QPushButton; -class QSpinBox; class QTableWidget; -class QTimer; class Response; class TabDeveloper : public Tab @@ -27,10 +24,6 @@ private: QTableWidget *commandTable; QPushButton *refreshButton; QLabel *statusLabel; - QCheckBox *autoRefreshCheckBox; - QSpinBox *refreshIntervalSpinBox; - QTimer *autoRefreshTimer; - bool requestPending = false; void appendStatRow(const QString &name, const QString &value); void appendSeparatorRow(const QString §ionTitle); @@ -40,8 +33,6 @@ private: private slots: void refreshClicked(); void serverStatsResponse(const Response &resp); - void autoRefreshToggled(bool checked); - void refreshIntervalChanged(); public: explicit TabDeveloper(TabSupervisor *_tabSupervisor, AbstractClient *_client); diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.cpp b/cockatrice/src/interface/widgets/tabs/tab_game.cpp index 035ab1004..196ea4526 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_game.cpp @@ -266,10 +266,6 @@ void TabGame::resetChatAndPhase() // reset phase markers game->getGameState()->setCurrentPhase(-1); - - // reset spectator state so the replay can rebuild it from the start - game->getPlayerManager()->clearSpectators(); - playerListWidget->clearSpectators(); } void TabGame::emitUserEvent() diff --git a/cockatrice/src/interface/widgets/tabs/tab_message.cpp b/cockatrice/src/interface/widgets/tabs/tab_message.cpp index 418843178..9506d96f3 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_message.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_message.cpp @@ -98,12 +98,6 @@ void TabMessage::closeEvent(QCloseEvent *event) void TabMessage::sendPrivateMessage(const QString &text) { - if (tabSupervisor->getUserListManager()->isUserIgnored(getUserName())) { - chatView->appendMessage(tr("You have ignored %1; your messages are not delivered.") - .arg(QString::fromStdString(otherUserInfo->name()))); - return; - } - Command_Message cmd; cmd.set_user_name(otherUserInfo->name()); cmd.set_message(text.toStdString()); diff --git a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp index bb8ec719e..1b61bf80f 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp @@ -117,10 +117,9 @@ void CloseButton::paintEvent(QPaintEvent * /*event*/) } TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget *parent) - : QTabWidget(parent), userInfo(nullptr), client(_client), tabsMenu(tabsMenu), tabHome(nullptr), - tabVisualDeckStorage(nullptr), tabServer(nullptr), tabAccount(nullptr), tabDeckStorage(nullptr), - tabReplays(nullptr), tabAdmin(nullptr), tabCardArtRules(nullptr), tabLog(nullptr), tabReport(nullptr), - tabModeration(nullptr), tabDeveloper(nullptr), isLocalGame(false) + : QTabWidget(parent), userInfo(nullptr), client(_client), tabsMenu(tabsMenu), tabVisualDeckStorage(nullptr), + tabServer(nullptr), tabAccount(nullptr), tabDeckStorage(nullptr), tabReplays(nullptr), tabAdmin(nullptr), + tabLog(nullptr), tabReport(nullptr), tabModeration(nullptr), tabDeveloper(nullptr), isLocalGame(false) { setElideMode(Qt::ElideRight); setMovable(true); @@ -251,7 +250,6 @@ void TabSupervisor::retranslateUi() aTabLog->setText(tr("Logs")); aTabReport->setText(tr("Report Queue")); aTabModeration->setText(tr("Moderation")); - aTabCardArtRules->setText(tr("Card Art Rules")); aTabDeveloper->setText(tr("Developer")); // tabs @@ -264,7 +262,6 @@ void TabSupervisor::retranslateUi() tabs.append(tabLog); tabs.append(tabReport); tabs.append(tabModeration); - tabs.append(tabCardArtRules); tabs.append(tabDeveloper); QMapIterator roomIterator(roomTabs); while (roomIterator.hasNext()) { @@ -530,9 +527,7 @@ void TabSupervisor::start(const ServerInfo_User &_userInfo) if (SettingsCache::instance().tabs().getTabModerationOpen()) { openTabModeration(); } - if (SettingsCache::instance().tabs().getTabCardArtRulesOpen()) { - openTabCardArtRules(); - } + openTabCardArtRules(); } if (userInfo->user_level() & ServerInfo_User::IsDeveloper) { @@ -608,9 +603,6 @@ void TabSupervisor::stop() if (tabModeration) { tabModeration->close(); } - if (tabCardArtRules) { - tabCardArtRules->close(); - } if (tabDeveloper) { tabDeveloper->close(); } @@ -807,7 +799,6 @@ void TabSupervisor::openTabAdmin() void TabSupervisor::actTabCardArtRules(bool checked) { - SettingsCache::instance().tabs().setTabCardArtRulesOpen(checked); if (checked && !tabCardArtRules) { openTabCardArtRules(); setCurrentWidget(tabCardArtRules); @@ -1114,13 +1105,6 @@ TabMessage *TabSupervisor::addMessageTab(const QString &receiverName, bool focus return tab; } - if (focus && userListManager->isUserIgnored(receiverName)) { - QMessageBox::information( - this, tr("Ignored user"), - tr("You have ignored %1. Remove them from your ignore list to open a private chat.").arg(receiverName)); - return nullptr; - } - tab = new TabMessage(this, client, *userInfo, otherUser, userOnline); connect(tab, &TabMessage::talkClosing, this, &TabSupervisor::talkLeft); connect(tab, &TabMessage::maximizeClient, this, &TabSupervisor::maximizeMainWindow); @@ -1335,21 +1319,7 @@ void TabSupervisor::processGameEventContainer(const GameEventContainer &cont) void TabSupervisor::processUserMessageEvent(const Event_UserMessage &event) { - // "Ignore all private messages" silences every PM, including messages to - // already-open tabs — unlike the unregistered/non-buddy filters below, - // which only apply when creating a new tab. Messages from moderators/admins - // are exempt to ensure warnings still reach users. QString senderName = QString::fromStdString(event.sender_name()); - if (SettingsCache::instance().chat().getIgnoreAllPrivateMessages()) { - const ServerInfo_User *onlineUserInfo = userListManager->getOnlineUser(senderName); - if (!onlineUserInfo) { - return; - } - const UserLevelFlags userLevel(onlineUserInfo->user_level()); - if (!userLevel.testFlag(ServerInfo_User::IsModerator) && !userLevel.testFlag(ServerInfo_User::IsAdmin)) { - return; - } - } TabMessage *tab = messageTabs.value(senderName); if (!tab) { tab = messageTabs.value(QString::fromStdString(event.receiver_name())); diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp index 0f43893d3..209a30642 100644 --- a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp +++ b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp @@ -4,7 +4,6 @@ #include "../../../../client/settings/shortcuts_settings.h" #include "../../cards/card_info_display_widget.h" #include "../../deck_editor/deck_state_manager.h" -#include "../../deck_editor/deck_zone_dialog.h" #include "../../filters/filter_builder.h" #include "../../interface/pixel_map_generator.h" #include "../../interface/widgets/cards/card_info_frame_widget.h" @@ -85,7 +84,6 @@ void TabDeckEditorVisual::createCentralFrame() connect(tabContainer, &TabDeckEditorVisualTabWidget::printingSelectorRequested, this, &TabDeckEditorVisual::showPrintingSelector); connect(tabContainer, &TabDeckEditorVisualTabWidget::cardInfoRequested, this, &TabDeckEditorVisual::updateCardInfo); - tabContainer->visualDatabaseDisplay->setNewZoneCreator([this] { return createNewZone(); }); centralFrame->addWidget(tabContainer); setCentralWidget(centralWidget); @@ -271,19 +269,6 @@ bool TabDeckEditorVisual::actSaveDeckAs() return result; } -/** @brief Prompts for and creates a new custom deck zone. Returns the name of the created zone. */ -QString TabDeckEditorVisual::createNewZone() -{ - QString boardName; - const QString zoneName = DeckZoneDialog::promptForNewZone(this, {}, &boardName, [this](const QString &candidate) { - return deckStateManager->validateNewZoneName(candidate); - }); - if (!zoneName.isEmpty()) { - deckStateManager->createCustomZone(boardName, zoneName); - } - return zoneName; -} - /** @brief Refreshes keyboard shortcuts for this tab from settings. */ void TabDeckEditorVisual::refreshShortcuts() { diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.h b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.h index fb09578c4..21335d2d0 100644 --- a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.h +++ b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.h @@ -165,12 +165,6 @@ public slots: */ bool actSaveDeckAs() override; - /** - * @brief Prompts for and creates a new custom deck zone. - * @return The name of the created zone, or an empty string if creation was cancelled. - */ - QString createNewZone(); - private: /** * @brief Sets the deck for this tab and selects the sub-tab to open on 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 76bbf344b..0cdf60d5d 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 @@ -21,7 +21,6 @@ #include #include #include -#include #include #include @@ -90,19 +89,6 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent, databaseView->setItemDelegate(nullptr); databaseView->setVisible(false); - // Without a deck model there is nothing to add cards to, so the zone menu stays hidden. - if (deckListModel) { - databaseView->setZoneMenuProvider( - [deckListModel]() -> QList> { - QList> result; - for (const QString &boardName : InnerDecklistNode::boardZoneNames()) { - result.append({boardName, deckListModel->getCustomZoneNames(boardName)}); - } - return result; - }, - [this] { return newZoneCreator ? newZoneCreator() : QString(); }); - } - searchEdit->setTreeView(databaseView); searchEdit->installEventFilter(databaseView->getKeySignals()); @@ -209,11 +195,6 @@ void VisualDatabaseDisplayWidget::showEvent(QShowEvent *event) initializeFilters(); } -void VisualDatabaseDisplayWidget::setNewZoneCreator(const std::function &creator) -{ - newZoneCreator = creator; -} - void VisualDatabaseDisplayWidget::retranslateUi() { databaseLoadIndicator->setText(tr("Loading database ...")); 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 d161ce362..6e4d87876 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 @@ -22,7 +22,6 @@ #include #include #include -#include #include #include #include @@ -47,12 +46,6 @@ public: 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. - */ - void setNewZoneCreator(const std::function &creator); - CardDatabaseDisplayModel *getDatabaseDisplayModel() { return databaseDisplayModel; @@ -113,7 +106,6 @@ private: VisualDatabaseDisplayFilterToolbarWidget *filterContainer; CardDatabaseDisplayModel *databaseDisplayModel; CardDatabaseView *databaseView; - std::function newZoneCreator; QList *cards; QVBoxLayout *mainLayout; QScrollArea *scrollArea; diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.cpp index 22d73b604..fbaabf90f 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.cpp @@ -125,7 +125,9 @@ void VisualDeckStorageFolderDisplayWidget::continueDeckPass() } const bool matches = index.data(VisualDeckStorageRoles::FilterMatchRole).toBool(); - deckPreviewWidget->setVisible(matches); + if (matches == deckPreviewWidget->isHidden()) { + deckPreviewWidget->setVisible(matches); + } if (matches) { ++visibleDeckCount; } diff --git a/cockatrice/src/interface/window_main.cpp b/cockatrice/src/interface/window_main.cpp index 43f1de9dd..4567991c8 100644 --- a/cockatrice/src/interface/window_main.cpp +++ b/cockatrice/src/interface/window_main.cpp @@ -682,7 +682,6 @@ void MainWindow::runFirstRunWizard() connect(wizard, &FirstRunWizard::cardDatabaseUpdateRequested, this, &MainWindow::actCheckCardUpdatesBackground); connect(wizard, &FirstRunWizard::manualCardDatabaseSetupRequested, this, &MainWindow::actCheckCardUpdates); connect(this, &MainWindow::cardDatabaseUpdateFinished, wizard, &FirstRunWizard::onCardDatabaseUpdateFinished); - connect(this, &MainWindow::cardDatabaseUpdateProgress, wizard, &FirstRunWizard::onCardDatabaseUpdateProgress); connect(wizard, &FirstRunWizard::registerRequested, connectionController, &ConnectionController::registerToServer); connect(wizard, &FirstRunWizard::connectRequested, connectionController, &ConnectionController::connectToServer); @@ -844,17 +843,6 @@ void MainWindow::closeEvent(QCloseEvent *event) } bClosingDown = true; - if (cardUpdateProcess && cardUpdateProcess->state() != QProcess::NotRunning) { - if (QMessageBox::question(this, tr("Are you sure?"), - tr("A card database update is still running. Quitting now will cancel it.\n" - "Are you sure you want to quit?"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::No) { - event->ignore(); - bClosingDown = false; - return; - } - } - if (!tabSupervisor->close()) { event->ignore(); bClosingDown = false; @@ -1069,45 +1057,11 @@ void MainWindow::createCardUpdateProcess(bool background) if (!background) { cardUpdateProcess->start(updaterCmd, QStringList()); } else { - cardUpdateOutputBuffer.clear(); - connect(cardUpdateProcess, &QProcess::readyReadStandardOutput, this, &MainWindow::cardUpdateProgressOutput); cardUpdateProcess->start(updaterCmd, QStringList("-b")); statusBar()->showMessage(tr("Card database update running.")); } } -void MainWindow::cardUpdateProgressOutput() -{ - if (!cardUpdateProcess) { - return; - } - cardUpdateOutputBuffer.append(cardUpdateProcess->readAllStandardOutput()); - while (true) { - const int newline = cardUpdateOutputBuffer.indexOf('\n'); - if (newline < 0) { - break; - } - const QByteArray line = cardUpdateOutputBuffer.left(newline).trimmed(); - cardUpdateOutputBuffer.remove(0, newline + 1); - // Protocol emitted by `oracle -b`: "PROGRESS " - if (!line.startsWith("PROGRESS ")) { - continue; - } - const QList parts = line.split(' '); - if (parts.size() != 4) { - continue; - } - bool doneOk = false; - bool totalOk = false; - const qint64 done = parts.at(2).toLongLong(&doneOk); - const qint64 total = parts.at(3).toLongLong(&totalOk); - if (!doneOk || !totalOk || done < 0 || total < 0) { - continue; - } - emit cardDatabaseUpdateProgress(QString::fromLatin1(parts.at(1)), done, total); - } -} - void MainWindow::exitCardDatabaseUpdate() { if (!cardUpdateProcess) { @@ -1155,8 +1109,6 @@ void MainWindow::cardUpdateError(QProcess::ProcessError err) void MainWindow::cardUpdateFinished(int exitCode, QProcess::ExitStatus exitStatus) { - cardUpdateProgressOutput(); // drain any progress lines not yet parsed - const bool success = (exitStatus == QProcess::NormalExit) && (exitCode == 0); if (exitStatus == QProcess::NormalExit) { SettingsCache::instance().updates().setLastCardUpdateCheck(QDateTime::currentDateTime().date()); diff --git a/cockatrice/src/interface/window_main.h b/cockatrice/src/interface/window_main.h index 08481fd36..920145552 100644 --- a/cockatrice/src/interface/window_main.h +++ b/cockatrice/src/interface/window_main.h @@ -68,11 +68,6 @@ signals: /** @brief Emitted after the background card-database update subprocess exits. */ void cardDatabaseUpdateFinished(bool success); - /** @brief Emitted while the background card-database update subprocess runs. - * @p stage is one of "download", "scan" or "import"; @p done/@p total - * are byte counts for the first two stages and set indices for "import". */ - void cardDatabaseUpdateProgress(const QString &stage, qint64 done, qint64 total); - public slots: void actCheckCardUpdates(); void actCheckCardUpdatesBackground(); @@ -101,7 +96,6 @@ private slots: void cardUpdateError(QProcess::ProcessError err); void cardUpdateFinished(int exitCode, QProcess::ExitStatus exitStatus); - void cardUpdateProgressOutput(); void refreshShortcuts(); void cardDatabaseLoadingFailed(); void cardDatabaseNewSetsFound(int numUnknownSets, QStringList unknownSetsNames); @@ -165,7 +159,6 @@ private: LatencyStatusWidget *latencyStatus = nullptr; ///< status bar widget with live round-trip stats and history graph bool bHasActivated, askedForDbUpdater; QProcess *cardUpdateProcess; - QByteArray cardUpdateOutputBuffer; DlgViewLog *logviewDialog; GameReplay *replay; DlgTipOfTheDay *tip; diff --git a/cockatrice/src/main.cpp b/cockatrice/src/main.cpp index d8aa1cd08..84d5d175f 100644 --- a/cockatrice/src/main.cpp +++ b/cockatrice/src/main.cpp @@ -53,7 +53,6 @@ #include #include #include -#include QTranslator *translator, *qtTranslator; RNG_Abstract *rng; @@ -293,7 +292,7 @@ int main(int argc, char *argv[]) } } - rng = new RNG_SFMT(CryptoUtil::randomUInt64()); + rng = new RNG_SFMT; themeManager = new ThemeManager; soundEngine = new SoundEngine; diff --git a/format.sh b/format.sh index 9e3a6069b..3fa435be1 100755 --- a/format.sh +++ b/format.sh @@ -18,11 +18,11 @@ include=("cockatrice/src" \ libcockatrice_* \ "oracle/src" \ "servatrice/src" \ -"cmake/pch" \ "tests") exclude=("libcockatrice_rng/libcockatrice/rng/sfmt/" \ "libcockatrice_utility/libcockatrice/utility/peglib.h" \ "oracle/src/lzma/" \ +"oracle/src/qt-json/" \ "oracle/src/zip/" \ "servatrice/src/smtp/") exts=("cpp" "h" "proto") diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.h b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.h index 5d91cd233..af1193f26 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.h +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.h @@ -115,25 +115,6 @@ public: */ QList getCustomZones(const QString &boardZoneName) const; - /** - * @brief Checks whether a zone name is taken anywhere in the deck. - * - * Covers the standard board names and any top-level or nested custom zone. - * @param zoneName The checked name. - * @return true if the name is reserved or already in use. - */ - bool hasZoneName(const QString &zoneName) const; - - /** - * @brief Finds a custom zone anywhere in the deck by name. - * - * Walks the children of every top-level zone, so a zone nested under any - * board (and not just the standard ones) is found. - * @param zoneName The zone name to find. - * @return The matching zone node, or nullptr if none exists. - */ - InnerDecklistNode *findCustomZoneByName(const QString &zoneName) const; - /** * @brief Applies a function to every card in the deck tree. This can modify the cards. * @@ -147,6 +128,8 @@ private: InnerDecklistNode *getZoneObjFromName(const QString &zoneName) const; InnerDecklistNode *findBoardZone(const QString &boardZoneName) const; InnerDecklistNode *findOrCreateBoardZone(const QString &boardZoneName); + InnerDecklistNode *findCustomZoneByName(const QString &zoneName) const; + bool hasZoneName(const QString &zoneName) const; }; #endif // COCKATRICE_DECKLIST_NODE_TREE_H diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.cpp index d082b3cca..ec860dc56 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.cpp +++ b/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.cpp @@ -43,13 +43,6 @@ void InnerDecklistNode::setSortMethod(DeckSortMethod method) } } -const QList &InnerDecklistNode::boardZoneNames() -{ - static const QList names = {QString(DECK_ZONE_MAIN), QString(DECK_ZONE_SIDE), - QString(DECK_ZONE_MAYBEBOARD)}; - return names; -} - QString InnerDecklistNode::getVisibleName() const { return visibleNameFromName(name); @@ -94,9 +87,6 @@ AbstractDecklistNode *InnerDecklistNode::findCardChildByNameProviderIdAndNumber( int InnerDecklistNode::height() const { - if (isEmpty()) { - return 1; - } return at(0)->height() + 1; } diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.h b/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.h index 0d454c11e..906ed6cb5 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.h +++ b/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.h @@ -18,9 +18,6 @@ #include "abstract_deck_list_node.h" -#include -#include - /** @brief Constant for the "main" deck zone name. */ #define DECK_ZONE_MAIN "main" /** @brief Constant for the "sideboard" zone name. */ @@ -121,13 +118,6 @@ public: */ static QString visibleNameFromName(const QString &_name); - /** - * @brief The standard board zone names, in display order. - * - * @return main, side and maybeboard. - */ - static const QList &boardZoneNames(); - /** * @brief Get this node’s display-friendly name. * @return Human-readable name (zone/group name). diff --git a/libcockatrice_filters/libcockatrice/filters/filter_string.cpp b/libcockatrice_filters/libcockatrice/filters/filter_string.cpp index aaf391c03..25e8e97db 100644 --- a/libcockatrice_filters/libcockatrice/filters/filter_string.cpp +++ b/libcockatrice_filters/libcockatrice/filters/filter_string.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include static peg::parser search(R"( @@ -20,7 +19,7 @@ SomewhatComplexQueryPart <- [(] QueryPartList [)] / QueryPart QueryPart <- NotQuery / SetQuery / RarityQuery / CMCQuery / FormatQuery / PowerQuery / ToughnessQuery / ColorQuery / TypeQuery / OracleQuery / FieldQuery / GenericQuery NotQuery <- ('NOT' ws/'-') SomewhatComplexQueryPart -SetQuery <- ('e'/'set') SetExpression / ([:] FlexStringValue) +SetQuery <- ('e'/'set') [:] FlexStringValue OracleQuery <- 'o' [:] MatcherString @@ -65,8 +64,6 @@ RegexMatcherString <- ('\\/' / !'/' .)+ FlexStringValue <- CompactStringSet / String / [(] StringList [)] CompactStringSet <- StringListString ([,+] StringListString)+ -SetExpression <- NumericOperator ws? String - NumericExpression <- NumericOperator ws? NumericValue NumericOperator <- [=:] / <[> NumericValue <- [0-9]+ @@ -104,25 +101,12 @@ static void setupParserRules() return [=](const CardData &x) -> bool { return matcher(x->getCardType()); }; }; search["SetQuery"] = [](const peg::SemanticValues &sv) -> Filter { - if (sv.choice() == 1) { - auto matcher = std::any_cast(sv[0]); - return [=](const CardData &x) -> bool { - QList sets = x->getSets().keys(); - - auto matchesSet = [&matcher](const QString &set) { return matcher(set); }; - return std::any_of(sets.begin(), sets.end(), matchesSet); - }; - } - - auto matcher = std::any_cast(sv[0]); + auto matcher = std::any_cast(sv[0]); return [=](const CardData &x) -> bool { - const auto &sets = x->getSets().values(); - auto matchesSet = [&](const PrintingInfo &printing) { - return printing.getSet()->getEnabled() && matcher(printing.getSet()->getReleaseDate().toJulianDay()); - }; - return std::any_of(sets.begin(), sets.end(), [&](const auto &printings) { - return std::any_of(printings.begin(), printings.end(), matchesSet); - }); + QList sets = x->getSets().keys(); + + auto matchesSet = [&matcher](const QString &set) { return matcher(set); }; + return std::any_of(sets.begin(), sets.end(), matchesSet); }; }; search["Rarity"] = [](const peg::SemanticValues &sv) -> QString { @@ -263,54 +247,40 @@ static void setupParserRules() return QString::fromStdString(std::string(sv.sv())); }; - search["NumericOperator"] = [](const peg::SemanticValues &sv) -> NumberComparer { - const auto op = QString::fromStdString(std::string(sv.sv())); + search["NumericExpression"] = [](const peg::SemanticValues &sv) -> NumberMatcher { + const auto arg = std::any_cast(sv[1]); + const auto op = std::any_cast(sv[0]); if (op == ">") { - return [=](const int s, const int arg) { return s > arg; }; + return [=](const int s) { return s > arg; }; } if (op == ">=") { - return [=](const int s, const int arg) { return s >= arg; }; + return [=](const int s) { return s >= arg; }; } if (op == "<") { - return [=](const int s, const int arg) { return s < arg; }; + return [=](const int s) { return s < arg; }; } if (op == "<=") { - return [=](const int s, const int arg) { return s <= arg; }; + return [=](const int s) { return s <= arg; }; } if (op == "=") { - return [=](const int s, const int arg) { return s == arg; }; + return [=](const int s) { return s == arg; }; } if (op == ":") { - return [=](const int s, const int arg) { return s == arg; }; + return [=](const int s) { return s == arg; }; } if (op == "!=") { - return [=](const int s, const int arg) { return s != arg; }; + return [=](const int s) { return s != arg; }; } - return [](int, int) { return false; }; + return [](int) { return false; }; }; search["NumericValue"] = [](const peg::SemanticValues &sv) -> int { return QString::fromStdString(std::string(sv.sv())).toInt(); }; - search["NumericExpression"] = [](const peg::SemanticValues &sv) -> NumberMatcher { - const auto comparer = std::any_cast(sv[0]); - const auto arg = std::any_cast(sv[1]); - return [=](int s) { return comparer(s, arg); }; - }; - - search["SetExpression"] = [](const peg::SemanticValues &sv) -> NumberMatcher { - const auto comparer = std::any_cast(sv[0]); - const auto setCode = std::any_cast(sv[1]); - const auto allSets = CardDatabaseManager::getInstance()->getSetList(); - for (auto &set : allSets) { - if (set->getShortName() == setCode) { - const int releaseDate = set->getReleaseDate().toJulianDay(); - return [=](int s) { return comparer(s, releaseDate); }; - } - } - return [](int) { return false; }; + search["NumericOperator"] = [](const peg::SemanticValues &sv) -> QString { + return QString::fromStdString(std::string(sv.sv())); }; search["NormalMatcher"] = [](const peg::SemanticValues &sv) -> StringMatcher { diff --git a/libcockatrice_filters/libcockatrice/filters/filter_string.h b/libcockatrice_filters/libcockatrice/filters/filter_string.h index a058f7d07..71a99f7b5 100644 --- a/libcockatrice_filters/libcockatrice/filters/filter_string.h +++ b/libcockatrice_filters/libcockatrice/filters/filter_string.h @@ -22,7 +22,6 @@ typedef CardInfoPtr CardData; typedef std::function Filter; typedef std::function StringMatcher; typedef std::function NumberMatcher; -typedef std::function NumberComparer; namespace peg { diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/interface_chat_settings_provider.h b/libcockatrice_interfaces/libcockatrice/interfaces/interface_chat_settings_provider.h index cdf2da5eb..cd9ad29e1 100644 --- a/libcockatrice_interfaces/libcockatrice/interfaces/interface_chat_settings_provider.h +++ b/libcockatrice_interfaces/libcockatrice/interfaces/interface_chat_settings_provider.h @@ -20,7 +20,6 @@ public: [[nodiscard]] virtual bool getShowMessagePopup() const = 0; [[nodiscard]] virtual bool getShowMentionPopup() const = 0; [[nodiscard]] virtual bool getRoomHistory() const = 0; - [[nodiscard]] virtual bool getIgnoreAllPrivateMessages() const = 0; [[nodiscard]] virtual QString getHighlightWords() const = 0; }; diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/interface_tabs_settings_provider.h b/libcockatrice_interfaces/libcockatrice/interfaces/interface_tabs_settings_provider.h index 054c4cd72..a81616cb0 100644 --- a/libcockatrice_interfaces/libcockatrice/interfaces/interface_tabs_settings_provider.h +++ b/libcockatrice_interfaces/libcockatrice/interfaces/interface_tabs_settings_provider.h @@ -21,7 +21,6 @@ public: [[nodiscard]] virtual bool getTabLogOpen() const = 0; [[nodiscard]] virtual bool getTabReportOpen() const = 0; [[nodiscard]] virtual bool getTabModerationOpen() const = 0; - [[nodiscard]] virtual bool getTabCardArtRulesOpen() const = 0; }; #endif // COCKATRICE_INTERFACE_TABS_SETTINGS_PROVIDER_H diff --git a/libcockatrice_models/libcockatrice/models/deck_list/CMakeLists.txt b/libcockatrice_models/libcockatrice/models/deck_list/CMakeLists.txt index a6ab2a204..d4aee3686 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/CMakeLists.txt +++ b/libcockatrice_models/libcockatrice/models/deck_list/CMakeLists.txt @@ -7,8 +7,7 @@ set(HEADERS deck_list_model.h deck_list_sort_filter_proxy_model.h) qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) add_library( - libcockatrice_models_deck_list STATIC ${MOC_SOURCES} deck_list_model.cpp deck_list_model_custom_zones.cpp - deck_list_sort_filter_proxy_model.cpp + libcockatrice_models_deck_list STATIC ${MOC_SOURCES} deck_list_model.cpp deck_list_sort_filter_proxy_model.cpp ) target_include_directories(libcockatrice_models_deck_list PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) 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 76afca0c4..9b43281c1 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp @@ -66,8 +66,7 @@ void DeckListModel::rebuildTree() for (int j = 0; j < currentZone->size(); j++) { auto *currentCard = dynamic_cast(currentZone->at(j)); - // Non-card children are custom zones; they are mirrored in a single - // pass below so each is mirrored exactly once. + //! \todo Better sanity checking. if (currentCard == nullptr) { continue; } @@ -83,19 +82,8 @@ void DeckListModel::rebuildTree() new DecklistModelCardNode(currentCard, groupNode); } - - // Custom zones nested under the board zone are mirrored as-is, with their - // cards as direct children (no further grouping). - DeckListModelCustomZones::mirrorCustomZones(currentZone, node); } - // The shadow tree was built in deck file order. Apply the active sort while - // the reset is still open so every consumer (tree view and visual editor) - // sees the canonical order from the start. sortShadowTree emits no signals, - // which is only valid before endResetModel closes the reset. - root->setSortMethod(lastKnownColumn == 0 ? DeckSortMethod::ByNumber : DeckSortMethod::ByName); - sortShadowTree(root, lastKnownOrder); - endResetModel(); refreshCardFormatLegalities(); @@ -166,9 +154,6 @@ QVariant DeckListModel::data(const QModelIndex &index, int role) const case DeckRoles::IsLegalRole: return true; - case DeckRoles::IsCustomZoneRole: - return DeckListModelCustomZones::isCustomZone(group); - default: return {}; } @@ -205,10 +190,6 @@ QVariant DeckListModel::data(const QModelIndex &index, int role) const return card->getFormatLegality(); } - case DeckRoles::IsCustomZoneRole: { - return false; - } - default: { return {}; } @@ -346,13 +327,6 @@ bool DeckListModel::removeRows(int row, int count, const QModelIndex &parent) return false; } - // Custom zone rows are managed through the deck tree, never removed as model rows. - for (int i = 0; i < count; i++) { - if (DeckListModelCustomZones::isCustomZone(node->at(row + i))) { - return false; - } - } - beginRemoveRows(parent, row, row + count - 1); for (int i = 0; i < count; i++) { AbstractDecklistNode *toDelete = node->takeAt(row); @@ -363,8 +337,7 @@ bool DeckListModel::removeRows(int row, int count, const QModelIndex &parent) } endRemoveRows(); - // Empty criteria groups get pruned, but custom zones stay until explicitly deleted. - if (node->empty() && (node != root) && !DeckListModelCustomZones::isCustomZone(node)) { + if (node->empty() && (node != root)) { removeRows(parent.row(), 1, parent.parent()); } else { emitRecursiveUpdates(parent); @@ -378,8 +351,7 @@ bool DeckListModel::removeRows(int row, int count, const QModelIndex &parent) InnerDecklistNode *DeckListModel::createNodeIfNeeded(const QString &name, InnerDecklistNode *parent) { - // Group lookups must not resolve a mirrored custom zone that shares the name. - auto *newNode = DeckListModelCustomZones::findGroupChild(parent, name); + auto *newNode = dynamic_cast(parent->findChild(name)); if (!newNode) { beginInsertRows(nodeToIndex(parent), parent->size(), parent->size()); newNode = new InnerDecklistNode(name, parent); @@ -393,44 +365,24 @@ DecklistModelCardNode *DeckListModel::findCardNode(const QString &cardName, const QString &providerId, const QString &cardNumber) const { + InnerDecklistNode *zoneNode = dynamic_cast(root->findChild(zoneName)); + if (!zoneNode) { + return nullptr; + } + CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(cardName); if (!info) { return nullptr; } - // 1. Board zone lookup: search the criteria groups, then the custom zones - // nested under the board. - if (auto *zoneNode = dynamic_cast(root->findChild(zoneName))) { - QString groupCriteria = extractGroupCriteriaValue(info, activeGroupCriteria); - if (auto *groupNode = DeckListModelCustomZones::findGroupChild(zoneNode, groupCriteria)) { - if (auto *card = dynamic_cast( - groupNode->findCardChildByNameProviderIdAndNumber(cardName, providerId, cardNumber))) { - return card; - } - } - - for (auto *child : *zoneNode) { - if (!DeckListModelCustomZones::isCustomZone(child)) { - continue; - } - auto *customZone = dynamic_cast(child); - if (!customZone) { - continue; - } - if (auto *card = dynamic_cast( - customZone->findCardChildByNameProviderIdAndNumber(cardName, providerId, cardNumber))) { - return card; - } - } + QString groupCriteria = extractGroupCriteriaValue(info, activeGroupCriteria); + InnerDecklistNode *groupNode = dynamic_cast(zoneNode->findChild(groupCriteria)); + if (!groupNode) { + return nullptr; } - // 2. Custom zone lookup by name (custom zone names are deck-unique). - if (auto *customZone = DeckListModelCustomZones::findSubZoneByName(root, zoneName)) { - return dynamic_cast( - customZone->findCardChildByNameProviderIdAndNumber(cardName, providerId, cardNumber)); - } - - return nullptr; + return dynamic_cast( + groupNode->findCardChildByNameProviderIdAndNumber(cardName, providerId, cardNumber)); } QModelIndex DeckListModel::findCard(const QString &cardName, @@ -471,95 +423,29 @@ QModelIndex DeckListModel::addCard(const ExactCard &card, const QString &zoneNam return {}; } + InnerDecklistNode *zoneNode = createNodeIfNeeded(zoneName, root); + CardInfoPtr cardInfo = card.getCardPtr(); PrintingInfo printingInfo = card.getPrinting(); - InnerDecklistNode *cardParent = nullptr; + QString groupCriteria = extractGroupCriteriaValue(cardInfo, activeGroupCriteria); + InnerDecklistNode *groupNode = createNodeIfNeeded(groupCriteria, zoneNode); - auto *boardNode = dynamic_cast(root->findChild(zoneName)); - auto *customZoneNode = boardNode ? nullptr : DeckListModelCustomZones::findSubZoneByName(root, zoneName); - - // Mirroring flattens nested deck sub-zones into shadow rows, so a shadow row - // index is only usable as a deck-tree position while both sides have the same - // direct-children shape. When they diverge, the card is appended to the deck - // zone instead of being written out of range. - InnerDecklistNode *deckCardParent = nullptr; - bool customZoneNeedsAppend = false; - - if (boardNode) { - // Board zone: cards are grouped by the active criteria. - QString groupCriteria = extractGroupCriteriaValue(cardInfo, activeGroupCriteria); - cardParent = createNodeIfNeeded(groupCriteria, boardNode); - } else if (customZoneNode) { - // Custom zone: cards live flat inside the zone. - cardParent = customZoneNode; - auto *listRoot = deckList->getTree()->getRoot(); - for (int i = 0; i < listRoot->size(); ++i) { - auto *boardZone = dynamic_cast(listRoot->at(i)); - if (!boardZone) { - continue; - } - deckCardParent = dynamic_cast(boardZone->findChild(zoneName)); - if (deckCardParent) { - break; - } - } - // A deck custom zone holding nested sub-zones mirrors with flattened rows, - // so a shadow row index does not map onto its direct children. - if (deckCardParent) { - for (int i = 0; i < deckCardParent->size(); ++i) { - if (dynamic_cast(deckCardParent->at(i))) { - customZoneNeedsAppend = true; - break; - } - } - } - } else { - // Not present in the shadow tree. The deck tree may still hold a custom - // zone that has not been mirrored (callers can add a zone and then a - // card without a rebuild). Check before falling back to creating a - // top-level zone the deck does not actually have. - auto *listRoot = deckList->getTree()->getRoot(); - bool hasDeckZone = false; - for (int i = 0; i < listRoot->size(); ++i) { - if (auto *boardZone = dynamic_cast(listRoot->at(i))) { - // Only real zones count: a card sitting directly under the board - // shares the name comparison but is not a zone, and treating it as - // one would recurse forever without mirroring anything. - if (dynamic_cast(boardZone->findChild(zoneName))) { - hasDeckZone = true; - break; - } - } - } - - if (hasDeckZone) { - rebuildTree(); - return addCard(card, zoneName); - } - - // Unknown zone: create a top-level zone (legacy behavior). - QString groupCriteria = extractGroupCriteriaValue(cardInfo, activeGroupCriteria); - auto *newZone = createNodeIfNeeded(zoneName, root); - cardParent = createNodeIfNeeded(groupCriteria, newZone); - } - - const QModelIndex parentIndex = nodeToIndex(cardParent); - auto *cardNode = dynamic_cast(cardParent->findCardChildByNameProviderIdAndNumber( + const QModelIndex parentIndex = nodeToIndex(groupNode); + auto *cardNode = dynamic_cast(groupNode->findCardChildByNameProviderIdAndNumber( card.getName(), printingInfo.getUuid(), printingInfo.getProperty("num"))); const auto cardSetName = printingInfo.getSet().isNull() ? "" : printingInfo.getSet()->getCorrectedShortName(); bool cardNodeAdded = false; if (!cardNode) { // Determine the correct index - int insertRow = findSortedInsertRow(cardParent, cardInfo); - int deckInsertRow = customZoneNeedsAppend ? -1 : insertRow; + int insertRow = findSortedInsertRow(groupNode, cardInfo); - auto *decklistCard = deckList->addCard(cardInfo->getName(), zoneName, deckInsertRow, cardSetName, + auto *decklistCard = deckList->addCard(cardInfo->getName(), zoneName, insertRow, cardSetName, printingInfo.getProperty("num"), printingInfo.getProperty("uuid")); beginInsertRows(parentIndex, insertRow, insertRow); - cardNode = new DecklistModelCardNode(decklistCard, cardParent, insertRow); + cardNode = new DecklistModelCardNode(decklistCard, groupNode, insertRow); endInsertRows(); cardNodeAdded = true; @@ -690,41 +576,21 @@ QModelIndex DeckListModel::nodeToIndex(AbstractDecklistNode *node) const return createIndex(node->getParent()->indexOf(node), 0, node); } -/** - * @brief Sorts a freshly built shadow subtree without emitting model signals. - * - * Used by rebuildTree while the model reset is still open (emitting layout - * changes during a reset is invalid). Reorders every node just like - * sortHelper does, but ignores the movement mapping because there are no - * persistent indices established yet. - */ -void DeckListModel::sortShadowTree(InnerDecklistNode *node, Qt::SortOrder order) -{ - // The mapping is not needed: fresh shadow nodes have no persistent indices yet. - (void)DeckListModelCustomZones::sortWithCustomZonesLast(root, node, order); - - for (int i = node->size() - 1; i >= 0; --i) { - if (auto *subNode = dynamic_cast(node->at(i))) { - sortShadowTree(subNode, order); - } - } -} - void DeckListModel::sortHelper(InnerDecklistNode *node, Qt::SortOrder order) { - // Sort children (custom zones always sorted after groups within a board) and - // use the movement mapping to update the list of persistent indices. - const auto mapping = DeckListModelCustomZones::sortWithCustomZonesLast(root, node, order); + // Sort children of node and save the information needed to + // update the list of persistent indexes. + QVector> sortResult = node->sort(order); QModelIndexList from, to; int columns = columnCount(); - for (const auto &move : mapping) { - const int preSortRow = move.first; - const int finalRow = move.second; - AbstractDecklistNode *temp = node->at(finalRow); + for (int i = sortResult.size() - 1; i >= 0; --i) { + const int fromRow = sortResult[i].first; + const int toRow = sortResult[i].second; + AbstractDecklistNode *temp = node->at(toRow); for (int j = 0; j < columns; ++j) { - from << createIndex(preSortRow, j, temp); - to << createIndex(finalRow, j, temp); + from << createIndex(fromRow, j, temp); + to << createIndex(toRow, j, temp); } } changePersistentIndexList(from, to); @@ -838,15 +704,6 @@ QList DeckListModel::getZones() const return zones; } -QStringList DeckListModel::getCustomZoneNames(const QString &boardZoneName) const -{ - QStringList zoneNames; - for (const auto *customZone : deckList->getTree()->getCustomZones(boardZoneName)) { - zoneNames.append(customZone->getName()); - } - return zoneNames; -} - static int maxAllowedForLegality(const FormatRules &format, const QString &legality) { for (const AllowedCount &c : format.allowedCounts) { diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h index 09600ca67..209ec8c42 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h @@ -1,8 +1,6 @@ #ifndef DECKLISTMODEL_H #define DECKLISTMODEL_H -#include "deck_list_model_custom_zones.h" - #include <../../../../libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.h> #include <../../../../libcockatrice_deck_list/libcockatrice/deck_list/tree/deck_list_card_node.h> #include @@ -32,8 +30,7 @@ enum { IsCardRole = Qt::UserRole + 1, /**< Indicates whether the item represents a card. */ DepthRole, /**< Depth level within the deck's grouping hierarchy. */ - IsLegalRole, /**< Whether the card is legal in the current deck format. */ - IsCustomZoneRole /**< Whether the item represents a custom zone nested under a board zone. */ + IsLegalRole /**< Whether the card is legal in the current deck format. */ }; } // namespace DeckRoles @@ -394,14 +391,6 @@ public: */ [[nodiscard]] QList getZones() const; - /** - * @brief Gets the names of the custom zones nested under the given board zone. - * - * @param boardZoneName The board zone to query (main/side/maybeboard) - * @return The custom zone names, in deck order - */ - [[nodiscard]] QStringList getCustomZoneNames(const QString &boardZoneName) const; - private: QSharedPointer deckList; /**< Pointer to the decklist providing the underlying data. */ InnerDecklistNode *root; /**< Root node of the model tree. */ @@ -438,7 +427,6 @@ private: void emitRecursiveUpdates(const QModelIndex &index); void sortHelper(InnerDecklistNode *node, Qt::SortOrder order); - void sortShadowTree(InnerDecklistNode *node, Qt::SortOrder order); template T getNode(const QModelIndex &index) const { diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model_custom_zones.cpp b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model_custom_zones.cpp deleted file mode 100644 index 1dc745e63..000000000 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model_custom_zones.cpp +++ /dev/null @@ -1,152 +0,0 @@ -#include "deck_list_model_custom_zones.h" - -#include "deck_list_model.h" - -#include <../../../../libcockatrice_deck_list/libcockatrice/deck_list/tree/deck_list_card_node.h> -#include -#include - -namespace DeckListModelCustomZones -{ - -bool isCustomZone(const AbstractDecklistNode *node) -{ - return dynamic_cast(node) != nullptr; -} - -namespace -{ - -/** - * @brief Flattens every card under @p zone into @p shadowZone, preserving order. - * - * Custom zones mirror as a single row level: cards nested in sub-zones of any - * depth are added as direct children of the mirrored zone so no card is left - * without a model row. - */ -void flattenCards(const InnerDecklistNode *zone, InnerDecklistNode *shadowZone) -{ - for (int k = 0; k < zone->size(); k++) { - if (auto *zoneCard = dynamic_cast(zone->at(k))) { - new DecklistModelCardNode(zoneCard, shadowZone); - } else if (auto *subZone = dynamic_cast(zone->at(k))) { - flattenCards(subZone, shadowZone); - } - } -} - -} // namespace - -void mirrorCustomZones(const InnerDecklistNode *deckBoardZone, InnerDecklistNode *shadowBoardZone) -{ - for (int j = 0; j < deckBoardZone->size(); j++) { - auto *customZone = dynamic_cast(deckBoardZone->at(j)); - if (!customZone) { - continue; - } - - auto *shadowZone = new DecklistModelSubZoneNode(customZone->getName(), shadowBoardZone); - flattenCards(customZone, shadowZone); - } -} - -InnerDecklistNode *findGroupChild(InnerDecklistNode *parent, const QString &name) -{ - for (int i = 0; i < parent->size(); i++) { - AbstractDecklistNode *child = parent->at(i); - if (isCustomZone(child)) { - continue; - } - auto *group = dynamic_cast(child); - if (group && group->getName() == name) { - return group; - } - } - return nullptr; -} - -DecklistModelSubZoneNode *findSubZoneByName(InnerDecklistNode *root, const QString &zoneName) -{ - for (int i = 0; i < root->size(); i++) { - auto *boardZone = dynamic_cast(root->at(i)); - if (!boardZone) { - continue; - } - - for (int j = 0; j < boardZone->size(); j++) { - auto *customZone = dynamic_cast(boardZone->at(j)); - if (customZone && customZone->getName() == zoneName) { - return customZone; - } - } - } - - return nullptr; -} - -namespace -{ - -/** - * @brief Sorts a node's children and returns the (preSortRow, finalRow) mapping. - */ -QList> plainSort(InnerDecklistNode *node, Qt::SortOrder order) -{ - const QVector> sortResult = node->sort(order); - - QList> mapping; - mapping.reserve(node->size()); - for (int i = 0; i < node->size(); ++i) { - mapping.append({sortResult[i].first, i}); - } - return mapping; -} - -/** - * @brief Sorts a board zone's children, then stably moves custom zones to the end. - * - * @return The (preSortRow, finalRow) mapping covering both the sort and the shift. - */ -QList> boardSort(InnerDecklistNode *node, Qt::SortOrder order) -{ - const QVector> sortResult = node->sort(order); - - QVector groups; - QVector customZones; - QHash preSortRowOf; - - groups.reserve(node->size()); - customZones.reserve(node->size()); - - for (int i = 0; i < node->size(); ++i) { - AbstractDecklistNode *child = node->at(i); - preSortRowOf.insert(child, sortResult[i].first); - if (isCustomZone(child)) { - customZones.append(child); - } else { - groups.append(child); - } - } - - QVector ordered = groups + customZones; - for (int i = 0; i < ordered.size(); ++i) { - node->replace(i, ordered[i]); - } - - QList> mapping; - mapping.reserve(ordered.size()); - for (int i = 0; i < ordered.size(); ++i) { - mapping.append({preSortRowOf.value(ordered[i]), i}); - } - return mapping; -} - -} // namespace - -QList> sortWithCustomZonesLast(InnerDecklistNode *root, InnerDecklistNode *node, Qt::SortOrder order) -{ - const bool isBoardZone = (node != root) && (node->getParent() == root); - return isBoardZone ? boardSort(node, order) : plainSort(node, order); -} - -} // namespace DeckListModelCustomZones diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model_custom_zones.h b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model_custom_zones.h deleted file mode 100644 index 518a9e1d2..000000000 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model_custom_zones.h +++ /dev/null @@ -1,98 +0,0 @@ -#ifndef DECK_LIST_MODEL_CUSTOM_ZONES_H -#define DECK_LIST_MODEL_CUSTOM_ZONES_H - -#include <../../../../libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.h> -#include -#include -#include - -/** - * @class DecklistModelSubZoneNode - * @ingroup DeckModels - * @brief Model node representing a custom zone nested under a board zone. - * - * Custom zones group cards by user-defined names (e.g. "Removal", "Utility") - * inside a board zone. They are mirrored from the underlying deck tree so that - * they can be told apart from criteria group nodes by type. - */ -class DecklistModelSubZoneNode : public InnerDecklistNode -{ -public: - using InnerDecklistNode::InnerDecklistNode; -}; - -/** - * @namespace DeckListModelCustomZones - * @ingroup DeckModels - * @brief Tree-level helpers for the deck list model's custom-zone shadow nodes. - * - * The deck list model keeps a second "shadow" tree of InnerDecklistNode that - * mirrors the canonical deck tree for grouping and sorting. Custom zones add a - * layer of bookkeeping to that shadow tree: they must be mirrored alongside - * criteria groups, always sort after the groups within a board, and be - * resolvable by deck-unique name. - * - * This namespace centralizes every "what is / where is a custom zone" decision - * so the model itself only wires the results into Qt model signals. - */ -namespace DeckListModelCustomZones -{ - -/** - * @brief Whether the given node is a custom zone (as opposed to a criteria group). - */ -[[nodiscard]] bool isCustomZone(const AbstractDecklistNode *node); - -/** - * @brief Finds a criteria-group child of @p parent by name, skipping custom zones. - * - * The shadow tree keeps criteria groups and mirrored custom zones as siblings - * under a board zone, and `InnerDecklistNode::findChild` matches both by name. - * Group lookups must not resolve a custom zone that happens to share the group - * name (e.g. a zone called "Creature"), so this searches only non-custom - * children. - * - * @param parent The shadow node whose children are searched. - * @param name The group name to find. - * @return The matching group node, or nullptr if none exists. - */ -[[nodiscard]] InnerDecklistNode *findGroupChild(InnerDecklistNode *parent, const QString &name); - -/** - * @brief Mirrors the custom zones of a deck board zone into its shadow board node. - * - * Each custom zone becomes a DecklistModelSubZoneNode under @p shadowBoardZone - * with its cards as direct (un-grouped) children. - * - * @param deckBoardZone The board zone in the canonical deck tree. - * @param shadowBoardZone The matching board zone in the model's shadow tree. - */ -void mirrorCustomZones(const InnerDecklistNode *deckBoardZone, InnerDecklistNode *shadowBoardZone); - -/** - * @brief Finds a custom zone in the shadow tree by deck-unique name. - * @param root Root of the shadow tree. - * @param zoneName The custom zone name to find. - * @return The matching custom zone node, or nullptr if not found. - */ -[[nodiscard]] DecklistModelSubZoneNode *findSubZoneByName(InnerDecklistNode *root, const QString &zoneName); - -/** - * @brief Sorts a shadow node's children, keeping a board's custom zones last. - * - * Sorting alone would interleave custom zones with criteria groups by name, but - * custom zones must always stay after the groups within a board, regardless of - * name. This applies the sort and, for board zones, stably moves the custom - * zones to the end. - * - * @param root Root of the shadow tree (used to classify board zones). - * @param node The shadow node whose children are reordered. - * @param order Sort order to apply. - * @return A list of (preSortRow, finalRow) pairs describing how each node moved. - */ -[[nodiscard]] QList> -sortWithCustomZonesLast(InnerDecklistNode *root, InnerDecklistNode *node, Qt::SortOrder order); - -} // namespace DeckListModelCustomZones - -#endif // DECK_LIST_MODEL_CUSTOM_ZONES_H diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt b/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt index 0791f8c14..73745e7ed 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt @@ -92,6 +92,7 @@ set(PROTO_FILES event_game_log_notice.proto event_game_say.proto event_game_state_changed.proto + event_game_state_changed.proto event_join.proto event_join_room.proto event_kicked.proto diff --git a/libcockatrice_rng/libcockatrice/rng/rng_sfmt.cpp b/libcockatrice_rng/libcockatrice/rng/rng_sfmt.cpp index 4c578b4e4..5b38deb3f 100644 --- a/libcockatrice_rng/libcockatrice/rng/rng_sfmt.cpp +++ b/libcockatrice_rng/libcockatrice/rng/rng_sfmt.cpp @@ -1,5 +1,6 @@ #include "rng_sfmt.h" +#include #include #include #include @@ -10,11 +11,10 @@ #define UINT64_MAX (~(uint64_t)0) #endif -RNG_SFMT::RNG_SFMT(uint64_t seed, QObject *parent) : RNG_Abstract(parent) +RNG_SFMT::RNG_SFMT(QObject *parent) : RNG_Abstract(parent) { - // initialize the random number generator with a 64bit seed, e.g. from a CSPRNG - uint32_t seedArray[2] = {static_cast(seed), static_cast(seed >> 32)}; - sfmt_init_by_array(&sfmt, seedArray, 2); + // initialize the random number generator with a 32bit integer seed (timestamp) + sfmt_init_gen_rand(&sfmt, QDateTime::currentDateTime().toSecsSinceEpoch()); } /** diff --git a/libcockatrice_rng/libcockatrice/rng/rng_sfmt.h b/libcockatrice_rng/libcockatrice/rng/rng_sfmt.h index a180dad99..7e9f53df3 100644 --- a/libcockatrice_rng/libcockatrice/rng/rng_sfmt.h +++ b/libcockatrice_rng/libcockatrice/rng/rng_sfmt.h @@ -36,7 +36,7 @@ private: unsigned int cdf(unsigned int min, unsigned int max); public: - explicit RNG_SFMT(uint64_t seed, QObject *parent = nullptr); + explicit RNG_SFMT(QObject *parent = nullptr); unsigned int rand(int min, int max) override; }; diff --git a/libcockatrice_settings/libcockatrice/settings/chat_settings.cpp b/libcockatrice_settings/libcockatrice/settings/chat_settings.cpp index 974173371..5e1102473 100644 --- a/libcockatrice_settings/libcockatrice/settings/chat_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/chat_settings.cpp @@ -65,11 +65,6 @@ bool ChatSettings::getRoomHistory() const return getValue("roomHistory", QString(), QString(), true).toBool(); } -bool ChatSettings::getIgnoreAllPrivateMessages() const -{ - return getValue("ignoreAllPrivateMessages", QString(), QString(), false).toBool(); -} - QString ChatSettings::getHighlightWords() const { return getValue("highlightWords").toString(); @@ -136,11 +131,6 @@ void ChatSettings::setRoomHistory(bool _roomHistory) setValue(_roomHistory, "roomHistory"); } -void ChatSettings::setIgnoreAllPrivateMessages(bool _ignoreAllPrivateMessages) -{ - setValue(_ignoreAllPrivateMessages, "ignoreAllPrivateMessages"); -} - void ChatSettings::setHighlightWords(const QString &_highlightWords) { setValue(_highlightWords, "highlightWords"); diff --git a/libcockatrice_settings/libcockatrice/settings/chat_settings.h b/libcockatrice_settings/libcockatrice/settings/chat_settings.h index 9566671e2..7cf4be3f6 100644 --- a/libcockatrice_settings/libcockatrice/settings/chat_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/chat_settings.h @@ -23,7 +23,6 @@ public: [[nodiscard]] bool getShowMessagePopup() const override; [[nodiscard]] bool getShowMentionPopup() const override; [[nodiscard]] bool getRoomHistory() const override; - [[nodiscard]] bool getIgnoreAllPrivateMessages() const override; [[nodiscard]] QString getHighlightWords() const override; void setChatMention(bool _chatMention); @@ -38,7 +37,6 @@ public: void setShowMessagePopups(bool _showMessagePopups); void setShowMentionPopups(bool _showMentionPopups); void setRoomHistory(bool _roomHistory); - void setIgnoreAllPrivateMessages(bool _ignoreAllPrivateMessages); void setHighlightWords(const QString &_highlightWords); signals: diff --git a/libcockatrice_settings/libcockatrice/settings/tabs_settings.cpp b/libcockatrice_settings/libcockatrice/settings/tabs_settings.cpp index cf5bfd81a..85a1424a6 100644 --- a/libcockatrice_settings/libcockatrice/settings/tabs_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/tabs_settings.cpp @@ -106,11 +106,6 @@ bool TabsSettings::getTabModerationOpen() const return getValue("moderation", QString(), QString(), false).toBool(); } -bool TabsSettings::getTabCardArtRulesOpen() const -{ - return getValue("cardArtRules", QString(), QString(), false).toBool(); -} - void TabsSettings::setTabVisualDeckStorageOpen(bool value) { setValue(value, "visualDeckStorage"); @@ -155,8 +150,3 @@ void TabsSettings::setTabModerationOpen(bool value) { setValue(value, "moderation"); } - -void TabsSettings::setTabCardArtRulesOpen(bool value) -{ - setValue(value, "cardArtRules"); -} diff --git a/libcockatrice_settings/libcockatrice/settings/tabs_settings.h b/libcockatrice_settings/libcockatrice/settings/tabs_settings.h index eb78d311b..365d91af7 100644 --- a/libcockatrice_settings/libcockatrice/settings/tabs_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/tabs_settings.h @@ -43,7 +43,6 @@ public: [[nodiscard]] bool getTabLogOpen() const override; [[nodiscard]] bool getTabReportOpen() const override; [[nodiscard]] bool getTabModerationOpen() const override; - [[nodiscard]] bool getTabCardArtRulesOpen() const override; void setStartupTabIndex(int value); void setStartupServerHost(const QString &host); @@ -58,7 +57,6 @@ public: void setTabLogOpen(bool value); void setTabReportOpen(bool value); void setTabModerationOpen(bool value); - void setTabCardArtRulesOpen(bool value); signals: void startupTabIndexChanged(int index); diff --git a/libcockatrice_utility/CMakeLists.txt b/libcockatrice_utility/CMakeLists.txt index db23f7951..c6411ea76 100644 --- a/libcockatrice_utility/CMakeLists.txt +++ b/libcockatrice_utility/CMakeLists.txt @@ -6,15 +6,13 @@ set(CMAKE_AUTOUIC ON) set(CMAKE_AUTORCC ON) set(UTILITY_SOURCES - libcockatrice/utility/cryptoutil.cpp libcockatrice/utility/expression.cpp libcockatrice/utility/levenshtein.cpp - libcockatrice/utility/passwordhasher.cpp libcockatrice/utility/server_rate_limiter.cpp - libcockatrice/utility/warning_categories.cpp + libcockatrice/utility/expression.cpp libcockatrice/utility/levenshtein.cpp libcockatrice/utility/passwordhasher.cpp + libcockatrice/utility/server_rate_limiter.cpp libcockatrice/utility/warning_categories.cpp ) set(UTILITY_HEADERS libcockatrice/utility/card_ref.h libcockatrice/utility/color.h - libcockatrice/utility/cryptoutil.h libcockatrice/utility/expression.h libcockatrice/utility/levenshtein.h libcockatrice/utility/macros.h @@ -34,9 +32,7 @@ add_library(libcockatrice_utility STATIC ${UTILITY_SOURCES} ${UTILITY_HEADERS}) target_include_directories(libcockatrice_utility PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) -find_package(OpenSSL REQUIRED) - -target_link_libraries(libcockatrice_utility PUBLIC libcockatrice_rng OpenSSL::Crypto ${QT_CORE_MODULE}) +target_link_libraries(libcockatrice_utility PUBLIC libcockatrice_rng ${QT_CORE_MODULE}) set(ORACLE_LIBS) diff --git a/libcockatrice_utility/libcockatrice/utility/cryptoutil.cpp b/libcockatrice_utility/libcockatrice/utility/cryptoutil.cpp deleted file mode 100644 index 416ef261b..000000000 --- a/libcockatrice_utility/libcockatrice/utility/cryptoutil.cpp +++ /dev/null @@ -1,25 +0,0 @@ -#include "cryptoutil.h" - -#include - -namespace CryptoUtil -{ -QByteArray randomBytes(int count) -{ - QByteArray bytes(count, '\0'); - if (RAND_bytes(reinterpret_cast(bytes.data()), count) != 1) { - // Randomness failure is fatal: never fall back to a predictable source. - qFatal("CryptoUtil::randomBytes: RAND_bytes failed"); - } - return bytes; -} - -quint64 randomUInt64() -{ - quint64 value; - if (RAND_bytes(reinterpret_cast(&value), sizeof(value)) != 1) { - qFatal("CryptoUtil::randomUInt64: RAND_bytes failed"); - } - return value; -} -} // namespace CryptoUtil diff --git a/libcockatrice_utility/libcockatrice/utility/cryptoutil.h b/libcockatrice_utility/libcockatrice/utility/cryptoutil.h deleted file mode 100644 index dba9dc37d..000000000 --- a/libcockatrice_utility/libcockatrice/utility/cryptoutil.h +++ /dev/null @@ -1,13 +0,0 @@ -#ifndef CRYPTOUTIL_H -#define CRYPTOUTIL_H - -#include -#include - -namespace CryptoUtil -{ -QByteArray randomBytes(int count); -quint64 randomUInt64(); -} // namespace CryptoUtil - -#endif diff --git a/libcockatrice_utility/libcockatrice/utility/passwordhasher.cpp b/libcockatrice_utility/libcockatrice/utility/passwordhasher.cpp index 1c22fdcfa..c40c5f94f 100644 --- a/libcockatrice_utility/libcockatrice/utility/passwordhasher.cpp +++ b/libcockatrice_utility/libcockatrice/utility/passwordhasher.cpp @@ -1,7 +1,7 @@ #include "passwordhasher.h" #include -#include +#include QString PasswordHasher::computeHash(const QString &password, const QString &salt) { @@ -21,28 +21,12 @@ QString PasswordHasher::generateRandomSalt(const int len) static const char alphanum[] = "0123456789" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz"; - const int size = sizeof(alphanum) - 1; - - // Two bytes per character, corrected for modulo bias via rejection sampling. - const int bucketSize = 65536 / size; - const int limit = bucketSize * size; QString ret; - ret.reserve(len); - QByteArray random = CryptoUtil::randomBytes(len * 2); - int bytesUsed = 0; + int size = sizeof(alphanum) - 1; + for (int i = 0; i < len; ++i) { - unsigned int value; - do { - if (bytesUsed >= random.size()) { - random = CryptoUtil::randomBytes(len * 2); - bytesUsed = 0; - } - value = static_cast(static_cast(random.at(bytesUsed))) << 8 | - static_cast(static_cast(random.at(bytesUsed + 1))); - bytesUsed += 2; - } while (value >= limit); - ret.append(alphanum[value / bucketSize]); + ret.append(alphanum[rng->rand(0, size)]); } return ret; @@ -50,5 +34,5 @@ QString PasswordHasher::generateRandomSalt(const int len) QString PasswordHasher::generateActivationToken() { - return QString(CryptoUtil::randomBytes(16).toBase64().left(16)); + return QCryptographicHash::hash(generateRandomSalt().toUtf8(), QCryptographicHash::Md5).toBase64().left(16); } diff --git a/oracle/CMakeLists.txt b/oracle/CMakeLists.txt index 68c4709bb..0736db7f5 100644 --- a/oracle/CMakeLists.txt +++ b/oracle/CMakeLists.txt @@ -23,7 +23,7 @@ set(oracle_SOURCES src/pages.cpp src/pagetemplates.cpp src/parsehelpers.cpp - src/raw_json_scanner.cpp + src/qt-json/json.cpp ../cockatrice/src/client/settings/cache_settings.cpp ../cockatrice/src/client/settings/card_counter_settings.cpp ../cockatrice/src/client/settings/shortcuts_settings.cpp @@ -113,8 +113,6 @@ qt6_add_executable( MANUAL_FINALIZATION ) -target_precompile_headers(oracle PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtwidgets_pch.h") - # ------------------------ # Link libraries # ------------------------ diff --git a/oracle/src/oracleimporter.cpp b/oracle/src/oracleimporter.cpp index fdf11ef02..fdb32bb8d 100644 --- a/oracle/src/oracleimporter.cpp +++ b/oracle/src/oracleimporter.cpp @@ -3,13 +3,10 @@ #include "libcockatrice/interfaces/noop_card_preference_provider.h" #include "libcockatrice/interfaces/noop_card_set_priority_controller.h" #include "parsehelpers.h" +#include "qt-json/json.h" #include -#include -#include -#include #include -#include #include #include #include @@ -45,37 +42,31 @@ static CardSet::Priority getSetPriority(const QString &setType, const QString &s return priority; } -bool OracleImporter::readSetsFromByteArray(QByteArray data) +bool OracleImporter::readSetsFromByteArray(const QByteArray &data) { - const RawJson::ScanProgressCallback progress = - progressReporting - ? [this]( - qsizetype bytesRead, - qsizetype - totalBytes) { emit dataReadProgress(static_cast(bytesRead), static_cast(totalBytes)); } - : RawJson::ScanProgressCallback{}; - - RawJson::ScanError error; - const QList ranges = RawJson::scanSetRanges(data, &error, progress); - if (error.isError()) { - qDebug() << "error: RawJson::scanSetRanges():" << error.message; + bool ok; + auto setsMap = QtJson::Json::parse(QString(data), ok).toMap().value("data").toMap(); + if (!ok) { + qDebug() << "error: QtJson::Json::parse()"; return false; } QList newSetList; - newSetList.reserve(ranges.size()); - for (const RawJson::SetRange &range : ranges) { - QString shortName = range.code.toUpper(); - QString longName = range.name; - QString setType = range.type; - QDate releaseDate = QDate::fromString(range.releaseDate, Qt::ISODate); + QListIterator it(setsMap.values()); + + while (it.hasNext()) { + QVariantMap map = it.next().toMap(); + QString shortName = map.value("code").toString().toUpper(); + QString longName = map.value("name").toString(); + QList setCards = map.value("cards").toList(); + QString setType = map.value("type").toString(); + QDate releaseDate = map.value("releaseDate").toDate(); CardSet::Priority priority = getSetPriority(setType, shortName); // capitalize set type if (setType.length() > 0) { // basic grammar for words that aren't capitalized, like in "From the Vault" - static const QStringList noCapitalize = {"the", "a", "an", "on", "to", "for", - "of", "in", "and", "with", "or"}; + const QStringList noCapitalize = {"the", "a", "an", "on", "to", "for", "of", "in", "and", "with", "or"}; QStringList words = setType.split("_"); setType.clear(); bool first = false; @@ -83,15 +74,13 @@ bool OracleImporter::readSetsFromByteArray(QByteArray data) if (first && noCapitalize.contains(item)) { setType += item + QString(" "); } else { - setType += item[0].toUpper() + item.mid(1) + QString(" "); + setType += item[0].toUpper() + item.mid(1, -1) + QString(" "); first = true; } } setType = setType.trimmed(); } - SetToDownload set(shortName, longName, priority, setType, releaseDate); - set.setRawRange(range.dataRange); - newSetList.append(set); + newSetList.append(SetToDownload(shortName, longName, setCards, priority, setType, releaseDate)); } std::sort(newSetList.begin(), newSetList.end()); @@ -100,34 +89,19 @@ bool OracleImporter::readSetsFromByteArray(QByteArray data) return false; } allSets = newSetList; - rawSetsData = std::move(data); return true; } -/** - * The priority order used to pick a card's main type when a card has multiple - * types (e.g. "Artifact Creature") or multiple faces (e.g. split/adventure cards). - * A lower index means a higher priority. - */ -static const QStringList MAIN_CARD_TYPE_PRIORITY = {"Planeswalker", "Creature", "Land", "Sorcery", - "Instant", "Artifact", "Enchantment"}; - -/** - * Returns the priority (index) of the given main card type. Known types map to their - * position in {@link mainCardTypePriority()}, unknown types map to -1 (lowest priority). - */ -static int mainCardTypePriority(const QString &mainCardType) -{ - return MAIN_CARD_TYPE_PRIORITY.indexOf(mainCardType); -} - static QString getMainCardType(const QStringList &typeList) { if (typeList.isEmpty()) { return {}; } - for (const auto &type : MAIN_CARD_TYPE_PRIORITY) { + static const QStringList typePriority = {"Planeswalker", "Creature", "Land", "Sorcery", + "Instant", "Artifact", "Enchantment"}; + + for (const auto &type : typePriority) { if (typeList.contains(type)) { return type; } @@ -148,8 +122,14 @@ static void sortAndReduceColors(QString &colors) std::sort(colors.begin(), colors.end(), [](const QChar a, const QChar b) { return colorOrder.value(a, INT_MAX) < colorOrder.value(b, INT_MAX); }); // reduce - auto last = std::unique(colors.begin(), colors.end()); - colors.erase(last, colors.end()); + QChar lastChar = '\0'; + for (int i = 0; i < colors.size(); ++i) { + if (colors.at(i) == lastChar) { + colors.remove(i, 1); + } else { + lastChar = colors.at(i); + } + } } CardInfoPtr OracleImporter::addCard(QString name, @@ -162,12 +142,9 @@ CardInfoPtr OracleImporter::addCard(QString name, // Workaround for card name weirdness name = name.replace("Æ", "AE"); name = name.replace("’", "'"); - auto existingIt = cards.constFind(name); - if (existingIt != cards.constEnd()) { - CardInfoPtr card = existingIt.value(); + if (cards.contains(name)) { + CardInfoPtr card = cards.value(name); card->addToSet(printingInfo.getSet(), printingInfo); - // Only merge legalities when the card has none yet, so multi-format - // printings don't overwrite each other's legality lists. if (card->getProperties().filter(formatRegex).empty()) { card->combineLegalities(properties); } @@ -205,9 +182,8 @@ CardInfoPtr OracleImporter::addCard(QString name, // DETECT CARD POSITIONING INFO - QString layoutVal = properties.value("layout"); - bool landscapeOrientation = - properties.value("maintype") == "Battle" || layoutVal == "split" || layoutVal == "planar"; + bool landscapeOrientation = properties.value("maintype") == "Battle" || properties.value("layout") == "split" || + properties.value("layout") == "planar"; // cards that enter the field tapped bool cipt = parseCipt(name, text) || landscapeOrientation; @@ -246,15 +222,12 @@ CardInfoPtr OracleImporter::addCard(QString name, return newCard; } -static QString getJsonString(const QJsonObject &obj, const QString &key) +static QString getStringPropertyFromMap(const QVariantMap &card, const QString &propertyName) { - // QVariant coerces numbers and booleans to text, while QJsonValue::toString() - // returns a null string for them — some MTGJSON fields (manaValue, - // convertedManaCost, isOnlineOnly, isRebalanced) carry those types. - return obj.value(key).toVariant().toString(); + return card.contains(propertyName) ? card.value(propertyName).toString() : QString(""); } -int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJsonArray &cardsList) +int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList &cardsList) { // mtgjson name => xml name static const QMap cardProperties{ @@ -275,7 +248,7 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJson static const QString ptSeparator = "/"; static constexpr bool isToken = false; - static const QSet setsWithCardsWithSameNameButDifferentText = {"UST"}; + static const QList setsWithCardsWithSameNameButDifferentText = {"UST"}; int numCards = 0; @@ -283,16 +256,16 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJson QMap, QString>> splitCards; // Keeps track of all names encountered so far - QSet allNameProps; + QList allNameProps; - for (const QJsonValue &cardVal : cardsList) { - QJsonObject card = cardVal.toObject(); + for (const QVariant &cardVar : cardsList) { + QVariantMap card = cardVar.toMap(); /* Currently used layouts are: * augment, double_faced_token, flip, host, leveler, meld, normal, planar, * saga, scheme, split, token, transform, vanguard */ - QString layout = getJsonString(card, "layout"); + QString layout = getStringPropertyFromMap(card, "layout"); // don't import tokens from the json file if (layout == "token") { @@ -300,9 +273,9 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJson } // normal cards handling - QString name = getJsonString(card, "name"); - QString text = getJsonString(card, "text"); - QString faceName = getJsonString(card, "faceName"); + QString name = getStringPropertyFromMap(card, "name"); + QString text = getStringPropertyFromMap(card, "text"); + QString faceName = getStringPropertyFromMap(card, "faceName"); if (faceName.isEmpty()) { faceName = name; } @@ -310,34 +283,39 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJson // card properties QHash properties; for (auto i = cardProperties.cbegin(), end = cardProperties.cend(); i != end; ++i) { - QString propertyValue = getJsonString(card, i.key()); + QString mtgjsonProperty = i.key(); + QString xmlPropertyName = i.value(); + QString propertyValue = getStringPropertyFromMap(card, mtgjsonProperty); if (!propertyValue.isEmpty()) { - properties.insert(i.value(), propertyValue); + properties.insert(xmlPropertyName, propertyValue); } } // per-set properties QHash printingProps; for (auto i = setInfoProperties.cbegin(), end = setInfoProperties.cend(); i != end; ++i) { - QString propertyValue = getJsonString(card, i.key()); + QString mtgjsonProperty = i.key(); + QString xmlPropertyName = i.value(); + QString propertyValue = getStringPropertyFromMap(card, mtgjsonProperty); if (!propertyValue.isEmpty()) { - printingProps.insert(i.value(), propertyValue); + printingProps.insert(xmlPropertyName, propertyValue); } } // handle flavorNames specially due to double-faced cards - QString faceFlavorName = getJsonString(card, "faceFlavorName"); - QString flavorName = !faceFlavorName.isEmpty() ? faceFlavorName : getJsonString(card, "flavorName"); + QString faceFlavorName = getStringPropertyFromMap(card, "faceFlavorName"); + QString flavorName = !faceFlavorName.isEmpty() ? faceFlavorName : getStringPropertyFromMap(card, "flavorName"); if (!flavorName.isEmpty()) { printingProps.insert("flavorName", flavorName); } // Identifiers - QJsonObject identifiers = card.value("identifiers").toObject(); for (auto i = identifierProperties.cbegin(), end = identifierProperties.cend(); i != end; ++i) { - QString propertyValue = getJsonString(identifiers, i.key()); + QString mtgjsonProperty = i.key(); + QString xmlPropertyName = i.value(); + QString propertyValue = getStringPropertyFromMap(card.value("identifiers").toMap(), mtgjsonProperty); if (!propertyValue.isEmpty()) { - printingProps.insert(i.value(), propertyValue); + printingProps.insert(xmlPropertyName, propertyValue); } } @@ -353,26 +331,21 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJson allNameProps.contains(faceName) && layout == "normal" && lastChar.isLetter()) { numComponent = " (" + QString(lastChar).toLower() + ")"; } - allNameProps.insert(faceName); + allNameProps.append(faceName); // special handling properties - QString colors; - for (const QJsonValue &color : card.value("colors").toArray()) { - colors += color.toString(); - } + QString colors = card.value("colors").toStringList().join(""); if (!colors.isEmpty()) { properties.insert("colors", colors); } - QString colorIdentity; - for (const QJsonValue &color : card.value("colorIdentity").toArray()) { - colorIdentity += color.toString(); - } + // special handling properties + QString colorIdentity = card.value("colorIdentity").toStringList().join(""); if (!colorIdentity.isEmpty()) { properties.insert("coloridentity", colorIdentity); } - const auto &mainCardType = getMainCardType(card.value("types").toVariant().toStringList()); + const auto &mainCardType = getMainCardType(card.value("types").toStringList()); if (mainCardType.isEmpty()) { qDebug() << "warning: no mainCardType for card:" << name; } else { @@ -381,22 +354,22 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJson // Depending on whether power and/or toughness are present, the format // is either P/T (most common), P (no toughness), or /T (no power). - QString power = getJsonString(card, "power"); - QString toughness = getJsonString(card, "toughness"); + QString power = getStringPropertyFromMap(card, "power"); + QString toughness = getStringPropertyFromMap(card, "toughness"); if (toughness.isEmpty() && !power.isEmpty()) { properties.insert("pt", power); } else if (!toughness.isEmpty()) { properties.insert("pt", power + ptSeparator + toughness); } - auto legalities = card.value("legalities").toObject(); - for (auto i = legalities.constBegin(), end = legalities.constEnd(); i != end; ++i) { + auto legalities = card.value("legalities").toMap(); + for (auto i = legalities.cbegin(), end = legalities.cend(); i != end; ++i) { properties.insert(QString("format-%1").arg(i.key()), i.value().toString().toLower()); } // split cards are considered a single card, enqueue for later merging if (layout == "split" || layout == "aftermath" || layout == "adventure" || layout == "prepare") { - auto _faceName = getJsonString(card, "faceName"); + auto _faceName = getStringPropertyFromMap(card, "faceName"); SplitCardPart split(_faceName, text, properties, printingInfo); auto found_iter = splitCards.find(name + numProperty); if (found_iter == splitCards.end()) { @@ -409,11 +382,11 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJson QList relatedCards; // add other face for split cards as card relation - if (!getJsonString(card, "side").isEmpty()) { - auto faceManaValue = getJsonString(card, "faceManaValue"); + if (!getStringPropertyFromMap(card, "side").isEmpty()) { + auto faceManaValue = getStringPropertyFromMap(card, "faceManaValue"); if (faceManaValue.isEmpty()) { // check the old name for the property, for backwards compatibility purposes - faceManaValue = getJsonString(card, "faceConvertedManaCost"); + faceManaValue = getStringPropertyFromMap(card, "faceConvertedManaCost"); } properties["cmc"] = faceManaValue; @@ -433,21 +406,20 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJson name = faceName; } - // mtgjson related cards - QJsonObject givenRelated = card.value("relatedCards").toObject(); - if (!givenRelated.isEmpty()) { + // mtgjon related cards + if (card.contains("relatedCards")) { + QVariantMap givenRelated = card.value("relatedCards").toMap(); // conjured cards from a spellbook - QJsonArray spellbook = givenRelated.value("spellbook").toArray(); - if (!spellbook.isEmpty()) { - for (const QJsonValue &spbkVal : spellbook) { - relatedCards.append(new CardRelation(spbkVal.toString(), CardRelationType::DoesNotAttach, false, - false, 1, true)); + if (givenRelated.contains("spellbook")) { + auto spbk = givenRelated.value("spellbook").toStringList(); + for (const QString &spbkName : spbk) { + relatedCards.append( + new CardRelation(spbkName, CardRelationType::DoesNotAttach, false, false, 1, true)); } } } - CardInfoPtr newCard = - addCard(name + numComponent, text, isToken, std::move(properties), relatedCards, printingInfo); + CardInfoPtr newCard = addCard(name + numComponent, text, isToken, properties, relatedCards, printingInfo); numCards++; } } @@ -455,6 +427,7 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJson // split cards handling static const QString splitCardPropSeparator = QString(" // "); static const QString splitCardTextSeparator = QString("\n\n---\n\n"); + static const QList noRelatedCards = {}; QList, QString>> partsAndNames = splitCards.values(); for (auto [splitCardParts, name] : partsAndNames) { @@ -480,17 +453,10 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJson if (!thisCardPropertyValue.isEmpty() && originalPropertyValue != thisCardPropertyValue) { if (originalPropertyValue.isEmpty()) { // don't create //es if one field is empty properties.insert(prop, thisCardPropertyValue); - } else if (prop == "colors" || prop == "coloridentity") { // the card is both colors + } else if (prop == "colors") { // the card is both colors properties.insert(prop, originalPropertyValue + thisCardPropertyValue); - } else if (prop == "maintype") { - // Use the same priority as getMainCardType() to pick the - // "best" type across faces — e.g. Creature over Instant - // for adventure cards like Bonecrusher Giant. - int currentPriority = mainCardTypePriority(originalPropertyValue); - int newPriority = mainCardTypePriority(thisCardPropertyValue); - if (newPriority >= 0 && (currentPriority < 0 || newPriority < currentPriority)) { - properties.insert(prop, thisCardPropertyValue); - } + } else if (prop == "maintype") { // don't create maintypes with //es in them + continue; } else { properties.insert(prop, originalPropertyValue + splitCardPropSeparator + thisCardPropertyValue); @@ -499,20 +465,20 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJson } } } - CardInfoPtr newCard = addCard(name, text, isToken, std::move(properties), {}, printingInfo); + CardInfoPtr newCard = addCard(name, text, isToken, properties, noRelatedCards, printingInfo); numCards++; } return numCards; } -static FormatRulesNameMap buildDefaultMagicFormats() +FormatRulesNameMap OracleImporter::createDefaultMagicFormats() { // Predefined common exceptions CardCondition superTypeIsBasic; superTypeIsBasic.field = "type"; superTypeIsBasic.matchType = "regex"; - superTypeIsBasic.value = R"(\bBasic\b[^—]+\bLand\b)"; + superTypeIsBasic.value = "\bBasic\b[^—]+\bLand\b"; ExceptionRule basicLands; basicLands.conditions.append(superTypeIsBasic); @@ -525,6 +491,7 @@ static FormatRulesNameMap buildDefaultMagicFormats() ExceptionRule mayContainAnyNumber; mayContainAnyNumber.conditions.append(anyNumberAllowed); + // Map to store default rules FormatRulesNameMap defaultFormatRulesNameMap; // ----------------- Helper lambda to create format ----------------- @@ -570,29 +537,10 @@ static FormatRulesNameMap buildDefaultMagicFormats() return defaultFormatRulesNameMap; } -const FormatRulesNameMap &OracleImporter::createDefaultMagicFormats() -{ - static const FormatRulesNameMap cached = buildDefaultMagicFormats(); - return cached; -} - int OracleImporter::startImport() { static ICardSetPriorityController *noOpController = new NoopCardSetPriorityController(); - importCancelled.storeRelease(0); - - // Pre-allocate the cards hash to avoid rehashing during import. Keys are - // distinct card names while raw ranges only count printings (AllPrintings - // ~100k printings vs ~35k names), so this over-reserves somewhat; an exact - // distinct-name count would require eagerly parsing, which the lazy reader - // deliberately avoids. It's a capacity hint, so the overshoot is harmless. - int estimatedCards = 0; - for (const SetToDownload &curSetToParse : allSets) { - estimatedCards += curSetToParse.getRawRange().cardCount; - } - cards.reserve(estimatedCards); - // add an empty set for tokens CardSetPtr tokenSet = CardSet::newInstance(noOpController, CardSet::TOKENS_SETNAME, tr("Dummy set containing tokens"), "Tokens"); @@ -601,53 +549,14 @@ int OracleImporter::startImport() int setIndex = 0; for (const SetToDownload &curSetToParse : allSets) { - if (importCancelled.loadAcquire()) { - // The wizard was closed mid-import: stop at the next set boundary so - // the caller can wait for this future without processing every set. - break; - } - CardSetPtr newSet = CardSet::newInstance(noOpController, curSetToParse.getShortName(), curSetToParse.getLongName(), curSetToParse.getSetType(), curSetToParse.getReleaseDate(), curSetToParse.getPriority()); - - // parse only this set's slice of the raw document so the whole JSON tree is - // never kept in memory at once - const RawJson::SetDataRange &rawRange = curSetToParse.getRawRange(); - const qsizetype rangeEnd = rawRange.start + rawRange.length; - if (rawRange.start < 0 || rawRange.length <= 0 || rangeEnd > rawSetsData.size()) { - // rawSetsData is cleared by releaseSetData() while SetToDownload copies - // taken from getSets() keep their ranges, and nothing else enforces the - // pairing — so never index past the buffer on stale/mismatched ranges. - qWarning() << "error: out-of-bounds raw range for set" << curSetToParse.getShortName() << "skipping"; - ++setIndex; - emit setIndexChanged(0, setIndex, curSetToParse.getLongName()); - continue; - } - // sliced() shares the buffer instead of deep-copying the slice; the largest - // sets in AllPrintings are tens of MB, so the copy is worth avoiding here. - const QByteArray setBytes = rawSetsData.sliced(rawRange.start, rawRange.length); - QJsonParseError parseError; - const QJsonDocument setDoc = QJsonDocument::fromJson(setBytes, &parseError); - if (parseError.error != QJsonParseError::NoError) { - qWarning() << "error: parsing card data for set" << curSetToParse.getShortName() << ":" - << parseError.errorString(); - ++setIndex; - // Keep the progress accounting honest: a set that failed to parse - // still advanced the index, so report it (with zero imported cards) - // rather than letting SaveSetsPage's bar stall per failed set. - emit setIndexChanged(0, setIndex, curSetToParse.getLongName()); - continue; - } - - // Only add the set to the database once its slice parsed cleanly; - // a set that fails here must not persist as an empty set in cards.xml. if (!sets.contains(newSet->getShortName())) { sets.insert(newSet->getShortName(), newSet); } - const QJsonArray setCards = setDoc.object().value("cards").toArray(); - int numCardsInSet = importCardsFromSet(newSet, setCards); + int numCardsInSet = importCardsFromSet(newSet, curSetToParse.getCards()); ++setIndex; @@ -667,16 +576,9 @@ bool OracleImporter::saveToFile(const QString &fileName, const QString &sourceUr return parser.saveToFile(createDefaultMagicFormats(), sets, cards, fileName, sourceUrl, sourceVersion); } -void OracleImporter::releaseSetData() -{ - allSets.clear(); - rawSetsData.clear(); -} - void OracleImporter::clear() { sets.clear(); cards.clear(); allSets.clear(); - rawSetsData.clear(); } diff --git a/oracle/src/oracleimporter.h b/oracle/src/oracleimporter.h index 748056772..99644f9ce 100644 --- a/oracle/src/oracleimporter.h +++ b/oracle/src/oracleimporter.h @@ -1,12 +1,6 @@ #ifndef ORACLEIMPORTER_H #define ORACLEIMPORTER_H -#include "raw_json_scanner.h" - -#include -#include -#include -#include #include #include #include @@ -50,12 +44,10 @@ class SetToDownload { private: QString shortName, longName; + QList cards; QDate releaseDate; QString setType; CardSet::Priority priority; - // Byte range of this set's object within the importer's raw JSON text. Parsing - // one set at a time keeps peak memory low instead of holding the whole document. - RawJson::SetDataRange rawRange; public: const QString &getShortName() const @@ -66,6 +58,10 @@ public: { return longName; } + const QList &getCards() const + { + return cards; + } const QString &getSetType() const { return setType; @@ -78,23 +74,16 @@ public: { return priority; } - const RawJson::SetDataRange &getRawRange() const - { - return rawRange; - } SetToDownload(QString _shortName, QString _longName, + QList _cards, CardSet::Priority _priority, QString _setType = QString(), const QDate &_releaseDate = QDate()) - : shortName(std::move(_shortName)), longName(std::move(_longName)), releaseDate(_releaseDate), - setType(std::move(_setType)), priority(_priority) + : shortName(std::move(_shortName)), longName(std::move(_longName)), cards(std::move(_cards)), + releaseDate(_releaseDate), setType(std::move(_setType)), priority(_priority) { } - void setRawRange(const RawJson::SetDataRange &_rawRange) - { - rawRange = _rawRange; - } bool operator<(const SetToDownload &set) const { return longName.compare(set.longName, Qt::CaseInsensitive) < 0; @@ -150,27 +139,6 @@ private: QList allSets; - /** - * The raw JSON text of the source document, retained for lazy per-set - * parsing during startImport(). Frees the card data as each set is imported. - */ - QByteArray rawSetsData; - - /** - * Whether readSetsFromByteArray() should report scan progress via - * dataReadProgress. A background run routes that signal to stdout (for the - * hosting Cockatrice client to parse); the flag exists to skip the scanner - * instrumentation entirely when no consumer needs it. - */ - bool progressReporting = true; - - /** - * Atomic "please stop importing" flag. startImport() checks it between sets - * so a wizard being closed mid-import can be torn down without waiting for - * the whole import (or racing it). - */ - QAtomicInt importCancelled; - CardInfoPtr addCard(QString name, const QString &text, bool isToken, @@ -183,41 +151,11 @@ signals: public: explicit OracleImporter(QObject *parent = nullptr); - /** - * @brief Controls whether readSetsFromByteArray() instruments the raw scan. - * - * When enabled (the default) the raw scanner reports progress via - * dataReadProgress(), which an interactive wizard shows on its progress bar - * and a background run routes to stdout for the hosting client. Switch it - * off only when nothing will consume scan progress. - */ - void setProgressReporting(bool enabled) - { - progressReporting = enabled; - } - /** - * Scans the given JSON document for set metadata. Takes the data by value so - * the wizard can hand over its decompressed buffer without copying it. - */ - bool readSetsFromByteArray(QByteArray data); + bool readSetsFromByteArray(const QByteArray &data); int startImport(); - /** - * @brief Requests an in-flight startImport() to stop at the next set boundary. - * - * Works by setting an atomic flag that startImport() polls between sets, so - * cancelImport() followed by a short waitForFinished() on the running future is - * safe the moment the wizard is about to be destroyed. - */ - void cancelImport() - { - importCancelled.storeRelease(1); - } bool saveToFile(const QString &fileName, const QString &sourceUrl, const QString &sourceVersion); - int importCardsFromSet(const CardSetPtr ¤tSet, const QJsonArray &cardsList); - /** - * @brief Returns the default format rules. The result is memoized on first use and must be treated as immutable. - */ - const FormatRulesNameMap &createDefaultMagicFormats(); + int importCardsFromSet(const CardSetPtr ¤tSet, const QList &cardsList); + FormatRulesNameMap createDefaultMagicFormats(); const CardNameMap &getCardList() const { return cards; @@ -226,11 +164,6 @@ public: { return allSets; } - const QByteArray &getRawSetsData() const - { - return rawSetsData; - } - void releaseSetData(); void clear(); }; diff --git a/oracle/src/oraclewizard.cpp b/oracle/src/oraclewizard.cpp index dd3ac6459..11200dfc5 100644 --- a/oracle/src/oraclewizard.cpp +++ b/oracle/src/oraclewizard.cpp @@ -110,24 +110,6 @@ void OracleWizard::accept() QDialog::accept(); } -void OracleWizard::reject() -{ - // The wizard is being closed while a page may still run a worker on the - // importer. Ask it to stop before the wizard (and the importer child) is - // destroyed, so the worker thread never touches freed memory. - if (auto *active = dynamic_cast(currentPage())) { - active->cancelWork(); - } - QWizard::reject(); -} - -void OracleWizard::runInBackground() -{ - backgroundMode = true; - hide(); - currentPage()->initializePage(); -} - void OracleWizard::enableButtons() { button(QWizard::NextButton)->setDisabled(false); diff --git a/oracle/src/oraclewizard.h b/oracle/src/oraclewizard.h index 9a509ce5e..78427175c 100644 --- a/oracle/src/oraclewizard.h +++ b/oracle/src/oraclewizard.h @@ -23,7 +23,6 @@ class OracleWizard : public QWizard public: explicit OracleWizard(QWidget *parent = nullptr); void accept() override; - void reject() override; void enableButtons(); void disableButtons(); void retranslateUi(); @@ -53,7 +52,12 @@ public: } bool saveTokensToFile(const QString &fileName); - void runInBackground(); + void runInBackground() + { + backgroundMode = true; + hide(); + currentPage()->initializePage(); + } public: OracleImporter *importer; diff --git a/oracle/src/pages.cpp b/oracle/src/pages.cpp index a10daadb3..df4d1a98c 100644 --- a/oracle/src/pages.cpp +++ b/oracle/src/pages.cpp @@ -12,7 +12,6 @@ #include #include #include -#include #include #include #include @@ -21,17 +20,14 @@ #include #include #include -#include #include #include #include #include #include #include -#include #include #include -#include #include #ifdef HAS_LZMA @@ -57,115 +53,6 @@ #define ALLSETS_URL "https://www.mtgjson.com/api/v5/AllPrintings.json" #endif -/** - * @brief Emits one machine-readable background-run progress line to stdout. - * - * Used only in background mode, so the hosting Cockatrice client can parse these - * lines to drive a determinate progress bar. stderr stays reserved for - * human-readable log output. - */ -static void emitBackgroundProgress(const char *stage, qint64 done, qint64 total) -{ - QTextStream out(stdout); - out << "PROGRESS " << stage << ' ' << done << ' ' << total << '\n'; - out.flush(); -} - -namespace -{ - -/** - * @brief Decompresses and dispatches a sets-file payload on a worker thread. - * - * Iteratively unwraps xz/zip compression, then either hands the JSON to the - * importer (which reports scan progress via dataReadProgress) or returns the raw - * XML for the plain-XML path. Must never touch the wizard or the page: the caller - * consumes the returned LoadSetsResult on the UI thread in importFinished(). - */ -LoadSetsResult loadSetsData(const QPointer &importer, QByteArray data) -{ - LoadSetsResult result; - - while (true) { - if (data.startsWith(XZ_SIGNATURE)) { -#ifdef HAS_LZMA - QBuffer inBuffer(&data); - QByteArray decompressed; - QBuffer outBuffer(&decompressed); - inBuffer.open(QBuffer::ReadOnly); - outBuffer.open(QBuffer::WriteOnly); - XzDecompressor xz; - if (!xz.decompress(&inBuffer, &outBuffer)) { - result.errorMessage = LoadSetsPage::tr("Xz extraction failed."); - result.offerUncompressedFallback = true; - return result; - } - data = decompressed; - continue; -#else - result.errorMessage = - LoadSetsPage::tr("Sorry, this version of Oracle does not support xz compressed files."); - result.offerUncompressedFallback = true; - return result; -#endif - } - - if (data.startsWith(ZIP_SIGNATURE)) { -#ifdef HAS_ZLIB - QBuffer inBuffer(&data); - UnZip uz; - const UnZip::ErrorCode openEc = uz.openArchive(&inBuffer); - if (openEc != UnZip::Ok) { - result.errorMessage = LoadSetsPage::tr("Failed to open Zip archive: %1.").arg(uz.formatError(openEc)); - result.offerUncompressedFallback = true; - return result; - } - if (uz.fileList().size() != 1) { - result.errorMessage = - LoadSetsPage::tr("Zip extraction failed: the Zip archive doesn't contain exactly one file."); - result.offerUncompressedFallback = true; - return result; - } - const QString fileName = uz.fileList().at(0); - QByteArray decompressed; - QBuffer outBuffer(&decompressed); - outBuffer.open(QBuffer::ReadWrite); - const UnZip::ErrorCode ec = uz.extractFile(fileName, &outBuffer); - uz.closeArchive(); - if (ec != UnZip::Ok) { - result.errorMessage = LoadSetsPage::tr("Zip extraction failed: %1.").arg(uz.formatError(ec)); - result.offerUncompressedFallback = true; - return result; - } - data = decompressed; - continue; -#else - result.errorMessage = LoadSetsPage::tr("Sorry, this version of Oracle does not support zipped files."); - result.offerUncompressedFallback = true; - return result; -#endif - } - break; - } - - if (data.startsWith("<")) { - result.ok = true; - result.plainXml = true; - result.xmlData = std::move(data); - return result; - } - - if (data.startsWith("{")) { - result.ok = importer && importer->readSetsFromByteArray(std::move(data)); - return result; - } - - result.errorMessage = LoadSetsPage::tr("Failed to interpret downloaded data."); - return result; -} - -} // namespace - #define TOKENS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Token/master/tokens.xml" #define SPOILERS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/spoiler.xml" @@ -295,11 +182,6 @@ LoadSetsPage::LoadSetsPage(QWidget *parent) : OracleWizardPage(parent) setLayout(layout); } -bool LoadSetsPage::isComplete() const -{ - return !loadActive; -} - void LoadSetsPage::initializePage() { urlLineEdit->setText(wizard()->settings->value("allsetsurl", ALLSETS_URL).toString()); @@ -378,7 +260,7 @@ bool LoadSetsPage::validatePage() return false; } - progressLabel->setText(tr("Downloading (0 MB)")); + progressLabel->setText(tr("Downloading (0MB)")); // show an infinite progressbar progressBar->setMaximum(0); progressBar->setMinimum(0); @@ -397,13 +279,18 @@ bool LoadSetsPage::validatePage() return false; } + if (!setsFile.open(QIODevice::ReadOnly)) { + QMessageBox::critical(nullptr, tr("Error"), tr("Cannot open file '%1'.").arg(fileLineEdit->text())); + return false; + } + wizard()->disableButtons(); setEnabled(false); wizard()->setCardSourceUrl(setsFile.fileName()); wizard()->setCardSourceVersion("unknown"); - readSetsFromFile(setsFile.fileName()); + readSetsFromByteArray(setsFile.readAll()); } return false; @@ -452,14 +339,11 @@ void LoadSetsPage::downloadSetsFile(const QUrl &url) void LoadSetsPage::actDownloadProgressSetsFile(qint64 received, qint64 total) { - if (wizard()->backgroundMode) { - emitBackgroundProgress("download", received, total); - } if (total > 0) { progressBar->setMaximum(static_cast(total)); progressBar->setValue(static_cast(received)); } - progressLabel->setText(tr("Downloading (%1 MB)").arg((int)received / (1024 * 1024))); + progressLabel->setText(tr("Downloading (%1MB)").arg((int)received / (1024 * 1024))); } void LoadSetsPage::actDownloadFinishedSetsFile() @@ -500,97 +384,106 @@ void LoadSetsPage::actDownloadFinishedSetsFile() reply->deleteLater(); } -void LoadSetsPage::updateParsingProgress(int bytesRead, int totalBytes) +void LoadSetsPage::readSetsFromByteArray(QByteArray _data) { - if (totalBytes <= 0) { - return; - } - progressBar->setRange(0, totalBytes); - progressBar->setValue(bytesRead); - const int percent = static_cast((100.0 * bytesRead) / totalBytes); - progressLabel->setText(tr("Parsing file (%1%)").arg(percent)); -} - -void LoadSetsPage::scanProgressToStdout(int bytesRead, int totalBytes) -{ - emitBackgroundProgress("scan", bytesRead, totalBytes); -} - -void LoadSetsPage::beginLoadSets(bool compressedFile) -{ - // Show an infinite progressbar while the worker decompresses; the scan - // steals the label via dataReadProgress as soon as it starts. + // show an infinite progressbar progressBar->setMaximum(0); progressBar->setMinimum(0); progressBar->setValue(0); - progressLabel->setText(compressedFile ? tr("Extracting file...") : tr("Parsing file")); + progressLabel->setText(tr("Parsing file")); progressLabel->show(); progressBar->show(); - // Keep Next disabled (via completeChanged) until the worker reports in; - // updateButtonStates() re-evaluates button state whenever we re-enable. - loadActive = true; - emit completeChanged(); - wizard()->downloadedPlainXml = false; wizard()->xmlData.clear(); - - if (wizard()->backgroundMode) { - connect(wizard()->importer, &OracleImporter::dataReadProgress, this, &LoadSetsPage::scanProgressToStdout, - Qt::UniqueConnection); - } else { - connect(wizard()->importer, &OracleImporter::dataReadProgress, this, &LoadSetsPage::updateParsingProgress, - Qt::UniqueConnection); - } + readSetsFromByteArrayRef(_data); } -void LoadSetsPage::readSetsFromByteArray(QByteArray _data) +void LoadSetsPage::readSetsFromByteArrayRef(QByteArray &_data) { - const bool compressed = _data.startsWith(XZ_SIGNATURE) || _data.startsWith(ZIP_SIGNATURE); - beginLoadSets(compressed); - - // Decompress and scan off the UI thread so a large download can't freeze the window. - const QPointer importer = wizard()->importer; - future = QtConcurrent::run( - [importer, data = std::move(_data)]() mutable { return loadSetsData(importer, std::move(data)); }); - watcher.setFuture(future); -} - -void LoadSetsPage::readSetsFromFile(const QString &fileName) -{ - // Peek at the header on the UI thread so the status text can distinguish - // "Extracting file..." from a plain JSON parse; the full read happens in the worker. - QFile headerFile(fileName); - bool compressed = false; - if (headerFile.open(QIODevice::ReadOnly)) { - const QByteArray header = headerFile.read(6); - compressed = header.startsWith(XZ_SIGNATURE) || header.startsWith(ZIP_SIGNATURE); - } - beginLoadSets(compressed); - - // Read, decompress and scan off the UI thread (a plain JSON can be hundreds - // of MB, so even the read itself must not block the window). - const QPointer importer = wizard()->importer; - future = QtConcurrent::run([importer, fileName]() mutable -> LoadSetsResult { - QFile file(fileName); - if (!file.open(QIODevice::ReadOnly)) { - LoadSetsResult readError; - readError.errorMessage = LoadSetsPage::tr("Cannot open file '%1'.").arg(fileName); - return readError; + // unzip the file if needed + if (_data.startsWith(XZ_SIGNATURE)) { +#ifdef HAS_LZMA + // zipped file + auto *inBuffer = new QBuffer(&_data); + auto newData = QByteArray(); + auto *outBuffer = new QBuffer(&newData); + inBuffer->open(QBuffer::ReadOnly); + outBuffer->open(QBuffer::WriteOnly); + XzDecompressor xz; + if (!xz.decompress(inBuffer, outBuffer)) { + zipDownloadFailed(tr("Xz extraction failed.")); + return; } - return loadSetsData(importer, file.readAll()); - }); - watcher.setFuture(future); -} + _data.clear(); + readSetsFromByteArrayRef(newData); + return; +#else + zipDownloadFailed(tr("Sorry, this version of Oracle does not support xz compressed files.")); -void LoadSetsPage::cancelWork() -{ - // The scan is short-lived; just wait it out before the wizard (and its - // importer) can be torn down underneath the worker thread. - if (future.isRunning()) { - future.cancel(); - watcher.cancel(); - future.waitForFinished(); + wizard()->enableButtons(); + setEnabled(true); + progressLabel->hide(); + progressBar->hide(); + return; +#endif + } else if (_data.startsWith(ZIP_SIGNATURE)) { +#ifdef HAS_ZLIB + // zipped file + auto *inBuffer = new QBuffer(&_data); + auto newData = QByteArray(); + auto *outBuffer = new QBuffer(&newData); + QString fileName; + UnZip::ErrorCode ec; + UnZip uz; + + ec = uz.openArchive(inBuffer); + if (ec != UnZip::Ok) { + zipDownloadFailed(tr("Failed to open Zip archive: %1.").arg(uz.formatError(ec))); + return; + } + + if (uz.fileList().size() != 1) { + zipDownloadFailed(tr("Zip extraction failed: the Zip archive doesn't contain exactly one file.")); + return; + } + fileName = uz.fileList().at(0); + + outBuffer->open(QBuffer::ReadWrite); + ec = uz.extractFile(fileName, outBuffer); + if (ec != UnZip::Ok) { + zipDownloadFailed(tr("Zip extraction failed: %1.").arg(uz.formatError(ec))); + uz.closeArchive(); + return; + } + _data.clear(); + readSetsFromByteArrayRef(newData); + return; +#else + zipDownloadFailed(tr("Sorry, this version of Oracle does not support zipped files.")); + + wizard()->enableButtons(); + setEnabled(true); + progressLabel->hide(); + progressBar->hide(); + return; +#endif + } else if (_data.startsWith("{")) { + // Start the computation. + jsonData = std::move(_data); + future = QtConcurrent::run([this] { return wizard()->importer->readSetsFromByteArray(std::move(jsonData)); }); + watcher.setFuture(future); + } else if (_data.startsWith("<")) { + // save xml file and don't do any processing + wizard()->downloadedPlainXml = true; + wizard()->xmlData = std::move(_data); + importFinished(); + } else { + wizard()->enableButtons(); + setEnabled(true); + progressLabel->hide(); + progressBar->hide(); + QMessageBox::critical(this, tr("Error"), tr("Failed to interpret downloaded data.")); } } @@ -616,65 +509,17 @@ void LoadSetsPage::zipDownloadFailed(const QString &message) void LoadSetsPage::importFinished() { - loadActive = false; - emit completeChanged(); wizard()->enableButtons(); setEnabled(true); + progressLabel->hide(); + progressBar->hide(); - const LoadSetsResult result = watcher.result(); - - if (result.plainXml) { - wizard()->downloadedPlainXml = true; - wizard()->xmlData = result.xmlData; + if (wizard()->downloadedPlainXml || watcher.future().result()) { + wizard()->next(); + } else { + QMessageBox::critical(this, tr("Error"), + tr("The file was retrieved successfully, but it does not contain any sets data.")); } - - if (wizard()->backgroundMode) { - progressLabel->hide(); - progressBar->hide(); - if (!result.errorMessage.isEmpty()) { - qWarning() << result.errorMessage; - } else if (!result.ok && !result.plainXml) { - qWarning() << tr("The file was retrieved successfully, but it does not contain any sets data."); - } - emit readyToContinue(); - return; - } - - const auto fail = [this](const QString &message) { - progressLabel->hide(); - progressBar->hide(); - QMessageBox::critical(this, tr("Error"), message); - }; - - if (!result.errorMessage.isEmpty()) { - if (result.offerUncompressedFallback) { - zipDownloadFailed(result.errorMessage); - return; - } - fail(result.errorMessage); - return; - } - - if (!result.ok && !result.plainXml) { - fail(tr("The file was retrieved successfully, but it does not contain any sets data.")); - return; - } - - // Snap the bar to 100% and hold it there for a moment so the completed state - // is actually visible before the next page's own import progress takes over - // (a zero-length deferral can still fire before the repaint is delivered). - progressBar->setMaximum(1); - progressBar->setValue(1); - progressLabel->setText(tr("Parsing file (100%)")); - QTimer::singleShot(500, this, [this] { - if (wizard()->currentPage() == this) { - // Leave the page pristine: hide the completed load bar so a later - // Back from the save page doesn't show stale progress. - progressLabel->hide(); - progressBar->hide(); - wizard()->next(); - } - }); } SaveSetsPage::SaveSetsPage(QWidget *parent) : OracleWizardPage(parent) @@ -682,113 +527,46 @@ SaveSetsPage::SaveSetsPage(QWidget *parent) : OracleWizardPage(parent) pathLabel = new QLabel(this); saveLabel = new QLabel(this); - progressBar = new QProgressBar(this); - progressBar->hide(); - defaultPathCheckBox = new QCheckBox(this); messageLog = new QTextEdit(this); messageLog->setReadOnly(true); auto *layout = new QGridLayout(this); - layout->addWidget(progressBar, 0, 0); - layout->addWidget(messageLog, 1, 0); - layout->addWidget(saveLabel, 2, 0); - layout->addWidget(pathLabel, 3, 0); - layout->addWidget(defaultPathCheckBox, 4, 0); + layout->addWidget(messageLog, 0, 0); + layout->addWidget(saveLabel, 1, 0); + layout->addWidget(pathLabel, 2, 0); + layout->addWidget(defaultPathCheckBox, 3, 0); setLayout(layout); } -bool SaveSetsPage::isComplete() const -{ - return !importActive; -} - void SaveSetsPage::cleanupPage() { - cancelWork(); - disconnect(wizard()->importer, &OracleImporter::setIndexChanged, this, &SaveSetsPage::updateTotalProgress); - disconnect(&importWatcher, &QFutureWatcher::finished, this, &SaveSetsPage::importFinished); wizard()->importer->clear(); + disconnect(wizard()->importer, &OracleImporter::setIndexChanged, nullptr, nullptr); } void SaveSetsPage::initializePage() { + messageLog->clear(); + retranslateUi(); if (wizard()->downloadedPlainXml) { messageLog->hide(); - progressBar->hide(); - if (wizard()->backgroundMode) { - emit readyToContinue(); + } else { + messageLog->show(); + connect(wizard()->importer, &OracleImporter::setIndexChanged, this, &SaveSetsPage::updateTotalProgress); + + int setsImported = wizard()->importer->startImport(); + + if (setsImported == 0) { + QMessageBox::critical(this, tr("Error"), tr("No set has been imported.")); } - return; - } - - messageLog->clear(); - messageLog->show(); - progressBar->show(); - - totalSets = wizard()->importer->getSets().size(); - progressBar->setRange(0, totalSets); - progressBar->setValue(0); - - connect(wizard()->importer, &OracleImporter::setIndexChanged, this, &SaveSetsPage::updateTotalProgress, - Qt::UniqueConnection); - connect(&importWatcher, &QFutureWatcher::finished, this, &SaveSetsPage::importFinished, Qt::UniqueConnection); - - wizard()->disableButtons(); - importActive = true; - emit completeChanged(); - - const QPointer importer = wizard()->importer; - importFuture = QtConcurrent::run([importer] { return importer ? importer->startImport() : 0; }); - importWatcher.setFuture(importFuture); -} - -void SaveSetsPage::cancelWork() -{ - if (!importActive) { - return; - } - // Ask the worker to stop at the next set boundary, then wait it out so the - // wizard (and the importer it owns) is never torn down under a running thread. - importActive = false; - emit completeChanged(); - wizard()->importer->cancelImport(); - importFuture.cancel(); - importWatcher.cancel(); - importFuture.waitForFinished(); -} - -void SaveSetsPage::importFinished() -{ - if (!importActive) { - return; - } - importActive = false; - emit completeChanged(); - - wizard()->enableButtons(); - - const int setsImported = importWatcher.result(); - const QPointer importer = wizard()->importer; - if (importer) { - importer->releaseSetData(); } if (wizard()->backgroundMode) { - if (setsImported == 0) { - qWarning() << tr("No set has been imported."); - } emit readyToContinue(); - return; - } - - progressBar->setValue(progressBar->maximum()); - - if (setsImported == 0) { - QMessageBox::critical(this, tr("Error"), tr("No set has been imported.")); } } @@ -810,28 +588,13 @@ void SaveSetsPage::retranslateUi() setButtonText(QWizard::NextButton, tr("&Save")); } -void SaveSetsPage::updateTotalProgress(int cardsImported, int setIndex, const QString &setName) +void SaveSetsPage::updateTotalProgress(int cardsImported, int /* setIndex */, const QString &setName) { - if (!importActive) { - return; - } if (setName.isEmpty()) { - progressBar->setValue(progressBar->maximum()); - const int cardCount = wizard()->importer->getCardList().size(); - if (wizard()->backgroundMode) { - qInfo() << tr("Import finished: %1 cards.").arg(cardCount); - emitBackgroundProgress("import", totalSets, totalSets); - } else { - messageLog->append("" + tr("Import finished: %1 cards.").arg(cardCount) + ""); - } + messageLog->append("" + tr("Import finished: %1 cards.").arg(wizard()->importer->getCardList().size()) + + ""); } else { - progressBar->setValue(setIndex); - if (wizard()->backgroundMode) { - qInfo() << tr("%1: %2 cards imported").arg(setName).arg(cardsImported); - emitBackgroundProgress("import", setIndex, totalSets); - } else { - messageLog->append(tr("%1: %2 cards imported").arg(setName).arg(cardsImported)); - } + messageLog->append(tr("%1: %2 cards imported").arg(setName).arg(cardsImported)); } messageLog->verticalScrollBar()->setValue(messageLog->verticalScrollBar()->maximum()); @@ -977,4 +740,4 @@ void LoadSpoilersPage::retranslateUi() pathLabel->setText(tr("The spoiler database will be saved at the following location:") + "
" + SettingsCache::instance().getSpoilerCardDatabasePath()); defaultPathCheckBox->setText(tr("Save to a custom path (not recommended)")); -} +} \ No newline at end of file diff --git a/oracle/src/pages.h b/oracle/src/pages.h index d0a9fcd43..066cc2e1b 100644 --- a/oracle/src/pages.h +++ b/oracle/src/pages.h @@ -3,10 +3,8 @@ #include "pagetemplates.h" -#include #include #include -#include #include #include #include @@ -59,31 +57,19 @@ protected: void initializePage() override; }; -/** @brief Result of a worker-thread sets-file load (read + decompress + dispatch). */ -struct LoadSetsResult -{ - bool ok = false; ///< JSON scan produced set data (or plain XML was handled) - bool plainXml = false; ///< input was a plain Cockatrice XML database - QByteArray xmlData; ///< raw XML for the plain-XML path - QString errorMessage; ///< set when the input could not be processed - bool offerUncompressedFallback = false; ///< decompression-only failure: offer the uncompressed URL -}; - class LoadSetsPage : public OracleWizardPage { Q_OBJECT public: explicit LoadSetsPage(QWidget *parent = nullptr); void retranslateUi() override; - bool isComplete() const override; protected: void initializePage() override; bool validatePage() override; void readSetsFromByteArray(QByteArray _data); - void readSetsFromFile(const QString &fileName); + void readSetsFromByteArrayRef(QByteArray &_data); void downloadSetsFile(const QUrl &url); - void cancelWork() override; private: QRadioButton *urlRadioButton; @@ -95,19 +81,15 @@ private: QLabel *progressLabel; QProgressBar *progressBar; - QFutureWatcher watcher; - QFuture future; - bool loadActive = false; - - void beginLoadSets(bool compressedFile = false); + QFutureWatcher watcher; + QFuture future; + QByteArray jsonData; private slots: void actLoadSetsFile(); void actRestoreDefaultUrl(); void actDownloadProgressSetsFile(qint64 received, qint64 total); void actDownloadFinishedSetsFile(); - void updateParsingProgress(int bytesRead, int totalBytes); - void scanProgressToStdout(int bytesRead, int totalBytes); void importFinished(); void zipDownloadFailed(const QString &message); }; @@ -118,28 +100,19 @@ class SaveSetsPage : public OracleWizardPage public: explicit SaveSetsPage(QWidget *parent = nullptr); void retranslateUi() override; - bool isComplete() const override; private: QTextEdit *messageLog; - QProgressBar *progressBar; QCheckBox *defaultPathCheckBox; QLabel *pathLabel; QLabel *saveLabel; - QFutureWatcher importWatcher; - QFuture importFuture; - int totalSets = 0; - bool importActive = false; - protected: void initializePage() override; void cleanupPage() override; bool validatePage() override; - void cancelWork() override; private slots: - void importFinished(); void updateTotalProgress(int cardsImported, int setIndex, const QString &setName); }; diff --git a/oracle/src/pagetemplates.h b/oracle/src/pagetemplates.h index ccad7ec62..6e79c867e 100644 --- a/oracle/src/pagetemplates.h +++ b/oracle/src/pagetemplates.h @@ -20,14 +20,6 @@ public: } virtual void retranslateUi() = 0; - /** - * @brief Asks an active page to stop any background worker before the wizard - * (and its importer) can be torn down underneath it. Default is a no-op. - */ - virtual void cancelWork() - { - } - signals: void readyToContinue(); diff --git a/oracle/src/qt-json/AUTHORS b/oracle/src/qt-json/AUTHORS new file mode 100644 index 000000000..29a85929f --- /dev/null +++ b/oracle/src/qt-json/AUTHORS @@ -0,0 +1,3 @@ +Eeli Reilin +Luis Gustavo S. Barreto +Stephen Kockentiedt diff --git a/oracle/src/qt-json/LICENSE b/oracle/src/qt-json/LICENSE new file mode 100644 index 000000000..3c42b515a --- /dev/null +++ b/oracle/src/qt-json/LICENSE @@ -0,0 +1,27 @@ +Copyright 2011 Eeli Reilin. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY ''AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL EELI REILIN OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation +are those of the authors and should not be interpreted as representing +official policies, either expressed or implied, of Eeli Reilin. + diff --git a/oracle/src/qt-json/README b/oracle/src/qt-json/README new file mode 100644 index 000000000..b60c1599b --- /dev/null +++ b/oracle/src/qt-json/README @@ -0,0 +1,96 @@ +######################################################################## +1. INTRODUCTION + +The Json class is a simple class for parsing JSON data into a QVariant +hierarchies. Now, we can also reverse the process and serialize +QVariant hierarchies into valid JSON data. + + +######################################################################## +2. HOW TO USE + +The parser is really easy to use. Let's say we have the following +QString of JSON data: + +------------------------------------------------------------------------ +{ + "encoding" : "UTF-8", + "plug-ins" : [ + "python", + "c++", + "ruby" + ], + "indent" : { + "length" : 3, + "use_space" : true + } +} +------------------------------------------------------------------------ + +We would first call the parse-method: + +------------------------------------------------------------------------ +//Say that we're using the QtJson namespace +using namespace QtJson; +bool ok; +//json is a QString containing the JSON data +QVariantMap result = Json::parse(json, ok).toMap(); + +if(!ok) { + qFatal("An error occurred during parsing"); + exit(1); +} +------------------------------------------------------------------------ + +Assuming the parsing process completed without errors, we would then +go through the hierarchy: + +------------------------------------------------------------------------ +qDebug() << "encoding:" << result["encoding"].toString(); +qDebug() << "plugins:"; + +foreach(QVariant plugin, result["plug-ins"].toList()) { + qDebug() << "\t-" << plugin.toString(); +} + +QVariantMap nestedMap = result["indent"].toMap(); +qDebug() << "length:" << nestedMap["length"].toInt(); +qDebug() << "use_space:" << nestedMap["use_space"].toBool(); +------------------------------------------------------------------------ + +The previous code would print out the following: + +------------------------------------------------------------------------ +encoding: "UTF-8" +plugins: + - "python" + - "c++" + - "ruby" +length: 3 +use_space: true +------------------------------------------------------------------------ + +To write JSON data from Qt object is as simple as parsing: + +------------------------------------------------------------------------ +QVariantMap map; +map["name"] = "Name"; +map["age"] = 22; + +QByteArray data = Json::serialize(map); +------------------------------------------------------------------------ + +The byte array 'data' contains valid JSON data: + +------------------------------------------------------------------------ +{ + name: "Luis Gustavo", + age: 22, +} +------------------------------------------------------------------------ + + +######################################################################## +4. CONTRIBUTING + +The code is available to download at GitHub. Contribute if you dare! diff --git a/oracle/src/qt-json/json.cpp b/oracle/src/qt-json/json.cpp new file mode 100644 index 000000000..ff739b49d --- /dev/null +++ b/oracle/src/qt-json/json.cpp @@ -0,0 +1,545 @@ +/* Copyright 2011 Eeli Reilin. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY ''AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL EELI REILIN OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation + * are those of the authors and should not be interpreted as representing + * official policies, either expressed or implied, of Eeli Reilin. + */ + +/** + * \file json.cpp + */ + +#include "json.h" + +#include +#include + +namespace QtJson +{ + +static QString sanitizeString(QString str) +{ + str.replace(QLatin1String("\\"), QLatin1String("\\\\")); + str.replace(QLatin1String("\""), QLatin1String("\\\"")); + str.replace(QLatin1String("\b"), QLatin1String("\\b")); + str.replace(QLatin1String("\f"), QLatin1String("\\f")); + str.replace(QLatin1String("\n"), QLatin1String("\\n")); + str.replace(QLatin1String("\r"), QLatin1String("\\r")); + str.replace(QLatin1String("\t"), QLatin1String("\\t")); + return QString(QLatin1String("\"%1\"")).arg(str); +} + +static QByteArray join(const QList &list, const QByteArray &sep) +{ + QByteArray res; + for (const QByteArray &i : list) { + if (!res.isEmpty()) { + res += sep; + } + res += i; + } + return res; +} + +/** + * parse + */ +QVariant Json::parse(const QString &json) +{ + bool success = true; + return Json::parse(json, success); +} + +/** + * parse + */ +QVariant Json::parse(const QString &json, bool &success) +{ + success = true; + + // Return an empty QVariant if the JSON data is either null or empty + if (!json.isNull() || !json.isEmpty()) { + // We'll start from index 0 + int index = 0; + + // Parse the first value + QVariant value = Json::parseValue(json, index, success); + + // Return the parsed value + return value; + } else { + // Return the empty QVariant + return QVariant(); + } +} + +QByteArray Json::serialize(const QVariant &data) +{ + bool success = true; + return Json::serialize(data, success); +} + +QByteArray Json::serialize(const QVariant &data, bool &success) +{ + QByteArray str; + success = true; + + if (!data.isValid()) // invalid or null? + { + str = "null"; + } + else if ((data.typeId() == QMetaType::Type::QVariantList) || + (data.typeId() == QMetaType::Type::QStringList)) // variant is a list? + { + QList values; + const QVariantList list = data.toList(); + for (const QVariant &v : list) { + QByteArray serializedValue = serialize(v); + if (serializedValue.isNull()) { + success = false; + break; + } + values << serializedValue; + } + + str = "[ " + join(values, ", ") + " ]"; + } + else if ((data.typeId() == QMetaType::Type::QVariantHash)) // variant is a hash? + { + const QVariantHash vhash = data.toHash(); + QHashIterator it(vhash); + str = "{ "; + QList pairs; + + while (it.hasNext()) { + it.next(); + QByteArray serializedValue = serialize(it.value()); + + if (serializedValue.isNull()) { + success = false; + break; + } + + pairs << sanitizeString(it.key()).toUtf8() + " : " + serializedValue; + } + + str += join(pairs, ", "); + str += " }"; + } + else if ((data.typeId() == QMetaType::Type::QVariantMap)) // variant is a map? + { + const QVariantMap vmap = data.toMap(); + QMapIterator it(vmap); + str = "{ "; + QList pairs; + while (it.hasNext()) { + it.next(); + QByteArray serializedValue = serialize(it.value()); + if (serializedValue.isNull()) { + success = false; + break; + } + pairs << sanitizeString(it.key()).toUtf8() + " : " + serializedValue; + } + str += join(pairs, ", "); + str += " }"; + } + else if ((data.typeId() == QMetaType::Type::QString) || + (data.typeId() == QMetaType::Type::QByteArray)) // a string or a byte array? + { + str = sanitizeString(data.toString()).toUtf8(); + } + else if (data.typeId() == QMetaType::Type::Double) // double? + { + str = QByteArray::number(data.toDouble(), 'g', 20); + if (!str.contains(".") && !str.contains("e")) { + str += ".0"; + } + } + else if (data.typeId() == QMetaType::Type::Bool) // boolean value? + { + str = data.toBool() ? "true" : "false"; + } + else if (data.typeId() == QMetaType::Type::ULongLong) // large unsigned number? + { + str = QByteArray::number(data.value()); + } else if (data.canConvert()) // any signed number? + { + str = QByteArray::number(data.value()); + } else if (data.canConvert()) { + str = QString::number(data.value()).toUtf8(); + } else if (data.canConvert()) // can value be converted to string? + { + // this will catch QDate, QDateTime, QUrl, ... + str = sanitizeString(data.toString()).toUtf8(); + } else { + success = false; + } + if (success) { + return str; + } else { + return QByteArray(); + } +} + +/** + * parseValue + */ +QVariant Json::parseValue(const QString &json, int &index, bool &success) +{ + // Determine what kind of data we should parse by + // checking out the upcoming token + switch (Json::lookAhead(json, index)) { + case JsonTokenString: + return Json::parseString(json, index, success); + case JsonTokenNumber: + return Json::parseNumber(json, index); + case JsonTokenCurlyOpen: + return Json::parseObject(json, index, success); + case JsonTokenSquaredOpen: + return Json::parseArray(json, index, success); + case JsonTokenTrue: + Json::nextToken(json, index); + return QVariant(true); + case JsonTokenFalse: + Json::nextToken(json, index); + return QVariant(false); + case JsonTokenNull: + Json::nextToken(json, index); + return QVariant(); + case JsonTokenNone: + break; + } + + // If there were no tokens, flag the failure and return an empty QVariant + success = false; + return QVariant(); +} + +/** + * parseObject + */ +QVariant Json::parseObject(const QString &json, int &index, bool &success) +{ + QVariantMap map; + int token; + + // Get rid of the whitespace and increment index + Json::nextToken(json, index); + + // Loop through all of the key/value pairs of the object + bool done = false; + while (!done) { + // Get the upcoming token + token = Json::lookAhead(json, index); + + if (token == JsonTokenNone) { + success = false; + return QVariantMap(); + } else if (token == JsonTokenComma) { + Json::nextToken(json, index); + } else if (token == JsonTokenCurlyClose) { + Json::nextToken(json, index); + return map; + } else { + // Parse the key/value pair's name + QString name = Json::parseString(json, index, success).toString(); + + if (!success) { + return QVariantMap(); + } + + // Get the next token + token = Json::nextToken(json, index); + + // If the next token is not a colon, flag the failure + // return an empty QVariant + if (token != JsonTokenColon) { + success = false; + return QVariant(QVariantMap()); + } + + // Parse the key/value pair's value + QVariant value = Json::parseValue(json, index, success); + + if (!success) { + return QVariantMap(); + } + + // Assign the value to the key in the map + map[name] = value; + } + } + + // Return the map successfully + return QVariant(map); +} + +/** + * parseArray + */ +QVariant Json::parseArray(const QString &json, int &index, bool &success) +{ + QVariantList list; + + Json::nextToken(json, index); + + bool done = false; + while (!done) { + int token = Json::lookAhead(json, index); + + if (token == JsonTokenNone) { + success = false; + return QVariantList(); + } else if (token == JsonTokenComma) { + Json::nextToken(json, index); + } else if (token == JsonTokenSquaredClose) { + Json::nextToken(json, index); + break; + } else { + QVariant value = Json::parseValue(json, index, success); + + if (!success) { + return QVariantList(); + } + + list.push_back(value); + } + } + + return QVariant(list); +} + +/** + * parseString + */ +QVariant Json::parseString(const QString &json, int &index, bool &success) +{ + QString s; + QChar c; + + Json::eatWhitespace(json, index); + + c = json[index++]; + + bool complete = false; + while (!complete) { + if (index == json.size()) { + break; + } + + c = json[index++]; + + if (c == '\"') { + complete = true; + break; + } else if (c == '\\') { + if (index == json.size()) { + break; + } + + c = json[index++]; + + if (c == '\"') { + s.append('\"'); + } else if (c == '\\') { + s.append('\\'); + } else if (c == '/') { + s.append('/'); + } else if (c == 'b') { + s.append('\b'); + } else if (c == 'f') { + s.append('\f'); + } else if (c == 'n') { + s.append('\n'); + } else if (c == 'r') { + s.append('\r'); + } else if (c == 't') { + s.append('\t'); + } else if (c == 'u') { + int remainingLength = json.size() - index; + + if (remainingLength >= 4) { + QString unicodeStr = json.mid(index, 4); + + int symbol = unicodeStr.toInt(0, 16); + + s.append(QChar(symbol)); + + index += 4; + } else { + break; + } + } + } else { + s.append(c); + } + } + + if (!complete) { + success = false; + return QVariant(); + } + + return QVariant(s); +} + +/** + * parseNumber + */ +QVariant Json::parseNumber(const QString &json, int &index) +{ + Json::eatWhitespace(json, index); + + int lastIndex = Json::lastIndexOfNumber(json, index); + int charLength = (lastIndex - index) + 1; + QString numberStr; + + numberStr = json.mid(index, charLength); + + index = lastIndex + 1; + + if (numberStr.contains('.')) { + return QVariant(numberStr.toDouble(NULL)); + } else if (numberStr.startsWith('-')) { + return QVariant(numberStr.toLongLong(NULL)); + } else { + return QVariant(numberStr.toULongLong(NULL)); + } +} + +/** + * lastIndexOfNumber + */ +int Json::lastIndexOfNumber(const QString &json, int index) +{ + static const QString numericCharacters("0123456789+-.eE"); + int lastIndex; + + for (lastIndex = index; lastIndex < json.size(); lastIndex++) { + if (numericCharacters.indexOf(json[lastIndex]) == -1) { + break; + } + } + + return lastIndex - 1; +} + +/** + * eatWhitespace + */ +void Json::eatWhitespace(const QString &json, int &index) +{ + static const QString whitespaceChars(" \t\n\r"); + for (; index < json.size(); index++) { + if (whitespaceChars.indexOf(json[index]) == -1) { + break; + } + } +} + +/** + * lookAhead + */ +int Json::lookAhead(const QString &json, int index) +{ + int saveIndex = index; + return Json::nextToken(json, saveIndex); +} + +/** + * nextToken + */ +int Json::nextToken(const QString &json, int &index) +{ + Json::eatWhitespace(json, index); + + if (index == json.size()) { + return JsonTokenNone; + } + + QChar c = json[index]; + index++; + switch (c.toLatin1()) { + case '{': + return JsonTokenCurlyOpen; + case '}': + return JsonTokenCurlyClose; + case '[': + return JsonTokenSquaredOpen; + case ']': + return JsonTokenSquaredClose; + case ',': + return JsonTokenComma; + case '"': + return JsonTokenString; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '-': + return JsonTokenNumber; + case ':': + return JsonTokenColon; + } + + index--; + + int remainingLength = json.size() - index; + + // True + if (remainingLength >= 4) { + if (json[index] == 't' && json[index + 1] == 'r' && json[index + 2] == 'u' && json[index + 3] == 'e') { + index += 4; + return JsonTokenTrue; + } + } + + // False + if (remainingLength >= 5) { + if (json[index] == 'f' && json[index + 1] == 'a' && json[index + 2] == 'l' && json[index + 3] == 's' && + json[index + 4] == 'e') { + index += 5; + return JsonTokenFalse; + } + } + + // Null + if (remainingLength >= 4) { + if (json[index] == 'n' && json[index + 1] == 'u' && json[index + 2] == 'l' && json[index + 3] == 'l') { + index += 4; + return JsonTokenNull; + } + } + + return JsonTokenNone; +} + +} // namespace QtJson diff --git a/oracle/src/qt-json/json.h b/oracle/src/qt-json/json.h new file mode 100644 index 000000000..cf0499d4e --- /dev/null +++ b/oracle/src/qt-json/json.h @@ -0,0 +1,204 @@ +/* Copyright 2011 Eeli Reilin. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY ''AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL EELI REILIN OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation + * are those of the authors and should not be interpreted as representing + * official policies, either expressed or implied, of Eeli Reilin. + */ + +/** + * \file json.h + */ + +#ifndef JSON_H +#define JSON_H + +#include +#include + +namespace QtJson +{ + +/** + * \enum JsonToken + */ +enum JsonToken +{ + JsonTokenNone = 0, + JsonTokenCurlyOpen = 1, + JsonTokenCurlyClose = 2, + JsonTokenSquaredOpen = 3, + JsonTokenSquaredClose = 4, + JsonTokenColon = 5, + JsonTokenComma = 6, + JsonTokenString = 7, + JsonTokenNumber = 8, + JsonTokenTrue = 9, + JsonTokenFalse = 10, + JsonTokenNull = 11 +}; + +/** + * \class Json + * \brief A JSON data parser + * + * Json parses a JSON data into a QVariant hierarchy. + */ +class Json +{ + public: + /** + * Parse a JSON string + * + * \param json The JSON data + */ + static QVariant parse(const QString &json); + + /** + * Parse a JSON string + * + * \param json The JSON data + * \param success The success of the parsing + */ + static QVariant parse(const QString &json, bool &success); + + /** + * This method generates a textual JSON representation + * + * \param data The JSON data generated by the parser. + * \param success The success of the serialization + */ + static QByteArray serialize(const QVariant &data); + + /** + * This method generates a textual JSON representation + * + * \param data The JSON data generated by the parser. + * \param success The success of the serialization + * + * \return QByteArray Textual JSON representation + */ + static QByteArray serialize(const QVariant &data, bool &success); + + private: + /** + * Parses a value starting from index + * + * \param json The JSON data + * \param index The start index + * \param success The success of the parse process + * + * \return QVariant The parsed value + */ + static QVariant parseValue(const QString &json, int &index, + bool &success); + + /** + * Parses an object starting from index + * + * \param json The JSON data + * \param index The start index + * \param success The success of the object parse + * + * \return QVariant The parsed object map + */ + static QVariant parseObject(const QString &json, int &index, + bool &success); + + /** + * Parses an array starting from index + * + * \param json The JSON data + * \param index The starting index + * \param success The success of the array parse + * + * \return QVariant The parsed variant array + */ + static QVariant parseArray(const QString &json, int &index, + bool &success); + + /** + * Parses a string starting from index + * + * \param json The JSON data + * \param index The starting index + * \param success The success of the string parse + * + * \return QVariant The parsed string + */ + static QVariant parseString(const QString &json, int &index, + bool &success); + + /** + * Parses a number starting from index + * + * \param json The JSON data + * \param index The starting index + * + * \return QVariant The parsed number + */ + static QVariant parseNumber(const QString &json, int &index); + + /** + * Get the last index of a number starting from index + * + * \param json The JSON data + * \param index The starting index + * + * \return The last index of the number + */ + static int lastIndexOfNumber(const QString &json, int index); + + /** + * Skip unwanted whitespace symbols starting from index + * + * \param json The JSON data + * \param index The start index + */ + static void eatWhitespace(const QString &json, int &index); + + /** + * Check what token lies ahead + * + * \param json The JSON data + * \param index The starting index + * + * \return int The upcoming token + */ + static int lookAhead(const QString &json, int index); + + /** + * Get the next JSON token + * + * \param json The JSON data + * \param index The starting index + * + * \return int The next JSON token + */ + static int nextToken(const QString &json, int &index); +}; + + +} //end namespace + +#endif //JSON_H diff --git a/oracle/src/raw_json_scanner.cpp b/oracle/src/raw_json_scanner.cpp deleted file mode 100644 index 8c4633598..000000000 --- a/oracle/src/raw_json_scanner.cpp +++ /dev/null @@ -1,677 +0,0 @@ -#include "raw_json_scanner.h" - -#include -#include - -namespace -{ - -// Nesting cap matching QJsonDocument's limit, so a pathologically deep document -// fails shallowly instead of overflowing the stack through the recursive -// skipValue/skipArray/skipObject walk (Qt's parser caps at 1024 for the same -// reason and reports DeepNesting). -constexpr int kMaxNestingDepth = 1024; - -/** - * @brief Throttled byte-position reporting for scanSetRanges(). - * - * Threaded through the skip walk so progress can be reported without materializing - * the whole document. Reports are rate-limited so a GUI showing progress isn't - * flooded with interrupts: a callback is invoked at most ~100 times per scan - * regardless of element count. The closing stretch (the last ~1%) is reported - * more finely so a large document doesn't stall the progress bar on the final - * percent before the scan wraps up. - */ -struct ScanProgress -{ - const char *begin = nullptr; - qsizetype size = 0; - RawJson::ScanProgressCallback callback; - qsizetype step = 1; - qsizetype lastReported = 0; - - /** - * @brief Reports the scanner's absolute offset, unless within @p step bytes - * of the previous report and not yet at the end of the document. - */ - void report(const char *p) - { - if (!callback) { - return; - } - const qsizetype offset = p - begin; - if (offset == lastReported) { - return; // the final element often already sits exactly at the end - } - const qsizetype reportingStep = offset >= size - step ? std::max(1, step / 16) : step; - if (offset - lastReported < reportingStep && offset < size) { - return; - } - lastReported = offset; - callback(offset, size); - } -}; - -inline bool isWhitespace(char c) -{ - return c == ' ' || c == '\t' || c == '\r' || c == '\n'; -} - -const char *skipWhitespace(const char *p, const char *end) -{ - while (p < end && isWhitespace(*p)) { - ++p; - } - return p; -} - -inline bool isHexDigit(char c) -{ - return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); -} - -inline quint8 hexValue(char c) -{ - if (c >= '0' && c <= '9') { - return c - '0'; - } - if (c >= 'a' && c <= 'f') { - return c - 'a' + 10; - } - return c - 'A' + 10; -} - -/** - * @brief Skips past a JSON string without decoding it, validating escapes. - * @param p In: pointing at the opening quote. Out: pointing past the closing quote. - */ -bool skipString(const char *&p, const char *end) -{ - ++p; // opening quote - for (;;) { - const void *quote = memchr(p, '"', static_cast(end - p)); - if (!quote) { - return false; // unterminated string - } - // Backslash escapes can only appear before the closing quote, so bound - // the scan to the string extent instead of the rest of the document. - const void *backslash = memchr(p, '\\', static_cast(static_cast(quote) - p)); - if (!backslash) { - p = static_cast(quote) + 1; - return true; - } - const char *b = static_cast(backslash); - if (end - b < 2) { - return false; - } - const char escaped = b[1]; - if (escaped == 'u') { - if (end - b < 6) { - return false; - } - quint32 codepoint = 0; - for (int i = 0; i < 4; ++i) { - if (!isHexDigit(b[2 + i])) { - return false; - } - codepoint = codepoint * 16 + hexValue(b[2 + i]); - } - p = b + 6; - if (codepoint >= 0xD800 && codepoint <= 0xDBFF) { - // expect the low-surrogate escape for the second half - if (end - p < 6 || p[0] != '\\' || p[1] != 'u') { - return false; // unpaired high surrogate - } - quint32 low = 0; - for (int i = 0; i < 4; ++i) { - if (!isHexDigit(p[2 + i])) { - return false; - } - low = low * 16 + hexValue(p[2 + i]); - } - if (low < 0xDC00 || low > 0xDFFF) { - return false; - } - p += 6; - } else if (codepoint >= 0xDC00 && codepoint <= 0xDFFF) { - return false; // unpaired low surrogate - } - continue; - } - switch (escaped) { - case '"': - case '\\': - case '/': - case 'b': - case 'f': - case 'n': - case 'r': - case 't': - p = b + 2; - continue; - default: - return false; // invalid escape - } - } -} - -/** - * @brief Decodes a JSON string into @p out, validating it as it goes. - * @param p In: pointing at the opening quote. Out: pointing past the closing quote. - */ -bool decodeString(const char *&p, const char *end, QString &out) -{ - out.clear(); - QByteArray utf8; - auto flush = [&out, &utf8]() { - if (!utf8.isEmpty()) { - out += QString::fromUtf8(utf8); - utf8.clear(); - } - }; - - ++p; // opening quote - while (p < end) { - const char c = *p; - if (c == '\\') { - flush(); - ++p; // escaped character - if (p >= end) { - return false; - } - const char escaped = *p; - if (escaped == 'u') { - ++p; // first hex digit - if (p + 4 > end) { - return false; - } - quint32 codepoint = 0; - for (int i = 0; i < 4; ++i) { - if (!isHexDigit(p[i])) { - return false; - } - codepoint = codepoint * 16 + hexValue(p[i]); - } - p += 4; - if (codepoint >= 0xD800 && codepoint <= 0xDBFF) { - // expect a low-surrogate escape for the second half - if (p + 6 > end || p[0] != '\\' || p[1] != 'u') { - return false; // unpaired high surrogate - } - quint32 low = 0; - for (int i = 0; i < 4; ++i) { - if (!isHexDigit(p[2 + i])) { - return false; - } - low = low * 16 + hexValue(p[2 + i]); - } - if (low < 0xDC00 || low > 0xDFFF) { - return false; - } - out += QChar(codepoint); - out += QChar(low); - p += 6; - } else if (codepoint >= 0xDC00 && codepoint <= 0xDFFF) { - return false; // unpaired low surrogate - } else { - out += QChar(codepoint); - } - continue; - } - switch (escaped) { - case '"': - out += '"'; - break; - case '\\': - out += '\\'; - break; - case '/': - out += '/'; - break; - case 'b': - out += '\b'; - break; - case 'f': - out += '\f'; - break; - case 'n': - out += '\n'; - break; - case 'r': - out += '\r'; - break; - case 't': - out += '\t'; - break; - default: - return false; - } - ++p; - continue; - } - if (c == '"') { - ++p; - flush(); - return true; - } - // Deliberately accept unescaped control characters (e.g. a tab inside - // a set name): QJsonDocument and skipString accept them too, so - // rejecting them here would fail the whole document on a byte that - // Qt is fine with — the very total-failure mode this scanner avoids. - utf8 += c; - ++p; - } - return false; -} - -/** - * @brief Reads a set-metadata field, tolerating null and non-string values. - * - * A set's metadata may carry null or non-string values in otherwise-valid - * payloads ("releaseDate": null, "type": 7). The token itself was already - * structurally validated by skipValue, so a non-string value is accepted and - * leaves @p out at its default (empty) — one bad set must not abort the - * import of every other set in the document. - */ -bool decodeStringMember(const char *&fs, const char *&fe, QString &out) -{ - if (fs >= fe) { - return false; - } - if (*fs != '"') { - return true; - } - return decodeString(fs, fe, out); -} - -bool matchLiteral(const char *&p, const char *end, const char *literal, int length) -{ - if (end - p < length || memcmp(p, literal, static_cast(length)) != 0) { - return false; - } - const char *after = p + length; - if (after < end && (QChar::isLetter(*after) || QChar::isDigit(*after) || *after == '_')) { - return false; - } - p = after; - return true; -} - -bool skipNumber(const char *&p, const char *end) -{ - // JSON number: -?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)? - if (p < end && *p == '-') { - ++p; - } - if (p < end && *p == '0') { - ++p; - } else if (p < end && *p >= '1' && *p <= '9') { - ++p; - while (p < end && QChar::isDigit(*p)) { - ++p; - } - } else { - return false; - } - if (p < end && *p == '.') { - ++p; - if (p >= end || !QChar::isDigit(*p)) { - return false; - } - while (p < end && QChar::isDigit(*p)) { - ++p; - } - } - if (p < end && (*p == 'e' || *p == 'E')) { - ++p; - if (p < end && (*p == '+' || *p == '-')) { - ++p; - } - if (p >= end || !QChar::isDigit(*p)) { - return false; - } - while (p < end && QChar::isDigit(*p)) { - ++p; - } - } - return true; -} - -bool skipValue(const char *&p, const char *end, int depth, ScanProgress &scan); -bool skipObject(const char *&p, const char *end, int depth, ScanProgress &scan); -bool skipArray(const char *&p, const char *end, int depth, ScanProgress &scan); - -bool skipPrimitive(const char *&p, const char *end) -{ - if (p >= end) { - return false; - } - const char c = *p; - if (c == '"') { - return skipString(p, end); - } - if (c == 't') { - return matchLiteral(p, end, "true", 4); - } - if (c == 'f') { - return matchLiteral(p, end, "false", 5); - } - if (c == 'n') { - return matchLiteral(p, end, "null", 4); - } - if (c == '-' || (c >= '0' && c <= '9')) { - return skipNumber(p, end); - } - return false; -} - -bool skipObject(const char *&p, const char *end, int depth, ScanProgress &scan) -{ - if (depth <= 0) { - return false; // nest deeper than the cap - } - ++p; // '{' - p = skipWhitespace(p, end); - if (p < end && *p == '}') { - ++p; - return true; - } - for (;;) { - p = skipWhitespace(p, end); - if (p >= end || *p != '"') { - return false; - } - if (!skipString(p, end)) { - return false; - } - p = skipWhitespace(p, end); - if (p >= end || *p != ':') { - return false; - } - ++p; - if (!skipValue(p, end, depth - 1, scan)) { - return false; - } - scan.report(p); - p = skipWhitespace(p, end); - if (p >= end) { - return false; - } - if (*p == ',') { - ++p; - continue; - } - if (*p == '}') { - ++p; - return true; - } - return false; - } -} - -bool skipArray(const char *&p, const char *end, int depth, ScanProgress &scan) -{ - if (depth <= 0) { - return false; // nest deeper than the cap - } - ++p; // '[' - p = skipWhitespace(p, end); - if (p < end && *p == ']') { - ++p; - return true; - } - for (;;) { - if (!skipValue(p, end, depth - 1, scan)) { - return false; - } - scan.report(p); - p = skipWhitespace(p, end); - if (p >= end) { - return false; - } - if (*p == ',') { - ++p; - continue; - } - if (*p == ']') { - ++p; - return true; - } - return false; - } -} - -bool skipValue(const char *&p, const char *end, int depth, ScanProgress &scan) -{ - p = skipWhitespace(p, end); - if (p >= end) { - return false; - } - const char c = *p; - if (c == '{') { - // pass depth through: skipObject consumes the single decrement for this level - return skipObject(p, end, depth, scan); - } - if (c == '[') { - return skipArray(p, end, depth, scan); - } - // a primitive is a leaf, so it never wastes a nesting level - return skipPrimitive(p, end); -} - -/** - * @brief Iterates the members of the object starting at @p p. - * - * For each member invokes @p memberCallback with the key and the byte range of - * its value. Advancing @p p is unaffected by the callback. - */ -template -bool forEachObjectMember(const char *&p, const char *end, int depth, F &&memberCallback, ScanProgress &scan) -{ - if (depth <= 0) { - return false; // nest deeper than the cap - } - ++p; // '{' - p = skipWhitespace(p, end); - if (p < end && *p == '}') { - ++p; - return true; - } - for (;;) { - p = skipWhitespace(p, end); - if (p >= end || *p != '"') { - return false; - } - QString key; - if (!decodeString(p, end, key)) { - return false; - } - p = skipWhitespace(p, end); - if (p >= end || *p != ':') { - return false; - } - ++p; - const char *valueStart = skipWhitespace(p, end); - const char *valueEnd = valueStart; - if (!skipValue(valueEnd, end, depth - 1, scan)) { - return false; - } - if (!memberCallback(key, valueStart, valueEnd)) { - return false; - } - p = valueEnd; - p = skipWhitespace(p, end); - if (p >= end) { - return false; - } - if (*p == ',') { - ++p; - continue; - } - if (*p == '}') { - ++p; - return true; - } - return false; - } -} - -// Counts the direct elements of an array value; returns -1 if the array is malformed. -int countArrayElements(const char *p, const char *end, int depth, ScanProgress &scan) -{ - if (depth <= 0) { - return -1; // nest deeper than the cap - } - ++p; // '[' - p = skipWhitespace(p, end); - int count = 0; - if (p < end && *p == ']') { - return 0; - } - for (;;) { - if (!skipValue(p, end, depth - 1, scan)) { - return -1; - } - scan.report(p); - ++count; - p = skipWhitespace(p, end); - if (p >= end) { - return -1; - } - if (*p == ',') { - ++p; - continue; - } - if (*p == ']') { - return count; - } - return -1; - } -} - -} // namespace - -namespace RawJson -{ - -QList scanSetRanges(const QByteArray &json, ScanError *error, const ScanProgressCallback &progress) -{ - QList ranges; - if (error) { - *error = ScanError{}; - } - - const auto fail = [&](const QString &message) -> QList { - if (error) { - error->message = message; - } - return {}; - }; - - const char *begin = json.constData(); - const char *end = begin + json.size(); - if (begin >= end) { - return fail(QStringLiteral("empty JSON document")); - } - - // Throttle reports to ~100 per scan so a GUI thread unthrottling them never - // drowns under per-card interrupts, whatever the document size. - ScanProgress scan; - scan.begin = begin; - scan.size = end - begin; - scan.step = std::max(1, scan.size / 100); - scan.callback = progress; - - const char *p = skipWhitespace(begin, end); - if (p >= end || *p != '{') { - return fail(QStringLiteral("top-level JSON must be an object")); - } - - bool foundData = false; - bool malformedSetData = false; - - const auto topLevelCallback = [&](const QString &key, const char *valueStart, const char *valueEnd) { - if (key == QStringLiteral("data")) { - foundData = true; - if (valueStart >= valueEnd || *valueStart != '{') { - malformedSetData = true; - return false; - } - const char *setP = valueStart; - const bool ok = forEachObjectMember( - setP, valueEnd, kMaxNestingDepth - 1, - [&](const QString &setCode, const char *setStart, const char *setEnd) { - if (setStart >= setEnd || *setStart != '{') { - malformedSetData = true; - return false; - } - SetRange range; - range.dataRange.start = setStart - begin; - range.dataRange.length = setEnd - setStart; - range.code = setCode; - - const char *memberP = setStart; - const bool metaOk = forEachObjectMember( - memberP, setEnd, kMaxNestingDepth - 2, - [&](const QString &field, const char *fs, const char *fe) { - if (field == QStringLiteral("code")) { - return decodeStringMember(fs, fe, range.code); - } - if (field == QStringLiteral("name")) { - return decodeStringMember(fs, fe, range.name); - } - if (field == QStringLiteral("type")) { - return decodeStringMember(fs, fe, range.type); - } - if (field == QStringLiteral("releaseDate")) { - return decodeStringMember(fs, fe, range.releaseDate); - } - if (field == QStringLiteral("cards")) { - if (fs >= fe) { - return false; - } - if (*fs != '[') { - // e.g. "cards": null — treat as an empty array, - // matching Qt's tolerance. - return true; - } - range.dataRange.cardCount = countArrayElements(fs, fe, kMaxNestingDepth - 2, scan); - return range.dataRange.cardCount >= 0; - } - return true; - }, - scan); - if (!metaOk) { - malformedSetData = true; - return false; - } - ranges.append(range); - return true; - }, - scan); - if (!ok) { - malformedSetData = true; - return false; - } - } - return true; - }; - - if (!forEachObjectMember(p, end, kMaxNestingDepth, topLevelCallback, scan)) { - return fail(malformedSetData ? QStringLiteral("malformed set data") : QStringLiteral("malformed JSON")); - } - p = skipWhitespace(p, end); - if (p != end) { - return fail(QStringLiteral("trailing content after top-level JSON object")); - } - if (!foundData) { - return fail(QStringLiteral("missing \"data\" object")); - } - if (ranges.isEmpty()) { - return fail(QStringLiteral("no sets found in \"data\"")); - } - scan.report(end); - return ranges; -} - -} // namespace RawJson \ No newline at end of file diff --git a/oracle/src/raw_json_scanner.h b/oracle/src/raw_json_scanner.h deleted file mode 100644 index 086f2b3e3..000000000 --- a/oracle/src/raw_json_scanner.h +++ /dev/null @@ -1,89 +0,0 @@ -#ifndef RAW_JSON_SCANNER_H -#define RAW_JSON_SCANNER_H - -#include -#include -#include -#include - -namespace RawJson -{ - -/** - * @brief The byte extent of a set's object inside the scanned document, plus - * the size of its cards array. This is the slice SetToDownload needs for lazy - * per-set parsing; the metadata strings live in SetRange alongside it. - */ -struct SetDataRange -{ - /** @brief Byte offset of the set's object within the scanned buffer. */ - qsizetype start = -1; - /** @brief Byte length of the set's object, including the surrounding braces. */ - qsizetype length = 0; - /** @brief Number of entries in the set's "cards" array. */ - int cardCount = 0; -}; - -struct SetRange -{ - /** @brief The byte slice of this set within the document. */ - SetDataRange dataRange; - QString code; - QString name; - QString type; - QString releaseDate; -}; - -struct ScanError -{ - bool isError() const - { - return !message.isEmpty(); - } - QString message; -}; - -/** - * @brief Optional progress callback receiving @c (bytesRead, totalBytes) while - * the document is walked. Invoked from the scanning thread; the caller decides - * how the throttled offsets are relayed to a GUI event loop. - */ -using ScanProgressCallback = std::function; - -/** - * @brief Scans a full MTGJSON document without materializing the JSON tree. - * - * Splits the top-level "data" object into per-set byte ranges and reads each - * set's metadata directly from the raw bytes. The oracle importer can then - * parse one set at a time during import, keeping peak memory far below a single - * QJsonDocument::fromJson() over the whole file. - * - * The whole document is structurally validated while scanning (strings, - * escapes, braces, and a trailing-content check) and nesting depth is capped at - * 1024 to match QJsonDocument, so pathologically deep documents fail shallowly - * instead of exhausting the stack. Verdicts agree with QJsonDocument::fromJson - * on structurally malformed input; unlike Qt, string metadata fields - * ("name", "type", "releaseDate", "code") tolerate null / non-string values by - * defaulting to empty rather than rejecting the whole document, so one broken - * set cannot abort the import of the rest. - * - * Following QJsonDocument::fromJson's convention, the parsed ranges are - * returned by value and any failure is reported through the @p error out - * parameter. - * - * @param json The raw MTGJSON document bytes. - * @param error Out parameter. Set to an error ScanError when the document - * cannot be parsed, otherwise left empty. Passing a null - * pointer disables error reporting. - * @param progress Optional progress callback. When non-empty it is invoked as - * the scanner advances through the document, throttled to a - * tiny fraction of the total size. - * @return The detected per-set ranges, or an empty list on failure. - */ -QList scanSetRanges(const QByteArray &json, - ScanError *error = nullptr, - const ScanProgressCallback &progress = ScanProgressCallback()); - -} // namespace RawJson - -#endif // RAW_JSON_SCANNER_H \ No newline at end of file diff --git a/servatrice/CMakeLists.txt b/servatrice/CMakeLists.txt index 68e422d8c..0c99a1512 100644 --- a/servatrice/CMakeLists.txt +++ b/servatrice/CMakeLists.txt @@ -97,8 +97,6 @@ set(DESKTOPDIR # Build servatrice binary and link it add_executable(servatrice MACOSX_BUNDLE ${servatrice_MOC_SRCS} ${servatrice_RESOURCES_RCC} ${servatrice_SOURCES}) -target_precompile_headers(servatrice PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h") - if(CMAKE_HOST_SYSTEM MATCHES "FreeBSD") target_link_libraries( servatrice libcockatrice_deck_list libcockatrice_network_server_remote Threads::Threads ${SERVATRICE_QT_MODULES} diff --git a/servatrice/src/main.cpp b/servatrice/src/main.cpp index 13bf95a82..9e7fe38d9 100644 --- a/servatrice/src/main.cpp +++ b/servatrice/src/main.cpp @@ -33,7 +33,6 @@ #include #include #include -#include #include RNG_Abstract *rng; @@ -170,7 +169,7 @@ int main(int argc, char *argv[]) signalhandler = new SignalHandler(); - rng = new RNG_SFMT(CryptoUtil::randomUInt64()); + rng = new RNG_SFMT; std::cerr << "Servatrice " << VERSION_STRING << " starting." << std::endl; std::cerr << "-------------------------" << std::endl; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4f5dc88eb..0530715bd 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -126,7 +126,6 @@ target_link_libraries(metrics_registry_test ${TEST_QT_MODULES} Threads::Threads add_subdirectory(card_zone_algorithms) add_subdirectory(carddatabase) -add_subdirectory(deck_list_model) add_subdirectory(deck_list_zones) add_subdirectory(loading_from_clipboard) add_subdirectory(movecard_tests) diff --git a/tests/card_zone_algorithms/card_zone_algorithms_test.cpp b/tests/card_zone_algorithms/card_zone_algorithms_test.cpp index 8fab42566..cc098cae9 100644 --- a/tests/card_zone_algorithms/card_zone_algorithms_test.cpp +++ b/tests/card_zone_algorithms/card_zone_algorithms_test.cpp @@ -134,35 +134,6 @@ TEST_F(AddCardAlgorithmTest, MidListInsertionPreservesOrder) EXPECT_EQ(knownList.at(2), &b); } -// Reconnecting to a game rebuilds zones from a ServerInfo_Zone. Non-coordinate zones -// (hand, piles, stack) report x == 0 on every card, so inserting each rebuilt card at -// that index would reverse the received server order. Appending (-1) keeps it. -TEST_F(AddCardAlgorithmTest, RebuildInsertAtZeroReversesServerOrder) -{ - MockCard a, b, c; - CardZoneAlgorithms::addCardToList(knownList, &a, 0, false); - CardZoneAlgorithms::addCardToList(knownList, &b, 0, false); - CardZoneAlgorithms::addCardToList(knownList, &c, 0, false); - - EXPECT_EQ(knownList.size(), 3); - EXPECT_EQ(knownList.at(0), &c); - EXPECT_EQ(knownList.at(1), &b); - EXPECT_EQ(knownList.at(2), &a); -} - -TEST_F(AddCardAlgorithmTest, RebuildAppendPreservesServerOrder) -{ - MockCard a, b, c; - CardZoneAlgorithms::addCardToList(knownList, &a, -1, false); - CardZoneAlgorithms::addCardToList(knownList, &b, -1, false); - CardZoneAlgorithms::addCardToList(knownList, &c, -1, false); - - EXPECT_EQ(knownList.size(), 3); - EXPECT_EQ(knownList.at(0), &a); - EXPECT_EQ(knownList.at(1), &b); - EXPECT_EQ(knownList.at(2), &c); -} - TEST_F(AddCardAlgorithmTest, KeepAnnotationsFalsePassedThrough) { MockCard card; diff --git a/tests/deck_list_model/CMakeLists.txt b/tests/deck_list_model/CMakeLists.txt deleted file mode 100644 index e3096c559..000000000 --- a/tests/deck_list_model/CMakeLists.txt +++ /dev/null @@ -1,33 +0,0 @@ -add_executable(deck_list_model_custom_zones_test ${VERSION_STRING_CPP} deck_list_model_custom_zones_test.cpp) - -if(NOT GTEST_FOUND) - add_dependencies(deck_list_model_custom_zones_test gtest) -endif() - -target_link_libraries( - deck_list_model_custom_zones_test - libcockatrice_models - libcockatrice_card - libcockatrice_deck_list - Threads::Threads - ${GTEST_BOTH_LIBRARIES} - ${TEST_QT_MODULES} -) -add_test(NAME deck_list_model_custom_zones_test COMMAND deck_list_model_custom_zones_test) - -add_executable(deck_list_model_zone_integration_test ${VERSION_STRING_CPP} deck_list_model_zone_integration_test.cpp) - -if(NOT GTEST_FOUND) - add_dependencies(deck_list_model_zone_integration_test gtest) -endif() - -target_link_libraries( - deck_list_model_zone_integration_test - libcockatrice_models - libcockatrice_card - libcockatrice_deck_list - Threads::Threads - ${GTEST_BOTH_LIBRARIES} - ${TEST_QT_MODULES} -) -add_test(NAME deck_list_model_zone_integration_test COMMAND deck_list_model_zone_integration_test) diff --git a/tests/deck_list_model/deck_list_model_custom_zones_test.cpp b/tests/deck_list_model/deck_list_model_custom_zones_test.cpp deleted file mode 100644 index 7f4ab81e1..000000000 --- a/tests/deck_list_model/deck_list_model_custom_zones_test.cpp +++ /dev/null @@ -1,276 +0,0 @@ -/** - * @file deck_list_model_custom_zones_test.cpp - * @brief Tests for the deck list model's custom-zone shadow-tree helpers. - * - * DeckListModelCustomZones centralizes every "what is / where is a custom zone" - * decision for the model's shadow tree: type testing, mirroring from the deck - * tree, name lookup, and the sort-with-custom-zones-last ordering. These tests - * exercise that logic directly on hand-built shadow trees, independent of the - * full model and card database machinery. - */ - -#include -#include -#include -#include - -namespace -{ - -DecklistModelCardNode *cardNode(InnerDecklistNode *parent, const QString &name, int number) -{ - // The underlying data node is detached; only the model wrapper is attached to the shadow tree. - auto *data = new DecklistCardNode(name, number, nullptr); - return new DecklistModelCardNode(data, parent); -} - -QStringList childNames(const InnerDecklistNode *node) -{ - QStringList names; - for (int i = 0; i < node->size(); ++i) { - names.append(node->at(i)->getName()); - } - return names; -} - -} // namespace - -// ===================================================================================================================== -// isCustomZone -// ===================================================================================================================== - -TEST(DeckListModelCustomZones, IsCustomZoneDistinguishesZoneFromGroup) -{ - InnerDecklistNode root; - auto *board = new InnerDecklistNode(DECK_ZONE_MAIN, &root); - auto *group = new InnerDecklistNode("Creature", board); - auto *zone = new DecklistModelSubZoneNode("Removal", board); - - auto *card = cardNode(group, "A", 1); - - EXPECT_FALSE(DeckListModelCustomZones::isCustomZone(board)); - EXPECT_FALSE(DeckListModelCustomZones::isCustomZone(group)); - EXPECT_FALSE(DeckListModelCustomZones::isCustomZone(card)); - EXPECT_TRUE(DeckListModelCustomZones::isCustomZone(zone)); -} - -// ===================================================================================================================== -// findSubZoneByName -// ===================================================================================================================== - -TEST(DeckListModelCustomZones, FindSubZoneByNameFindsAcrossBoards) -{ - InnerDecklistNode root; - auto *main = new InnerDecklistNode(DECK_ZONE_MAIN, &root); - auto *side = new InnerDecklistNode(DECK_ZONE_SIDE, &root); - new DecklistModelSubZoneNode("Removal", main); - new DecklistModelSubZoneNode("Utility", side); - new InnerDecklistNode("Plain", main); // not a custom zone - - auto *removal = DeckListModelCustomZones::findSubZoneByName(&root, "Removal"); - ASSERT_NE(removal, nullptr); - EXPECT_EQ(removal->getName(), QString("Removal")); - - auto *utility = DeckListModelCustomZones::findSubZoneByName(&root, "Utility"); - ASSERT_NE(utility, nullptr); - EXPECT_EQ(utility->getName(), QString("Utility")); - - // Names are deck-unique; a plain group or built-in board is not matched. - EXPECT_EQ(DeckListModelCustomZones::findSubZoneByName(&root, "Plain"), nullptr); - EXPECT_EQ(DeckListModelCustomZones::findSubZoneByName(&root, DECK_ZONE_MAIN), nullptr); - EXPECT_EQ(DeckListModelCustomZones::findSubZoneByName(&root, "Missing"), nullptr); -} - -// ===================================================================================================================== -// mirrorCustomZones -// ===================================================================================================================== - -TEST(DeckListModelCustomZones, MirrorCustomZonesCopiesCardsFlat) -{ - // Deck-tree board zone: one direct card plus one nested custom zone. - auto *deckBoard = new InnerDecklistNode(DECK_ZONE_MAIN); - new DecklistCardNode("Direct", 2, deckBoard); - - auto *deckZone = new InnerDecklistNode("Removal", deckBoard); - auto *deckCard1 = new DecklistCardNode("Bolt", 3, deckZone); - auto *deckCard2 = new DecklistCardNode("Swords", 1, deckZone); - - InnerDecklistNode shadowRoot; - auto *shadowBoard = new InnerDecklistNode(DECK_ZONE_MAIN, &shadowRoot); - - DeckListModelCustomZones::mirrorCustomZones(deckBoard, shadowBoard); - - // Only the custom zone is mirrored as a sub-zone; the direct card is not. - ASSERT_EQ(shadowBoard->size(), 1); - auto *shadowZone = dynamic_cast(shadowBoard->at(0)); - ASSERT_NE(shadowZone, nullptr); - EXPECT_EQ(shadowZone->getName(), QString("Removal")); - - // Cards live flat (un-grouped) inside the mirrored zone, wrapping the same data nodes. - ASSERT_EQ(shadowZone->size(), 2); - auto *shadowCard1 = dynamic_cast(shadowZone->at(0)); - auto *shadowCard2 = dynamic_cast(shadowZone->at(1)); - ASSERT_NE(shadowCard1, nullptr); - ASSERT_NE(shadowCard2, nullptr); - EXPECT_EQ(shadowCard1->getDataNode(), deckCard1); - EXPECT_EQ(shadowCard2->getDataNode(), deckCard2); -} - -TEST(DeckListModelCustomZones, MirrorCustomZonesWithNoCustomZonesIsNoop) -{ - // A board zone with only direct cards has nothing to mirror. - auto *deckBoard = new InnerDecklistNode(DECK_ZONE_MAIN); - new DecklistCardNode("Direct", 2, deckBoard); - - InnerDecklistNode shadowRoot; - auto *shadowBoard = new InnerDecklistNode(DECK_ZONE_MAIN, &shadowRoot); - - DeckListModelCustomZones::mirrorCustomZones(deckBoard, shadowBoard); - EXPECT_EQ(shadowBoard->size(), 0); -} - -TEST(DeckListModelCustomZones, MirrorCustomZonesFlattensNestedSubzones) -{ - // Cards deeper than one level under a custom zone still get a model row. - auto *deckBoard = new InnerDecklistNode(DECK_ZONE_MAIN); - auto *deckZone = new InnerDecklistNode("Removal", deckBoard); - auto *deckCard1 = new DecklistCardNode("Bolt", 1, deckZone); - auto *deeper = new InnerDecklistNode("Deeper", deckZone); - auto *deckCard2 = new DecklistCardNode("Swords", 1, deeper); - - InnerDecklistNode shadowRoot; - auto *shadowBoard = new InnerDecklistNode(DECK_ZONE_MAIN, &shadowRoot); - - DeckListModelCustomZones::mirrorCustomZones(deckBoard, shadowBoard); - - ASSERT_EQ(shadowBoard->size(), 1); - auto *shadowZone = dynamic_cast(shadowBoard->at(0)); - ASSERT_NE(shadowZone, nullptr); - EXPECT_EQ(shadowZone->getName(), QString("Removal")); - - // Both cards are flattened into the mirrored zone, preserving order. - ASSERT_EQ(shadowZone->size(), 2); - auto *shadowCard1 = dynamic_cast(shadowZone->at(0)); - auto *shadowCard2 = dynamic_cast(shadowZone->at(1)); - ASSERT_NE(shadowCard1, nullptr); - ASSERT_NE(shadowCard2, nullptr); - EXPECT_EQ(shadowCard1->getDataNode(), deckCard1); - EXPECT_EQ(shadowCard2->getDataNode(), deckCard2); -} - -// ===================================================================================================================== -// findGroupChild -// ===================================================================================================================== - -TEST(DeckListModelCustomZones, FindGroupChildSkipsCustomZones) -{ - InnerDecklistNode root; - auto *board = new InnerDecklistNode(DECK_ZONE_MAIN, &root); - auto *group = new InnerDecklistNode("Creature", board); - new DecklistModelSubZoneNode("Creature", board); - - EXPECT_EQ(DeckListModelCustomZones::findGroupChild(board, "Creature"), group); - EXPECT_EQ(DeckListModelCustomZones::findGroupChild(board, "Missing"), nullptr); - EXPECT_EQ(DeckListModelCustomZones::findGroupChild(&root, DECK_ZONE_MAIN), board); -} - -// ===================================================================================================================== -// sortWithCustomZonesLast -// ===================================================================================================================== - -TEST(DeckListModelCustomZones, SortBoardKeepsCustomZonesAfterGroupsAscending) -{ - InnerDecklistNode root; - auto *board = new InnerDecklistNode(DECK_ZONE_MAIN, &root); - new DecklistModelSubZoneNode("Zebra", board); - new InnerDecklistNode("Creature", board); - new InnerDecklistNode("Instant", board); - new DecklistModelSubZoneNode("Alpha", board); - - root.setSortMethod(DeckSortMethod::ByName); - - auto mapping = DeckListModelCustomZones::sortWithCustomZonesLast(&root, board, Qt::AscendingOrder); - - // Groups sort first (by name), then custom zones (by name), always after groups. - EXPECT_EQ(childNames(board), (QStringList{"Creature", "Instant", "Alpha", "Zebra"})); - - // Some non-identity movement occurred. - EXPECT_FALSE(mapping.isEmpty()); -} - -TEST(DeckListModelCustomZones, SortBoardKeepsCustomZonesAfterGroupsDescending) -{ - InnerDecklistNode root; - auto *board = new InnerDecklistNode(DECK_ZONE_MAIN, &root); - new DecklistModelSubZoneNode("Zebra", board); - new InnerDecklistNode("Creature", board); - new InnerDecklistNode("Instant", board); - new DecklistModelSubZoneNode("Alpha", board); - - root.setSortMethod(DeckSortMethod::ByName); - - (void)DeckListModelCustomZones::sortWithCustomZonesLast(&root, board, Qt::DescendingOrder); - - // Groups still lead (descending), custom zones still last. - EXPECT_EQ(childNames(board), (QStringList{"Instant", "Creature", "Zebra", "Alpha"})); -} - -TEST(DeckListModelCustomZones, SortBoardMappingIsConsistent) -{ - InnerDecklistNode root; - auto *board = new InnerDecklistNode(DECK_ZONE_MAIN, &root); - - QList originalOrder; - auto *g0 = new InnerDecklistNode("Creature", board); - originalOrder.append(g0); - auto *z0 = new DecklistModelSubZoneNode("Zebra", board); - originalOrder.append(z0); - auto *g1 = new InnerDecklistNode("Instant", board); - originalOrder.append(g1); - auto *z1 = new DecklistModelSubZoneNode("Alpha", board); - originalOrder.append(z1); - - root.setSortMethod(DeckSortMethod::ByName); - - auto mapping = DeckListModelCustomZones::sortWithCustomZonesLast(&root, board, Qt::AscendingOrder); - - // The mapping reports, for each final row, the original row of the node now sitting there. - ASSERT_EQ(mapping.size(), board->size()); - for (const auto &move : mapping) { - const int preSortRow = move.first; - const int finalRow = move.second; - ASSERT_GE(preSortRow, 0); - ASSERT_LT(preSortRow, originalOrder.size()); - EXPECT_EQ(board->at(finalRow), originalOrder[preSortRow]) << "row " << finalRow; - } - - // Final order sanity: groups first in name order, then custom zones. - EXPECT_EQ(childNames(board), (QStringList{"Creature", "Instant", "Alpha", "Zebra"})); -} - -TEST(DeckListModelCustomZones, SortPlainNodeDoesNotReorderCustomZones) -{ - // A non-board node (e.g. a group whose children are cards) is sorted plainly; - // custom zones are not a special case there. Cards sort by name. - InnerDecklistNode root; - auto *board = new InnerDecklistNode(DECK_ZONE_MAIN, &root); - auto *group = new InnerDecklistNode("Creature", board); - cardNode(group, "Swords", 1); - cardNode(group, "Bolt", 3); - - root.setSortMethod(DeckSortMethod::ByName); - - auto mapping = DeckListModelCustomZones::sortWithCustomZonesLast(&root, group, Qt::AscendingOrder); - EXPECT_EQ(childNames(group), (QStringList{"Bolt", "Swords"})); - ASSERT_EQ(mapping.size(), 2); - EXPECT_EQ(mapping[0].first, 1); // "Bolt" was originally at row 1 - EXPECT_EQ(mapping[0].second, 0); - EXPECT_EQ(mapping[1].first, 0); - EXPECT_EQ(mapping[1].second, 1); -} - -int main(int argc, char **argv) -{ - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/tests/deck_list_model/deck_list_model_zone_integration_test.cpp b/tests/deck_list_model/deck_list_model_zone_integration_test.cpp deleted file mode 100644 index a4562c95d..000000000 --- a/tests/deck_list_model/deck_list_model_zone_integration_test.cpp +++ /dev/null @@ -1,283 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ - -int totalCustomZoneRows(const DeckListModel &model) -{ - int count = 0; - const int rootRows = model.rowCount(QModelIndex()); - for (int r = 0; r < rootRows; ++r) { - const QModelIndex board = model.index(r, 0, QModelIndex()); - const int childRows = model.rowCount(board); - for (int c = 0; c < childRows; ++c) { - const QModelIndex child = model.index(c, 0, board); - if (child.data(DeckRoles::IsCustomZoneRole).toBool()) { - ++count; - } - } - } - return count; -} - -QModelIndex findBoardIndex(const DeckListModel &model, const QString &boardName) -{ - for (int r = 0; r < model.rowCount(QModelIndex()); ++r) { - const QModelIndex idx = model.index(r, 0, QModelIndex()); - if (idx.data(DeckRoles::IsCardRole).toBool()) { - continue; - } - const QString name = idx.sibling(idx.row(), DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); - if (name == boardName) { - return idx; - } - } - return {}; -} - -QModelIndex findZoneRow(const DeckListModel &model, const QModelIndex &board) -{ - for (int r = 0; r < model.rowCount(board); ++r) { - const QModelIndex child = model.index(r, 0, board); - if (child.data(DeckRoles::IsCustomZoneRole).toBool()) { - return child; - } - } - return {}; -} - -} // namespace - -// The "Add to Zone" combobox/submenu lists getCustomZoneNames(), which reads the -// deck tree. These verify the source data a freshly-created zone populates. - -TEST(DeckListModelZoneIntegration, CreateZoneThenReadCustomZoneNames) -{ - QSharedPointer deck(new DeckList()); - DeckListModel model(nullptr, deck); - auto *tree = deck->getTree(); - - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr); - EXPECT_EQ(model.getCustomZoneNames(DECK_ZONE_MAIN), (QStringList{"Removal"})); -} - -TEST(DeckListModelZoneIntegration, CreateTwoZonesThenReadBoth) -{ - QSharedPointer deck(new DeckList()); - DeckListModel model(nullptr, deck); - auto *tree = deck->getTree(); - - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr); - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Utility"), nullptr); - EXPECT_EQ(model.getCustomZoneNames(DECK_ZONE_MAIN), (QStringList{"Removal", "Utility"})); -} - -// Mirroring regression: rebuildTree must mirror each custom zone exactly once. -TEST(DeckListModelZoneIntegration, RebuildTreeMirrorsEachZoneOnce) -{ - QSharedPointer deck(new DeckList()); - DeckListModel model(nullptr, deck); - auto *tree = deck->getTree(); - - // One direct mainboard card plus two nested custom zones. - tree->addCard("Lightning Bolt", 2, DECK_ZONE_MAIN, -1); - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr); - tree->addCard("Swords to Plowshares", 1, "Removal", -1); - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Utility"), nullptr); - - model.rebuildTree(); - - EXPECT_EQ(model.getCustomZoneNames(DECK_ZONE_MAIN), (QStringList{"Removal", "Utility"})); - EXPECT_EQ(totalCustomZoneRows(model), 2); -} - -// ===================================================================================================================== -// Model behaviour: addCard routing, findCard lookup, removeRows guard, empty-zone survival. -// ===================================================================================================================== - -TEST(DeckListModelZoneIntegration, AddCardRoutesIntoMirroredCustomZone) -{ - QSharedPointer deck(new DeckList()); - DeckListModel model(nullptr, deck); - auto *tree = deck->getTree(); - - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr); - model.rebuildTree(); - - QModelIndex added = model.addCard(ExactCard(CardInfo::newInstance("Lightning Bolt")), "Removal"); - ASSERT_TRUE(added.isValid()); - - // The card is a direct child of the mirrored custom zone, not a new top-level zone. - const QModelIndex zoneParent = added.parent(); - ASSERT_TRUE(zoneParent.isValid()); - EXPECT_TRUE(zoneParent.data(DeckRoles::IsCustomZoneRole).toBool()); - EXPECT_EQ(zoneParent.sibling(zoneParent.row(), DeckListModelColumns::CARD_NAME).data(Qt::DisplayRole).toString(), - QString("Removal")); - - // No "Removal" top-level zone appeared in the deck tree. - auto *listRoot = tree->getRoot(); - bool topLevelRemoval = false; - for (int i = 0; i < listRoot->size(); ++i) { - if (auto *zone = dynamic_cast(listRoot->at(i))) { - topLevelRemoval |= zone->getName() == "Removal"; - } - } - EXPECT_FALSE(topLevelRemoval); -} - -TEST(DeckListModelZoneIntegration, AddCardToUnmirroredCustomZoneRebuildsNotCreatesTopLevel) -{ - QSharedPointer deck(new DeckList()); - DeckListModel model(nullptr, deck); - auto *tree = deck->getTree(); - - // The zone exists on the deck tree but the shadow tree has never mirrored it. - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr); - - QModelIndex added = model.addCard(ExactCard(CardInfo::newInstance("Lightning Bolt")), "Removal"); - ASSERT_TRUE(added.isValid()); - - const QModelIndex zoneParent = added.parent(); - ASSERT_TRUE(zoneParent.isValid()); - EXPECT_TRUE(zoneParent.data(DeckRoles::IsCustomZoneRole).toBool()); - EXPECT_EQ(zoneParent.sibling(zoneParent.row(), DeckListModelColumns::CARD_NAME).data(Qt::DisplayRole).toString(), - QString("Removal")); -} - -TEST(DeckListModelZoneIntegration, AddCardCreatesGroupSeparatelyFromSameNamedZone) -{ - QSharedPointer deck(new DeckList()); - DeckListModel model(nullptr, deck); - auto *tree = deck->getTree(); - - // A custom zone named exactly like a grouping criterion. - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Creature"), nullptr); - model.rebuildTree(); - - CardInfoPtr bear = CardInfo::newInstance("Grizzly Bears"); - bear->setProperty(Mtg::MainCardType, "Creature"); - - QModelIndex added = model.addCard(ExactCard(bear), DECK_ZONE_MAIN); - ASSERT_TRUE(added.isValid()); - - // The card lands in a *group* node called "Creature", not swallowed by the custom zone. - const QModelIndex groupParent = added.parent(); - ASSERT_TRUE(groupParent.isValid()); - EXPECT_FALSE(groupParent.data(DeckRoles::IsCustomZoneRole).toBool()); - EXPECT_EQ(groupParent.sibling(groupParent.row(), DeckListModelColumns::CARD_NAME).data(Qt::DisplayRole).toString(), - QString("Creature")); - - // The board keeps both rows: the "Creature" group and the "Creature" custom zone. - const QModelIndex boardIndex = groupParent.parent(); - ASSERT_TRUE(boardIndex.isValid()); - EXPECT_EQ(model.rowCount(boardIndex), 2); -} - -TEST(DeckListModelZoneIntegration, FindCardResolvesCardInsideCustomZone) -{ - QSharedPointer deck(new DeckList()); - DeckListModel model(nullptr, deck); - auto *tree = deck->getTree(); - - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr); - model.rebuildTree(); - - // findCard resolves through the card database; register the card we add. - const QString cardName = "Swords to Plowshares"; - CardInfoPtr info = CardInfo::newInstance(cardName); - CardDatabaseManager::getInstance()->addCard(info); - - QModelIndex added = model.addCard(ExactCard(info), "Removal"); - ASSERT_TRUE(added.isValid()); - - QModelIndex found = model.findCard(cardName, "Removal"); - EXPECT_TRUE(found.isValid()); - EXPECT_EQ(found, added); -} - -TEST(DeckListModelZoneIntegration, RemoveRowsRefusesCustomZoneRow) -{ - QSharedPointer deck(new DeckList()); - DeckListModel model(nullptr, deck); - auto *tree = deck->getTree(); - - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr); - tree->addCard("Lightning Bolt", 2, DECK_ZONE_MAIN, -1); - model.rebuildTree(); - - const QModelIndex mainIndex = findBoardIndex(model, DECK_ZONE_MAIN); - ASSERT_TRUE(mainIndex.isValid()); - const QModelIndex zoneRow = findZoneRow(model, mainIndex); - ASSERT_TRUE(zoneRow.isValid()); - - EXPECT_FALSE(model.removeRow(zoneRow.row(), zoneRow.parent())); - EXPECT_EQ(model.rowCount(mainIndex), 2); // the zone survives, alongside the card group -} - -TEST(DeckListModelZoneIntegration, EmptyCustomZoneSurvivesMirrorAndPruning) -{ - QSharedPointer deck(new DeckList()); - DeckListModel model(nullptr, deck); - auto *tree = deck->getTree(); - - // An empty custom zone must be mirrored (the stack deliberately keeps it alive). - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr); - model.rebuildTree(); - - const QModelIndex mainIndex = findBoardIndex(model, DECK_ZONE_MAIN); - ASSERT_TRUE(mainIndex.isValid()); - EXPECT_EQ(model.rowCount(mainIndex), 1); - EXPECT_TRUE(findZoneRow(model, mainIndex).isValid()); -} - -// Regression: a board card named like the requested zone must not be mistaken for -// a zone. Previously `findChild` matched any child by name, so a mainboard card -// called "Lightning Bolt" made addCard believe a "Lightning Bolt" zone existed and -// recurse through rebuildTree forever. -TEST(DeckListModelZoneIntegration, AddCardToCardNamedZoneDoesNotRecurse) -{ - QSharedPointer deck(new DeckList()); - DeckListModel model(nullptr, deck); - auto *tree = deck->getTree(); - - tree->addCard("Lightning Bolt", 2, DECK_ZONE_MAIN, -1); - - QModelIndex added = model.addCard(ExactCard(CardInfo::newInstance("Swords to Plowshares")), "Lightning Bolt"); - ASSERT_TRUE(added.isValid()); -} - -// Regression: adding to a custom zone that holds a nested sub-zone mirrored the -// nested cards as flattened shadow rows, so the sorted shadow row index pointed -// past the deck zone's direct children. The card must be appended to the deck -// zone instead of being written out of range. -TEST(DeckListModelZoneIntegration, AddCardToCustomZoneWithNestedSubZoneAppends) -{ - QSharedPointer deck(new DeckList()); - DeckListModel model(nullptr, deck); - auto *tree = deck->getTree(); - - auto *removal = tree->addCustomZone(DECK_ZONE_MAIN, "Removal"); - ASSERT_NE(removal, nullptr); - auto *deeper = new InnerDecklistNode("Deeper", removal); - new DecklistCardNode("Lightning Bolt", 2, deeper, -1); - model.rebuildTree(); - - QModelIndex added = model.addCard(ExactCard(CardInfo::newInstance("Swords to Plowshares")), "Removal"); - ASSERT_TRUE(added.isValid()); - ASSERT_TRUE(added.parent().data(DeckRoles::IsCustomZoneRole).toBool()); -} - -int main(int argc, char **argv) -{ - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/tests/deck_list_zones/deck_list_zones_test.cpp b/tests/deck_list_zones/deck_list_zones_test.cpp index 801f226a9..a5148621d 100644 --- a/tests/deck_list_zones/deck_list_zones_test.cpp +++ b/tests/deck_list_zones/deck_list_zones_test.cpp @@ -213,42 +213,6 @@ TEST(DeckListZones, MoveCustomZoneMovesCards) EXPECT_FALSE(tree->moveCustomZone("Removal", "not_a_board")); } -TEST(DeckListZones, MoveCustomZoneFailsForUnknownBoard) -{ - DeckList deck; - auto *tree = deck.getTree(); - - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr); - tree->addCard("Lightning Bolt", 2, "Removal", -1); - - EXPECT_FALSE(tree->moveCustomZone("Removal", "not_a_board")); - - // The zone is still under main. - EXPECT_EQ(tree->getCustomZones(DECK_ZONE_MAIN).size(), 1); -} - -// Regression: findCustomZoneByName walks every top-level zone, so a custom zone -// an imported deck carries under a non-standard board (tokens) is still found and -// movable. The pre-fix manager-level moveCustomZone only scanned the standard -// boards and returned false for these with no feedback. -TEST(DeckListZones, MoveCustomZoneNestedUnderTokensBoard) -{ - DeckList deck; - auto *tree = deck.getTree(); - auto *root = tree->getRoot(); - - auto *tokens = new InnerDecklistNode(DECK_ZONE_TOKENS, root); - auto *removal = new InnerDecklistNode("Removal", tokens); - new DecklistCardNode("Lightning Bolt", 2, removal, -1); - - EXPECT_TRUE(tree->findCustomZoneByName("Removal")); - EXPECT_TRUE(tree->moveCustomZone("Removal", DECK_ZONE_SIDE)); - - auto pairs = collectBoardCardPairs(deck); - EXPECT_FALSE(hasPair(pairs, DECK_ZONE_TOKENS, "Lightning Bolt")); - EXPECT_TRUE(hasPair(pairs, DECK_ZONE_SIDE, "Lightning Bolt")); -} - TEST(DeckListZones, RemoveCustomZoneRemovesCards) { DeckList deck; diff --git a/tests/oracle/CMakeLists.txt b/tests/oracle/CMakeLists.txt index 9bc5ee5be..c5c1e9097 100644 --- a/tests/oracle/CMakeLists.txt +++ b/tests/oracle/CMakeLists.txt @@ -7,63 +7,3 @@ endif() target_link_libraries(parse_cipt_test Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}) add_test(NAME parse_cipt_test COMMAND parse_cipt_test) - -# Oracle importer unit tests -add_executable( - oracle_importer_test ${VERSION_STRING_CPP} ../../oracle/src/oracleimporter.cpp ../../oracle/src/parsehelpers.cpp - ../../oracle/src/raw_json_scanner.cpp oracle_importer_test.cpp -) - -if(NOT GTEST_FOUND) - add_dependencies(oracle_importer_test gtest) -endif() - -target_link_libraries( - oracle_importer_test libcockatrice_card libcockatrice_interfaces Threads::Threads ${GTEST_BOTH_LIBRARIES} - ${TEST_QT_MODULES} -) - -add_test(NAME oracle_importer_test COMMAND oracle_importer_test) - -# Oracle importer benchmark tests (manual, not run in CI, incl. RAM benchmark) -# Optional compression libs, mirrored from oracle/CMakeLists.txt, so the benchmark -# can download and decompress whatever AllPrintings format the default URL selects. -find_package(ZLIB) -if(ZLIB_FOUND) - add_definitions("-DHAS_ZLIB") - set(_ORACLE_BENCH_EXTRA_SOURCES ../../oracle/src/zip/unzip.cpp ../../oracle/src/zip/zipglobal.cpp) - set(_ORACLE_BENCH_EXTRA_LIBRARIES ${ZLIB_LIBRARIES}) - include_directories(${ZLIB_INCLUDE_DIRS}) -else() - message(STATUS "Oracle tests: zlib not found; zip download benchmark disabled") -endif() - -find_package(LibLZMA) -if(LIBLZMA_FOUND) - add_definitions("-DHAS_LZMA") - list(APPEND _ORACLE_BENCH_EXTRA_SOURCES ../../oracle/src/lzma/decompress.cpp) - list(APPEND _ORACLE_BENCH_EXTRA_LIBRARIES ${LIBLZMA_LIBRARIES}) - include_directories(${LIBLZMA_INCLUDE_DIRS}) -else() - message(STATUS "Oracle tests: LibLZMA not found; xz download benchmark disabled") -endif() - -add_executable( - oracle_importer_benchmark_test - ${VERSION_STRING_CPP} ../../oracle/src/oracleimporter.cpp ../../oracle/src/parsehelpers.cpp - ../../oracle/src/raw_json_scanner.cpp oracle_importer_benchmark_test.cpp ${_ORACLE_BENCH_EXTRA_SOURCES} -) - -if(NOT GTEST_FOUND) - add_dependencies(oracle_importer_benchmark_test gtest) -endif() - -target_link_libraries( - oracle_importer_benchmark_test - libcockatrice_card - libcockatrice_interfaces - Threads::Threads - ${GTEST_BOTH_LIBRARIES} - ${TEST_QT_MODULES} - ${_ORACLE_BENCH_EXTRA_LIBRARIES} -) diff --git a/tests/oracle/oracle_importer_benchmark_test.cpp b/tests/oracle/oracle_importer_benchmark_test.cpp deleted file mode 100644 index 633c9e40a..000000000 --- a/tests/oracle/oracle_importer_benchmark_test.cpp +++ /dev/null @@ -1,578 +0,0 @@ -#include "../../oracle/src/oracleimporter.h" - -#include "gtest/gtest.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#if defined(HAS_LZMA) -#include "../../oracle/src/lzma/decompress.h" -#endif -#if defined(HAS_ZLIB) -#include "../../oracle/src/zip/unzip.h" -#endif -#if defined(Q_OS_MACOS) -#include -#include -#endif - -// Helper: build a synthetic MTGJSON-style JSON with the given number of sets and cards per set -static QByteArray buildSyntheticData(int numSets, int cardsPerSet) -{ - QJsonObject dataObj; - for (int s = 0; s < numSets; ++s) { - QJsonArray cardsArray; - for (int c = 0; c < cardsPerSet; ++c) { - QJsonObject card; - card["name"] = QString("Card %1").arg(s * cardsPerSet + c); - card["text"] = "This is a test card with some rules text."; - card["layout"] = "normal"; - card["manaCost"] = "{W}"; - card["type"] = "Creature — Human"; - card["power"] = "2"; - card["toughness"] = "2"; - card["colors"] = QJsonArray{"W"}; - card["colorIdentity"] = QJsonArray{"W"}; - card["types"] = QJsonArray{"Creature"}; - // Real MTGJSON types: floats and booleans, not strings. This - // exercises the QVariant coercion in the property reader. - card["convertedManaCost"] = 1.0; - card["manaValue"] = 1.0; - card["isOnlineOnly"] = false; - card["isRebalanced"] = false; - - QJsonObject legalities; - legalities["standard"] = "legal"; - legalities["modern"] = "legal"; - legalities["legacy"] = "legal"; - legalities["vintage"] = "legal"; - legalities["commander"] = "legal"; - card["legalities"] = legalities; - - QJsonObject identifiers; - identifiers["scryfallId"] = QString("id-%1-%2").arg(s).arg(c); - card["identifiers"] = identifiers; - - // In AllPrintings, number and rarity are flat fields on the card - // object, exactly as set below. - card["number"] = QString::number(c + 1); - card["rarity"] = "common"; - - cardsArray.append(card); - } - - QJsonObject setObj; - setObj["code"] = QString("T%1").arg(s, 2, 10, QChar('0')); - setObj["name"] = QString("Test Set %1").arg(s); - setObj["type"] = "expansion"; - setObj["releaseDate"] = "2024-01-01"; - setObj["cards"] = cardsArray; - - dataObj[QString("T%1").arg(s, 2, 10, QChar('0'))] = setObj; - } - - QJsonObject root; - root["data"] = dataObj; - return QJsonDocument(root).toJson(QJsonDocument::Compact); -} - -// ============================================================================ -// Import throughput benchmark -// ============================================================================ - -TEST(OracleBenchmark, ImportThroughput) -{ - static constexpr int numSets = 10; - static constexpr int cardsPerSet = 500; - - QByteArray data = buildSyntheticData(numSets, cardsPerSet); - - OracleImporter importer; - - // Phase 1: Parse JSON - QElapsedTimer timer; - timer.start(); - bool ok = importer.readSetsFromByteArray(data); - ASSERT_TRUE(ok); - qint64 parseMs = timer.elapsed(); - - // Phase 2: Import cards - timer.restart(); - int importedSets = importer.startImport(); - qint64 importMs = timer.elapsed(); - - int totalImported = 0; - for (const auto &card : importer.getCardList()) { - Q_UNUSED(card); - totalImported++; - } - - // The fixture generates globally unique card names, so the expected - // counts are exact: a regression here means cards were dropped. - ASSERT_EQ(importedSets, numSets); - ASSERT_EQ(totalImported, numSets * cardsPerSet); - // Real-data probe: numeric convertedManaCost must be coerced to text - // (regression for the QJsonValue::toString() reader in #7214). - auto probeCard = importer.getCardList().value("Card 0"); - ASSERT_FALSE(probeCard.isNull()); - ASSERT_EQ(probeCard->getProperty("cmc"), "1"); - - qDebug().noquote() - << QString("Oracle Import Benchmark: %1 sets, %2 unique cards").arg(importedSets).arg(totalImported); - qDebug().noquote() << QString(" JSON parse: %1 ms").arg(parseMs); - qDebug().noquote() << QString(" Card import: %1 ms").arg(importMs); - qDebug().noquote() << QString(" Total: %1 ms").arg(parseMs + importMs); - if (importMs > 0) { - qDebug().noquote() << QString(" Throughput: %1 cards/sec") - .arg(static_cast(totalImported) / importMs * 1000.0, 0, 'f', 0); - } -} - -// ============================================================================ -// readSetsFromByteArray benchmark -// ============================================================================ - -TEST(OracleBenchmark, ParseJsonThroughput) -{ - static constexpr int numSets = 20; - static constexpr int cardsPerSet = 1000; - - QByteArray data = buildSyntheticData(numSets, cardsPerSet); - - // Run 5 iterations and report average - static constexpr int iterations = 5; - qint64 totalMs = 0; - - for (int i = 0; i < iterations; ++i) { - OracleImporter importer; - QByteArray source = data; - QElapsedTimer timer; - timer.start(); - bool ok = importer.readSetsFromByteArray(std::move(source)); - ASSERT_TRUE(ok); - totalMs += timer.elapsed(); - } - - qint64 avgMs = totalMs / iterations; - qDebug().noquote() << QString("Parse Benchmark (%1 iterations): avg %2 ms for %3 sets x %4 cards") - .arg(iterations) - .arg(avgMs) - .arg(numSets) - .arg(cardsPerSet); -} - -// ============================================================================ -// Split card merging benchmark -// ============================================================================ - -TEST(OracleBenchmark, SplitCardMerging) -{ - static constexpr int numSplitCards = 1000; - - QJsonArray cardsList; - for (int i = 0; i < numSplitCards; ++i) { - QJsonObject face1; - face1["name"] = QString("Fire %1 // Ice %1").arg(i); - face1["text"] = "Fire side text."; - face1["layout"] = "split"; - face1["side"] = "a"; - face1["faceName"] = QString("Fire %1").arg(i); - face1["colors"] = QJsonArray{"R"}; - face1["colorIdentity"] = QJsonArray{"R"}; - face1["types"] = QJsonArray{"Instant"}; - face1["manaCost"] = "{R}"; - face1["legalities"] = QJsonObject{{"standard", "not_legal"}}; - face1["identifiers"] = QJsonObject{{"scryfallId", QString("f-%1").arg(i)}}; - face1["number"] = QString::number(i + 1); - face1["rarity"] = "uncommon"; - - QJsonObject face2; - face2["name"] = QString("Fire %1 // Ice %1").arg(i); - face2["text"] = "Ice side text."; - face2["layout"] = "split"; - face2["side"] = "b"; - face2["faceName"] = QString("Ice %1").arg(i); - face2["colors"] = QJsonArray{"U"}; - face2["colorIdentity"] = QJsonArray{"U"}; - face2["types"] = QJsonArray{"Instant"}; - face2["manaCost"] = "{U}"; - face2["legalities"] = QJsonObject{{"standard", "not_legal"}}; - face2["identifiers"] = QJsonObject{{"scryfallId", QString("i-%1").arg(i)}}; - face2["number"] = QString::number(i + 1); - face2["rarity"] = "uncommon"; - - cardsList.append(face1); - cardsList.append(face2); - } - - NoopCardSetPriorityController controller; - OracleImporter importer; - CardSetPtr set = CardSet::newInstance(&controller, "TST", "Split Test"); - - QElapsedTimer timer; - timer.start(); - int count = importer.importCardsFromSet(set, cardsList); - qint64 ms = timer.elapsed(); - - ASSERT_EQ(count, numSplitCards); - qDebug().noquote() << QString("Split Card Merge Benchmark: %1 cards in %2 ms (%3 cards/sec)") - .arg(count) - .arg(ms) - .arg(ms > 0 ? static_cast(count) / ms * 1000.0 : 0.0, 0, 'f', 0); -} - -// ============================================================================ -// sortAndReduceColors microbenchmark -// ============================================================================ - -// We can't call sortAndReduceColors directly (it's static), so we benchmark -// through importCardsFromSet with color properties. - -TEST(OracleBenchmark, ImportCardsWithColors) -{ - static constexpr int numCards = 10000; - - NoopCardSetPriorityController controller; - OracleImporter importer; - CardSetPtr set = CardSet::newInstance(&controller, "TST", "Color Test"); - - QJsonArray cardsList; - for (int i = 0; i < numCards; ++i) { - QJsonObject card; - card["name"] = QString("Color Card %1").arg(i); - card["text"] = "Rules text."; - card["layout"] = "normal"; - card["manaCost"] = "{W}"; - card["type"] = "Creature — Human"; - card["types"] = QJsonArray{"Creature"}; - card["colors"] = QJsonArray{"B", "R", "G", "W", "U"}; - card["colorIdentity"] = QJsonArray{"B", "R", "G", "W", "U"}; - card["number"] = QString::number(i + 1); - card["rarity"] = "common"; - card["legalities"] = QJsonObject{{"standard", "legal"}}; - card["identifiers"] = QJsonObject{{"scryfallId", QString("c-%1").arg(i)}}; - cardsList.append(card); - } - - QElapsedTimer timer; - timer.start(); - int count = importer.importCardsFromSet(set, cardsList); - qint64 ms = timer.elapsed(); - - ASSERT_EQ(count, numCards); - qDebug().noquote() << QString("Import with Colors Benchmark: %1 cards in %2 ms (%3 cards/sec)") - .arg(count) - .arg(ms) - .arg(ms > 0 ? static_cast(count) / ms * 1000.0 : 0.0, 0, 'f', 0); -} - -// ============================================================================ -// RAM usage measurement -// ============================================================================ - -// Mirrors the default AllPrintings URL selection in oracle/src/pages.cpp. -#if defined(HAS_LZMA) -static const QUrl kDefaultAllPrintingsUrl("https://www.mtgjson.com/api/v5/AllPrintings.json.xz"); -#elif defined(HAS_ZLIB) -static const QUrl kDefaultAllPrintingsUrl("https://www.mtgjson.com/api/v5/AllPrintings.json.zip"); -#else -static const QUrl kDefaultAllPrintingsUrl("https://www.mtgjson.com/api/v5/AllPrintings.json"); -#endif - -// Magic bytes also from oracle/src/pages.cpp -static const QByteArray kXzSignature("\xFD\x37\x7A\x58\x5A", 6); -static const QByteArray kZipSignature("PK"); - -struct MemorySnapshot -{ - qint64 peakRssKb = -1; // process high-water mark (VmHWM on Linux, ru_maxrss on macOS) - qint64 rssKb = -1; // current resident set size - bool available = false; - - static MemorySnapshot current() - { - MemorySnapshot snap; -#if defined(Q_OS_LINUX) - QFile statusFile("/proc/self/status"); - if (statusFile.open(QIODevice::ReadOnly | QIODevice::Text)) { - // /proc files report size() == 0, so atEnd() is immediately true: read everything first. - const QList lines = statusFile.readAll().split('\n'); - for (const QByteArray &line : lines) { - if (line.startsWith("VmHWM:")) { - snap.peakRssKb = line.mid(6).trimmed().split(' ').value(0).toLongLong(); - } else if (line.startsWith("VmRSS:")) { - snap.rssKb = line.mid(6).trimmed().split(' ').value(0).toLongLong(); - } - } - snap.available = snap.peakRssKb >= 0; - } -#elif defined(Q_OS_MACOS) - struct rusage usage; - if (getrusage(RUSAGE_SELF, &usage) == 0) { - snap.peakRssKb = usage.ru_maxrss / 1024; // bytes -> kB - snap.available = snap.peakRssKb >= 0; - } - // getrusage has no current-RSS equivalent; task_info's resident_size - // is the closest macOS analog to Linux VmRSS. - mach_task_basic_info info = {}; - mach_msg_type_number_t count = MACH_TASK_BASIC_INFO_COUNT; - if (task_info(mach_task_self(), MACH_TASK_BASIC_INFO, reinterpret_cast(&info), &count) == - KERN_SUCCESS) { - snap.rssKb = info.resident_size / 1024; - } -#endif - return snap; - } -}; - -static QString formatKb(qint64 kb) -{ - if (kb < 0) { - return "N/A"; - } - return QString("%1 MB").arg(kb / 1024.0, 0, 'f', 1); -} - -static void logRamPhase(const QString &phase, const MemorySnapshot &baseline, const MemorySnapshot ¤t) -{ - if (!baseline.available || !current.available) { - qDebug().noquote() << QString(" %1: memory stats unavailable on this platform").arg(phase); - return; - } - // VmHWM / ru_maxrss are monotonically non-decreasing high-water marks, so a - // peak-based delta between phases is ~0.0 MB by construction once the - // fixture build has set the process peak. The live signals are current RSS - // and the process peak; the delta is meaningful only where the baseline was - // taken immediately before the phase it measures (e.g. the import phase, - // which compares afterParse against afterImport). - QString rssDelta = "N/A"; - if (current.rssKb >= 0 && baseline.rssKb >= 0) { - rssDelta = formatKb(current.rssKb - baseline.rssKb); - } - qDebug().noquote() << QString(" %1: current RSS %2 | delta vs baseline %3 | process peak %4") - .arg(phase) - .arg(formatKb(current.rssKb)) - .arg(rssDelta) - .arg(formatKb(current.peakRssKb)); -} - -// Decompresses the download payload when the default URL is a compressed build, -// mirroring the wizard's magic-byte handling in oracle/src/pages.cpp. -static QByteArray decompressSetsData(const QByteArray &payload) -{ - if (payload.startsWith(kXzSignature)) { -#if defined(HAS_LZMA) - QBuffer inBuffer(const_cast(&payload)); - QByteArray out; - QBuffer outBuffer(&out); - inBuffer.open(QIODevice::ReadOnly); - outBuffer.open(QIODevice::WriteOnly); - XzDecompressor xz; - if (!xz.decompress(&inBuffer, &outBuffer)) { - qDebug() << "RAM benchmark: xz decompression failed"; - return {}; - } - return out; -#else - qDebug() << "RAM benchmark: download is xz-compressed but this build has no LZMA support"; - return {}; -#endif - } - if (payload.startsWith(kZipSignature)) { -#if defined(HAS_ZLIB) - QBuffer inBuffer(const_cast(&payload)); - inBuffer.open(QIODevice::ReadOnly); - UnZip unzip; - if (unzip.openArchive(&inBuffer) != UnZip::Ok) { - qDebug() << "RAM benchmark: zip archive open failed"; - return {}; - } - if (unzip.fileList().size() != 1) { - qDebug() << "RAM benchmark: zip archive doesn't contain exactly one file"; - return {}; - } - QByteArray out; - QBuffer outBuffer(&out); - outBuffer.open(QIODevice::WriteOnly); - const auto errorCode = unzip.extractFile(unzip.fileList().value(0), &outBuffer); - unzip.closeArchive(); - if (errorCode != UnZip::Ok) { - qDebug() << "RAM benchmark: zip extraction failed"; - return {}; - } - return out; -#else - qDebug() << "RAM benchmark: download is zip-compressed but this build has no zlib support"; - return {}; -#endif - } - return payload; -} - -TEST(OracleBenchmark, ImportRamUsage) -{ - static constexpr int numSets = 30; - static constexpr int cardsPerSet = 2000; // ~60k cards, roughly AllPrintings scale - - // Baseline must precede the fixture build: a high-water mark set while - // generating the synthetic JSON would otherwise mask the importer phases. - // Where memory stats are unavailable (Windows), skip before doing the - // 60k-card fixture build, which would otherwise be pure wasted work. - const MemorySnapshot baseline = MemorySnapshot::current(); - if (!baseline.available) { - GTEST_SKIP() << "Memory stats unavailable on this platform"; - } - - const QByteArray data = buildSyntheticData(numSets, cardsPerSet); - - // The fixture build leaves freed-but-unreturned arenas behind (current RSS - // rarely falls once glibc allocates). Baseline immediately after it so the - // parse phase measures only the importer's own growth (~40 MB) rather than - // swallowing the fixture builder's spike. - const MemorySnapshot afterFixture = MemorySnapshot::current(); - logRamPhase("fixture build", baseline, afterFixture); - - NoopCardSetPriorityController controller; - OracleImporter importer; - - QElapsedTimer timer; - timer.start(); - ASSERT_TRUE(importer.readSetsFromByteArray(std::move(data))); - const qint64 parseMs = timer.elapsed(); - const MemorySnapshot afterParse = MemorySnapshot::current(); - - timer.restart(); - const int importedSets = importer.startImport(); - const qint64 importMs = timer.elapsed(); - const MemorySnapshot afterImport = MemorySnapshot::current(); - - importer.releaseSetData(); - const MemorySnapshot afterRelease = MemorySnapshot::current(); - - const int totalCards = importer.getCardList().size(); - qDebug().noquote() << QString("Oracle RAM Benchmark (synthetic): %1 sets, %2 cards, %3 MB JSON") - .arg(importedSets) - .arg(totalCards) - .arg(data.size() / (1024.0 * 1024.0), 0, 'f', 1); - qDebug().noquote() << QString(" JSON parse: %1 ms").arg(parseMs); - qDebug().noquote() << QString(" Card import: %1 ms").arg(importMs); - logRamPhase("parse", afterFixture, afterParse); - logRamPhase("import", afterParse, afterImport); - logRamPhase("after releaseSetData()", afterImport, afterRelease); - - // Freeing the parsed tree rarely moves current RSS (allocator reuse), so the - // meaningful signal that release actually dropped the buffers is emptiness, - // not an RSS delta. - ASSERT_TRUE(importer.getSets().isEmpty()); -} - -TEST(OracleBenchmark, ImportRamUsageAllPrintings) -{ - // Only "1" enables the download: unset (the default and the CI setup) and - // an explicit "0" both disable it. - bool envOk = false; - const int enabled = qEnvironmentVariableIntValue("COCKATRICE_ORACLE_RAM_BENCHMARK", &envOk); - if (!envOk || enabled == 0) { - GTEST_SKIP() << "Set COCKATRICE_ORACLE_RAM_BENCHMARK=1 to download the real AllPrintings dataset for this " - "RAM benchmark. Default URL: " - << kDefaultAllPrintingsUrl.toDisplayString().toStdString(); - } - - // Baseline must precede the request so the phase covers the download + - // decompress step, including the payload materialized by readAll(). - const MemorySnapshot baseline = MemorySnapshot::current(); - if (!baseline.available) { - GTEST_SKIP() << "Memory stats unavailable on this platform"; - } - - QNetworkAccessManager nam; - QNetworkRequest request(kDefaultAllPrintingsUrl); - request.setHeader(QNetworkRequest::UserAgentHeader, "Cockatrice Oracle RAM benchmark"); - QNetworkReply *reply = nam.get(request); - - QEventLoop loop; - QTimer timeoutTimer; - timeoutTimer.setSingleShot(true); - bool timedOut = false; - QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); - QObject::connect(&timeoutTimer, &QTimer::timeout, &loop, [&] { - timedOut = true; - reply->abort(); - }); - timeoutTimer.start(10 * 60 * 1000); - loop.exec(); - timeoutTimer.stop(); - - // abort() leaves reply->error() as OperationCanceledError, so a timed-out - // download takes the same GTEST_SKIP path as any other network error - // instead of reading a truncated body and failing the parse below. - if (timedOut || reply->error() != QNetworkReply::NoError) { - GTEST_SKIP() << "Download failed: " << reply->errorString().toStdString(); - } - const QByteArray payload = reply->readAll(); - reply->deleteLater(); - - // mtgjson can answer 200 with an HTML page (mirrors the wizard's '<' check - // in pages.cpp); reject it before trying to decompress/parse. - if (payload.startsWith("<")) { - GTEST_SKIP() << "Download returned a non-JSON body (HTML page instead of data), skipping"; - } - - const QByteArray setsData = decompressSetsData(payload); - const MemorySnapshot afterDownload = MemorySnapshot::current(); - if (setsData.isEmpty()) { - GTEST_SKIP() << "No data to import (download or decompression failed)"; - } - - NoopCardSetPriorityController controller; - OracleImporter importer; - - QElapsedTimer timer; - timer.start(); - ASSERT_TRUE(importer.readSetsFromByteArray(std::move(setsData))); - const qint64 parseMs = timer.elapsed(); - const MemorySnapshot afterParse = MemorySnapshot::current(); - - timer.restart(); - const int importedSets = importer.startImport(); - const qint64 importMs = timer.elapsed(); - const MemorySnapshot afterImport = MemorySnapshot::current(); - - importer.releaseSetData(); - const MemorySnapshot afterRelease = MemorySnapshot::current(); - - const int totalCards = importer.getCardList().size(); - qDebug().noquote() << QString("Oracle RAM Benchmark (real AllPrintings): %1 sets, %2 unique cards") - .arg(importedSets) - .arg(totalCards); - qDebug().noquote() << QString(" URL: %1").arg(kDefaultAllPrintingsUrl.toDisplayString()); - qDebug().noquote() << QString(" Downloaded: %1 MB, decompressed: %2 MB") - .arg(payload.size() / (1024.0 * 1024.0), 0, 'f', 1) - .arg(setsData.size() / (1024.0 * 1024.0), 0, 'f', 1); - qDebug().noquote() << QString(" JSON parse: %1 ms").arg(parseMs); - qDebug().noquote() << QString(" Card import: %1 ms").arg(importMs); - logRamPhase("download+decompress", baseline, afterDownload); - logRamPhase("parse", afterDownload, afterParse); - logRamPhase("import", afterParse, afterImport); - logRamPhase("after releaseSetData()", afterImport, afterRelease); -} - -int main(int argc, char **argv) -{ - // Required for the event loop used by the real-AllPrintings download benchmark - QCoreApplication app(argc, argv); - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/tests/oracle/oracle_importer_test.cpp b/tests/oracle/oracle_importer_test.cpp deleted file mode 100644 index 66c7bdbe1..000000000 --- a/tests/oracle/oracle_importer_test.cpp +++ /dev/null @@ -1,879 +0,0 @@ -#include "../../oracle/src/oracleimporter.h" - -#include "gtest/gtest.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class OracleImporterTest : public ::testing::Test -{ -protected: - void SetUp() override - { - controller = new NoopCardSetPriorityController(); - importer = new OracleImporter(); - set = CardSet::newInstance(controller, "TST", "Test Set"); - } - - void TearDown() override - { - delete importer; - delete controller; - } - - // Helper: build a minimal card JSON object - QJsonObject makeCard(const QString &name, - const QString &colors = "", - const QString &colorIdentity = "", - const QVariantMap &legalities = {}) - { - QJsonObject card; - card["name"] = name; - card["text"] = "Rules text."; - card["layout"] = "normal"; - card["manaCost"] = "{W}"; - card["type"] = "Creature — Human"; - card["types"] = QJsonArray{"Creature"}; - card["number"] = "1"; - card["rarity"] = "common"; - - if (!colors.isEmpty()) { - QJsonArray arr; - for (const QChar &c : colors) { - arr.append(QString(c)); - } - card["colors"] = arr; - } - if (!colorIdentity.isEmpty()) { - QJsonArray arr; - for (const QChar &c : colorIdentity) { - arr.append(QString(c)); - } - card["colorIdentity"] = arr; - } - if (!legalities.isEmpty()) { - QJsonObject legalObj; - for (auto it = legalities.constBegin(); it != legalities.constEnd(); ++it) { - legalObj[it.key()] = it.value().toString(); - } - card["legalities"] = legalObj; - } - - QJsonObject identifiers; - identifiers["scryfallId"] = QUuid::createUuid().toString(QUuid::WithoutBraces); - card["identifiers"] = identifiers; - - return card; - } - - NoopCardSetPriorityController *controller; - OracleImporter *importer; - CardSetPtr set; -}; - -// ============================================================================ -// sortAndReduceColors tests (tested via importCardsFromSet) -// ============================================================================ - -TEST_F(OracleImporterTest, SortAndReduceColorsSingleColor) -{ - QJsonArray cards{makeCard("Red Card", "R", "R")}; - importer->importCardsFromSet(set, cards); - - auto card = importer->getCardList().value("Red Card"); - ASSERT_FALSE(card.isNull()); - ASSERT_EQ(card->getProperty("colors"), "R"); -} - -TEST_F(OracleImporterTest, SortAndReduceColorsDeduplicates) -{ - QJsonArray cards{makeCard("Dedup Card", "WWUUB", "WU")}; - importer->importCardsFromSet(set, cards); - - auto card = importer->getCardList().value("Dedup Card"); - ASSERT_FALSE(card.isNull()); - ASSERT_EQ(card->getProperty("colors"), "WUB"); -} - -TEST_F(OracleImporterTest, SortAndReduceColorsSortsWUBRG) -{ - QJsonArray cards{makeCard("Sort Card", "RGW", "RGW")}; - importer->importCardsFromSet(set, cards); - - auto card = importer->getCardList().value("Sort Card"); - ASSERT_FALSE(card.isNull()); - ASSERT_EQ(card->getProperty("colors"), "WRG"); -} - -TEST_F(OracleImporterTest, SortAndReduceColorsAllFive) -{ - QJsonArray cards{makeCard("Five Color", "BRGWU", "BRGWU")}; - importer->importCardsFromSet(set, cards); - - auto card = importer->getCardList().value("Five Color"); - ASSERT_FALSE(card.isNull()); - ASSERT_EQ(card->getProperty("colors"), "WUBRG"); -} - -TEST_F(OracleImporterTest, SortAndReduceColorIdentity) -{ - QJsonArray cards{makeCard("Color Id Card", "W", "GWR")}; - importer->importCardsFromSet(set, cards); - - auto card = importer->getCardList().value("Color Id Card"); - ASSERT_FALSE(card.isNull()); - ASSERT_EQ(card->getProperty("coloridentity"), "WRG"); -} - -TEST_F(OracleImporterTest, SingleColorNotSorted) -{ - QJsonArray cards{makeCard("Single Card", "B", "B")}; - importer->importCardsFromSet(set, cards); - - auto card = importer->getCardList().value("Single Card"); - ASSERT_FALSE(card.isNull()); - ASSERT_EQ(card->getProperty("colors"), "B"); -} - -// ============================================================================ -// Legality guard tests -// ============================================================================ - -TEST_F(OracleImporterTest, NewCardKeepsLegalityProperties) -{ - // Verifies that format-* properties survive addCard on a fresh card - // (not the combineLegalities guard, which only runs on existing printings). - QVariantMap leg; - leg["standard"] = "legal"; - leg["modern"] = "legal"; - QJsonArray cards{makeCard("Legal Card", "", "", leg)}; - - importer->importCardsFromSet(set, cards); - auto card = importer->getCardList().value("Legal Card"); - ASSERT_FALSE(card.isNull()); - ASSERT_EQ(card->getProperty("format-standard"), "legal"); - ASSERT_EQ(card->getProperty("format-modern"), "legal"); -} - -TEST_F(OracleImporterTest, LegalityMergeAllowedWhenCardHasNoLegalities) -{ - // First printing carries no legalities at all, so the guard's - // `properties.filter(formatRegex).empty()` predicate is true and the - // second printing's legalities must be merged in. - QJsonArray cards1{makeCard("Unmerged Card")}; - importer->importCardsFromSet(set, cards1); - - CardSetPtr set2 = CardSet::newInstance(controller, "TS2", "Second Set"); - QVariantMap leg; - leg["standard"] = "legal"; - QJsonArray cards2{makeCard("Unmerged Card", "", "", leg)}; - importer->importCardsFromSet(set2, cards2); - - auto card = importer->getCardList().value("Unmerged Card"); - ASSERT_FALSE(card.isNull()); - ASSERT_EQ(card->getProperty("format-standard"), "legal"); -} - -TEST_F(OracleImporterTest, LegalityGuardPreservesFirstPrinting) -{ - // First printing: standard=legal, modern=legal - QVariantMap leg1; - leg1["standard"] = "legal"; - leg1["modern"] = "legal"; - QJsonArray cards1{makeCard("Guarded Card", "", "", leg1)}; - importer->importCardsFromSet(set, cards1); - - // Second printing: standard=banned, modern=not_legal - CardSetPtr set2 = CardSet::newInstance(controller, "TS2", "Second Set"); - QVariantMap leg2; - leg2["standard"] = "banned"; - leg2["modern"] = "not_legal"; - QJsonArray cards2{makeCard("Guarded Card", "", "", leg2)}; - importer->importCardsFromSet(set2, cards2); - - auto card = importer->getCardList().value("Guarded Card"); - ASSERT_FALSE(card.isNull()); - // Guard should preserve first printing's legalities - ASSERT_EQ(card->getProperty("format-standard"), "legal"); - ASSERT_EQ(card->getProperty("format-modern"), "legal"); -} - -// ============================================================================ -// createDefaultMagicFormats tests -// ============================================================================ - -TEST_F(OracleImporterTest, CreateDefaultMagicFormatsContainsExpectedFormats) -{ - auto formats = importer->createDefaultMagicFormats(); - ASSERT_TRUE(formats.contains("standard")); - ASSERT_TRUE(formats.contains("modern")); - ASSERT_TRUE(formats.contains("legacy")); - ASSERT_TRUE(formats.contains("vintage")); - ASSERT_TRUE(formats.contains("commander")); - ASSERT_TRUE(formats.contains("pauper")); - ASSERT_TRUE(formats.contains("pioneer")); - ASSERT_TRUE(formats.contains("brawl")); - ASSERT_TRUE(formats.contains("historic")); - ASSERT_TRUE(formats.contains("timeless")); - ASSERT_TRUE(formats.contains("duel")); - ASSERT_TRUE(formats.contains("oathbreaker")); -} - -TEST_F(OracleImporterTest, CreateDefaultMagicFormatsSingletonDeckSizes) -{ - auto formats = importer->createDefaultMagicFormats(); - auto commander = formats.value("commander"); - ASSERT_FALSE(commander.isNull()); - ASSERT_EQ(commander->minDeckSize, 100); - ASSERT_EQ(commander->maxDeckSize, 100); - ASSERT_EQ(commander->maxSideboardSize, 15); - - auto brawl = formats.value("brawl"); - ASSERT_FALSE(brawl.isNull()); - ASSERT_EQ(brawl->minDeckSize, 60); - ASSERT_EQ(brawl->maxDeckSize, 60); -} - -TEST_F(OracleImporterTest, CreateDefaultMagicFormatsVintageHasRestricted) -{ - auto formats = importer->createDefaultMagicFormats(); - auto vintage = formats.value("vintage"); - ASSERT_FALSE(vintage.isNull()); - bool hasRestricted = false; - for (const auto &ac : vintage->allowedCounts) { - if (ac.label == "restricted") { - hasRestricted = true; - ASSERT_EQ(ac.max, 1); - } - } - ASSERT_TRUE(hasRestricted); -} - -TEST_F(OracleImporterTest, CreateDefaultMagicFormatsRegexMatchesBasicLands) -{ - auto formats = importer->createDefaultMagicFormats(); - auto standard = formats.value("standard"); - ASSERT_FALSE(standard.isNull()); - ASSERT_FALSE(standard->exceptions.isEmpty()); - - auto &basicLandsException = standard->exceptions.first(); - ASSERT_FALSE(basicLandsException.conditions.isEmpty()); - - auto &condition = basicLandsException.conditions.first(); - ASSERT_EQ(condition.field, "type"); - ASSERT_EQ(condition.matchType, "regex"); - - // Verify the regex actually works (was broken before: \b = backspace, not word boundary) - QRegularExpression regex(condition.value); - ASSERT_TRUE(regex.isValid()); - ASSERT_TRUE(regex.match("Basic Land — Forest").hasMatch()); - ASSERT_TRUE(regex.match("Basic Snow Land — Mountain").hasMatch()); - ASSERT_FALSE(regex.match("Creature — Elf Warrior").hasMatch()); -} - -TEST_F(OracleImporterTest, CreateDefaultMagicFormatsCaching) -{ - // The memoized map returns the same FormatRulesPtr instances, so the - // shared pointers must be identical across calls. This is the only - // observable effect of the cache: contents would match either way. - auto first = importer->createDefaultMagicFormats(); - auto second = importer->createDefaultMagicFormats(); - ASSERT_EQ(first.value("standard").data(), second.value("standard").data()); -} - -// ============================================================================ -// readSetsFromByteArray tests -// ============================================================================ - -TEST_F(OracleImporterTest, ReadSetsFromByteArrayValidJson) -{ - QJsonObject setObj; - setObj["code"] = "tst"; - setObj["name"] = "Test Set"; - setObj["type"] = "expansion"; - setObj["releaseDate"] = "2024-01-01"; - setObj["cards"] = QJsonArray(); - - QJsonObject root; - root["data"] = QJsonObject{{"TST", setObj}}; - - QByteArray data = QJsonDocument(root).toJson(); - ASSERT_TRUE(importer->readSetsFromByteArray(data)); - ASSERT_EQ(importer->getSets().size(), 1); - ASSERT_EQ(importer->getSets().first().getShortName(), "TST"); -} - -TEST_F(OracleImporterTest, ReadSetsFromByteArrayInvalidJson) -{ - QByteArray data = "not valid json"; - ASSERT_FALSE(importer->readSetsFromByteArray(data)); -} - -TEST_F(OracleImporterTest, ReadSetsFromByteArrayEmptyData) -{ - QJsonObject root; - root["data"] = QJsonObject(); - - QByteArray data = QJsonDocument(root).toJson(); - ASSERT_FALSE(importer->readSetsFromByteArray(data)); -} - -TEST_F(OracleImporterTest, ReadSetsFromByteArrayCapitalizesSetType) -{ - QJsonObject setObj; - setObj["code"] = "ftv"; - setObj["name"] = "From The Vault"; - setObj["type"] = "from_the_vault"; - setObj["releaseDate"] = "2024-01-01"; - setObj["cards"] = QJsonArray(); - - QJsonObject root; - root["data"] = QJsonObject{{"FTV", setObj}}; - - QByteArray data = QJsonDocument(root).toJson(); - ASSERT_TRUE(importer->readSetsFromByteArray(data)); - ASSERT_EQ(importer->getSets().first().getSetType(), "From the Vault"); -} - -TEST_F(OracleImporterTest, ReadSetsFromByteArraySortsSetsByName) -{ - // QJsonObject iterates keys in lexicographic order ("AAA" before "ZZZ"), - // so leaving the natural order matching the alphabetical sort makes the - // assertion pass trivially. Inverting it keeps the sort meaningful: - // iteration yields "AAA" (Zeta Set) first, then the sort by name must - // promote "ZZZ" (Alpha Set) to the front. - QJsonObject setA; - setA["code"] = "aaa"; - setA["name"] = "Zeta Set"; - setA["type"] = "expansion"; - setA["releaseDate"] = "2024-01-01"; - setA["cards"] = QJsonArray(); - - QJsonObject setB; - setB["code"] = "zzz"; - setB["name"] = "Alpha Set"; - setB["type"] = "expansion"; - setB["releaseDate"] = "2024-01-01"; - setB["cards"] = QJsonArray(); - - QJsonObject root; - root["data"] = QJsonObject{{"AAA", setA}, {"ZZZ", setB}}; - - QByteArray data = QJsonDocument(root).toJson(); - ASSERT_TRUE(importer->readSetsFromByteArray(data)); - auto sets = importer->getSets(); - ASSERT_GE(sets.size(), 2); - ASSERT_EQ(sets.first().getShortName(), "ZZZ"); -} - -// ============================================================================ -// Split card coloridentity tests -// ============================================================================ - -TEST_F(OracleImporterTest, SplitCardColorIdentityConcatenated) -{ - QJsonObject leg{{"standard", "not_legal"}}; - - QJsonObject face1; - face1["name"] = "Fire // Ice"; - face1["text"] = "Fire deals 2 damage."; - face1["layout"] = "split"; - face1["side"] = "a"; - face1["faceName"] = "Fire"; - face1["colors"] = QJsonArray{"R"}; - face1["colorIdentity"] = QJsonArray{"R"}; - face1["types"] = QJsonArray{"Instant"}; - face1["manaCost"] = "{R}"; - face1["legalities"] = leg; - face1["identifiers"] = QJsonObject{{"scryfallId", "aaa"}}; - face1["number"] = "1"; - face1["rarity"] = "uncommon"; - - QJsonObject face2; - face2["name"] = "Fire // Ice"; - face2["text"] = "Ice taps target artifact."; - face2["layout"] = "split"; - face2["side"] = "b"; - face2["faceName"] = "Ice"; - face2["colors"] = QJsonArray{"U"}; - face2["colorIdentity"] = QJsonArray{"U"}; - face2["types"] = QJsonArray{"Instant"}; - face2["manaCost"] = "{U}"; - face2["legalities"] = leg; - face2["identifiers"] = QJsonObject{{"scryfallId", "bbb"}}; - face2["number"] = "1"; - face2["rarity"] = "uncommon"; - - QJsonArray cardsList{face1, face2}; - int count = importer->importCardsFromSet(set, cardsList); - ASSERT_EQ(count, 1); - - auto card = importer->getCardList().value("Fire // Ice"); - ASSERT_FALSE(card.isNull()); - - // coloridentity should be "RU" (concatenated), then sorted to "UR" - // by sortAndReduceColors when it reaches addCard - ASSERT_EQ(card->getProperty("coloridentity"), "UR"); -} - -TEST_F(OracleImporterTest, SplitCardColorsConcatenated) -{ - QJsonObject leg{{"standard", "not_legal"}}; - - QJsonObject face1; - face1["name"] = "Fire // Ice"; - face1["text"] = "Fire deals 2 damage."; - face1["layout"] = "split"; - face1["side"] = "a"; - face1["faceName"] = "Fire"; - face1["colors"] = QJsonArray{"R"}; - face1["colorIdentity"] = QJsonArray{"R"}; - face1["types"] = QJsonArray{"Instant"}; - face1["manaCost"] = "{R}"; - face1["legalities"] = leg; - face1["identifiers"] = QJsonObject{{"scryfallId", "aaa"}}; - face1["number"] = "1"; - face1["rarity"] = "uncommon"; - - QJsonObject face2; - face2["name"] = "Fire // Ice"; - face2["text"] = "Ice taps target artifact."; - face2["layout"] = "split"; - face2["side"] = "b"; - face2["faceName"] = "Ice"; - face2["colors"] = QJsonArray{"U"}; - face2["colorIdentity"] = QJsonArray{"U"}; - face2["types"] = QJsonArray{"Instant"}; - face2["manaCost"] = "{U}"; - face2["legalities"] = leg; - face2["identifiers"] = QJsonObject{{"scryfallId", "bbb"}}; - face2["number"] = "1"; - face2["rarity"] = "uncommon"; - - QJsonArray cardsList{face1, face2}; - importer->importCardsFromSet(set, cardsList); - - auto card = importer->getCardList().value("Fire // Ice"); - ASSERT_FALSE(card.isNull()); - - QString colors = card->getProperty("colors"); - ASSERT_FALSE(colors.contains("//")) << "colors should not contain '//', got: " << colors.toStdString(); - ASSERT_TRUE(colors.contains("R")); - ASSERT_TRUE(colors.contains("U")); -} - -// ============================================================================ -// Mana cost formatting tests -// ============================================================================ - -TEST_F(OracleImporterTest, ManaCostStripsBraces) -{ - QJsonObject card = makeCard("Mana Card"); - card["manaCost"] = "{2}{W}{B}"; - QJsonArray cards{card}; - - importer->importCardsFromSet(set, cards); - auto result = importer->getCardList().value("Mana Card"); - ASSERT_FALSE(result.isNull()); - ASSERT_EQ(result->getProperty("manacost"), "2WB"); -} - -// cmc comes through as a JSON number ("convertedManaCost"/"manaValue" are -// floats in AllPrintings), so this pins the number-to-text coercion that -// QJsonValue::toString() dropped in #7214. -TEST_F(OracleImporterTest, NumericManaValueCoercedToCmc) -{ - QJsonObject card = makeCard("Cmc Card"); - card["manaValue"] = 3; - QJsonArray cards{card}; - - importer->importCardsFromSet(set, cards); - auto result = importer->getCardList().value("Cmc Card"); - ASSERT_FALSE(result.isNull()); - ASSERT_EQ(result->getProperty("cmc"), "3"); -} - -TEST_F(OracleImporterTest, LegacyConvertedManaCostCoercedToCmc) -{ - QJsonObject card = makeCard("Legacy Cmc Card"); - card["convertedManaCost"] = 3.0; - QJsonArray cards{card}; - - importer->importCardsFromSet(set, cards); - auto result = importer->getCardList().value("Legacy Cmc Card"); - ASSERT_FALSE(result.isNull()); - ASSERT_EQ(result->getProperty("cmc"), "3"); -} - -// ============================================================================ -// Card deduplication tests -// ============================================================================ - -TEST_F(OracleImporterTest, DuplicateCardNameReturnsExisting) -{ - QJsonArray cards{makeCard("Dupe Card")}; - importer->importCardsFromSet(set, cards); - - CardSetPtr set2 = CardSet::newInstance(controller, "TS2", "Second Set"); - QJsonArray cards2{makeCard("Dupe Card")}; - importer->importCardsFromSet(set2, cards2); - - ASSERT_EQ(importer->getCardList().size(), 1); -} - -TEST_F(OracleImporterTest, AELigatureReplaced) -{ - QJsonObject card = makeCard(QString::fromUtf8("\xC3\x86ther Vial")); // Æther Vial - QJsonArray cards{card}; - - importer->importCardsFromSet(set, cards); - // Æ is replaced with AE, resulting in "AEther Vial" - ASSERT_FALSE(importer->getCardList().contains(QString::fromUtf8("\xC3\x86ther Vial"))); - ASSERT_TRUE(importer->getCardList().contains("AEther Vial")); -} - -TEST_F(OracleImporterTest, ApostropheNormalized) -{ - QJsonObject card = makeCard(QString::fromUtf8("Jace\u2019s Ingenuity")); - QJsonArray cards{card}; - - importer->importCardsFromSet(set, cards); - ASSERT_TRUE(importer->getCardList().contains("Jace's Ingenuity")); -} - -// ============================================================================ -// RawJson scanner tests -// ============================================================================ - -TEST_F(OracleImporterTest, ScanSetRangesMatchFullJsonParse) -{ - QJsonObject root; - QJsonObject data; - data["AAA"] = makeCard("Alpha Card"); - data["BBB"] = makeCard("Beta Card"); - root["data"] = data; - - const QByteArray bytes = QJsonDocument(root).toJson(QJsonDocument::Compact); - - RawJson::ScanError error; - const QList ranges = RawJson::scanSetRanges(bytes, &error); - ASSERT_FALSE(error.isError()) << error.message.toStdString(); - ASSERT_EQ(ranges.size(), 2); - - const QJsonObject wholeData = QJsonDocument::fromJson(bytes).object().value("data").toObject(); - for (const RawJson::SetRange &range : ranges) { - QJsonParseError parseError; - const QJsonDocument sliceDoc = QJsonDocument::fromJson( - QByteArray(bytes.constData() + range.dataRange.start, range.dataRange.length), &parseError); - ASSERT_EQ(parseError.error, QJsonParseError::NoError) - << range.code.toStdString() << ": " << parseError.errorString().toStdString(); - ASSERT_EQ(sliceDoc.object(), wholeData.value(range.code).toObject()) << "set " << range.code.toStdString(); - } -} - -TEST_F(OracleImporterTest, ScanSetRangesDecodesEscapesAndCountsCards) -{ - const QByteArray json = "{\"data\":{\"KEY\":{\"code\":\"zzz\",\"name\":\"\\u00c9tude \\ud83d\\ude00\"," - "\"type\":\"expansion\",\"releaseDate\":\"2024-01-05\"," - "\"cards\":[{\"name\":\"a\"},{\"name\":\"b\"},{\"name\":\"c\"}]}}}"; - - RawJson::ScanError error; - const QList ranges = RawJson::scanSetRanges(json, &error); - ASSERT_FALSE(error.isError()); - ASSERT_EQ(ranges.size(), 1); - - const RawJson::SetRange &range = ranges.first(); - ASSERT_EQ(range.code, "zzz"); // inner "code" wins over the object key - const QString expectedName = QString::fromUtf8("\xC3\x89tude ") + QChar(0xD83D) + QChar(0xDE00); - ASSERT_EQ(range.name, expectedName); - ASSERT_EQ(range.type, "expansion"); - ASSERT_EQ(range.releaseDate, "2024-01-05"); - ASSERT_EQ(range.dataRange.cardCount, 3); - - QJsonParseError parseError; - const QJsonDocument sliceDoc = QJsonDocument::fromJson( - QByteArray(json.constData() + range.dataRange.start, range.dataRange.length), &parseError); - ASSERT_EQ(parseError.error, QJsonParseError::NoError); - ASSERT_EQ(sliceDoc.object().value("name").toString(), expectedName); - ASSERT_EQ(sliceDoc.object().value("cards").toArray().size(), 3); -} - -TEST_F(OracleImporterTest, ScanSetRangesRejectsInvalidJson) -{ - const QList invalid = {"not json", - "[]", - "{\"data\":[]}", - "{\"data\":{}}", - "{\"other\":{}}", - "{\"data\":{\"A\":{\"code\":\"a\",\"name\":\"ok\",\"type\":\"x\"," - "\"releaseDate\":\"2024-01-01\",\"cards\":[]}}} trailing", - "{\"data\":{\"A\":{\"cards\":[{\"name\":\"\\uZZZZ\"}]}}}", - "{\"data\":{\"A\":{\"cards\":[{\"name\":\"bad \\q escape\"}]}}}", - "{\"data\":{\"A\":{\"cards\":[{\"name\":\"\\ud800\"}]}}}"}; - - for (const QByteArray &json : invalid) { - RawJson::ScanError error; - RawJson::scanSetRanges(json, &error); - EXPECT_TRUE(error.isError()) << "expected failure for: " << json.constData(); - } -} - -TEST_F(OracleImporterTest, ScanSetRangesMatchesFullJsonParseVerdicts) -{ - // Verdicts must agree with QJsonDocument::fromJson for the inputs below — - // including the metadata quirks ("name": null, "type": 7, "releaseDate": null, - // "cards": null) that used to make the scanner reject sets Qt accepts. - const QList inputs = { - "{\"data\":{\"A\":{\"code\":\"a\",\"name\":\"ok\",\"type\":\"x\",\"releaseDate\":\"2024-01-01\",\"cards\":[{" - "\"n\":1}]}}}", - "{\"data\":{\"A\":{\"code\":\"a\",\"name\":null,\"type\":\"x\",\"releaseDate\":\"2024-01-01\",\"cards\":[]}}}", - "{\"data\":{\"A\":{\"code\":\"a\",\"name\":\"ok\",\"type\":null,\"releaseDate\":\"2024-01-01\",\"cards\":[]}}}", - "{\"data\":{\"A\":{\"code\":\"a\",\"name\":\"ok\",\"type\":7,\"releaseDate\":\"2024-01-01\",\"cards\":null}}}", - "{\"data\":{\"A\":{\"code\":\"a\",\"name\":\"ok\",\"releaseDate\":\"2024-01-01\",\"cards\":[1,2,3]}}}", - // unescaped control character inside a string: QJsonDocument and - // skipString both accept it, so the scanner must not reject the whole doc - "{\"data\":{\"A\":{\"code\":\"a\",\"name\":\"N\tX\",\"releaseDate\":\"2024-01-01\",\"cards\":[{\"n\":1}]}}}", - // structurally invalid JSON (both parsers must reject) - "not json", - "{\"data\":{\"A\":{\"name\":\"unterminated}}", - }; - - for (const QByteArray &input : inputs) { - QJsonParseError qtError; - QJsonDocument::fromJson(input, &qtError); - const bool qtOk = qtError.error == QJsonParseError::NoError; - - RawJson::ScanError scanError; - const QList ranges = RawJson::scanSetRanges(input, &scanError); - EXPECT_EQ(qtOk, !scanError.isError()) << "verdict mismatch for: " << input.constData(); - if (scanError.isError()) { - continue; - } - for (const RawJson::SetRange &range : ranges) { - QJsonParseError sliceError; - QJsonDocument::fromJson(QByteArray(input.constData() + range.dataRange.start, range.dataRange.length), - &sliceError); - EXPECT_EQ(sliceError.error, QJsonParseError::NoError) << "bad range slice for: " << input.constData(); - } - } -} - -TEST_F(OracleImporterTest, ScanSetRangesRejectsDeepNesting) -{ - // Far beyond the shared 1024 container cap: Qt reports DeepNesting and the - // scanner must reject too, without overflowing the stack through its - // recursive skipValue walk. - QString nesting; - nesting.reserve(10000); - for (int i = 0; i < 5000; ++i) { - nesting += '['; - } - for (int i = 0; i < 5000; ++i) { - nesting += ']'; - } - const QByteArray json = ("{\"data\":{\"A\":{\"code\":\"a\",\"cards\":" + nesting + "}}}").toUtf8(); - - QJsonParseError qtError; - QJsonDocument::fromJson(json, &qtError); - ASSERT_NE(qtError.error, QJsonParseError::NoError) << "expected Qt to reject deep nesting"; - - RawJson::ScanError scanError; - RawJson::scanSetRanges(json, &scanError); - ASSERT_TRUE(scanError.isError()) << "scanner accepted a document Qt rejects as too deeply nested"; -} - -TEST_F(OracleImporterTest, ScanSetRangesAcceptsQtMaxNesting) -{ - // Pins the boundary rather than only the far-past case: a depth Qt still - // accepts must be accepted by the scanner too. Before the fix the scanner's - // cap was roughly half of Qt's (each level cost two decrements), so a - // depth of 1000 here was rejected even though QJsonDocument parses it. - constexpr int depth = 1000; - QString nesting; - nesting.reserve(2 * depth); - for (int i = 0; i < depth; ++i) { - nesting += '['; - } - for (int i = 0; i < depth; ++i) { - nesting += ']'; - } - const QByteArray json = ("{\"data\":{\"A\":{\"code\":\"a\",\"cards\":" + nesting + "}}}").toUtf8(); - - QJsonParseError qtError; - QJsonDocument::fromJson(json, &qtError); - ASSERT_EQ(qtError.error, QJsonParseError::NoError) << "expected Qt to accept depth " << depth; - - RawJson::ScanError scanError; - RawJson::scanSetRanges(json, &scanError); - ASSERT_FALSE(scanError.isError()) << "scanner rejected a document Qt accepts at depth " << depth; -} - -// ============================================================================ -// Lazy per-set parsing tests -// ============================================================================ - -TEST_F(OracleImporterTest, StartImportParsesSetsLazily) -{ - QJsonObject setObj = makeCard("Lazy Import Card"); - QJsonArray cards; - cards.append(setObj); - QJsonObject dataSet; - dataSet["code"] = "tst"; - dataSet["name"] = "Test Set"; - dataSet["type"] = "expansion"; - dataSet["releaseDate"] = "2024-01-01"; - dataSet["cards"] = cards; - - QJsonObject root; - root["data"] = QJsonObject{{"TST", dataSet}}; - - const QByteArray data = QJsonDocument(root).toJson(QJsonDocument::Compact); - ASSERT_TRUE(importer->readSetsFromByteArray(data)); - ASSERT_FALSE(importer->getRawSetsData().isEmpty()); - - const int importedSets = importer->startImport(); - ASSERT_EQ(importedSets, 1); - ASSERT_EQ(importer->getCardList().size(), 1); - ASSERT_FALSE(importer->getCardList().value("Lazy Import Card").isNull()); -} - -// ============================================================================ -// Scan progress reporting tests -// ============================================================================ - -TEST(OracleScanProgress, ScanProgressReportsMonotonicBytesToTotal) -{ - QJsonObject setObj; - setObj["code"] = "tst"; - setObj["name"] = "Test Set"; - setObj["type"] = "expansion"; - setObj["releaseDate"] = "2024-01-01"; - QJsonArray cards; - for (int i = 0; i < 40; ++i) { - QJsonObject card; - card["name"] = QString("Card %1").arg(i); - card["text"] = "Some rules text used to bulk up the card payload."; - card["layout"] = "normal"; - cards.append(card); - } - setObj["cards"] = cards; - - QJsonObject root; - root["data"] = QJsonObject{{"TST", setObj}}; - - const QByteArray data = QJsonDocument(root).toJson(QJsonDocument::Compact); - - QList> reports; - RawJson::ScanError error; - const QList ranges = - RawJson::scanSetRanges(data, &error, [&reports](qsizetype bytesRead, qsizetype totalBytes) { - reports.append({bytesRead, totalBytes}); - }); - - ASSERT_FALSE(error.isError()) << error.message.toStdString(); - ASSERT_EQ(ranges.size(), 1); - ASSERT_FALSE(reports.isEmpty()); - ASSERT_GT(reports.size(), 1); - - qsizetype last = 0; - for (const auto &[bytesRead, totalBytes] : reports) { - ASSERT_EQ(totalBytes, data.size()); - ASSERT_GE(bytesRead, last) << "scan progress must be monotonic"; - ASSERT_LE(bytesRead, totalBytes) << "scan progress must not overshoot the document size"; - last = bytesRead; - } - ASSERT_EQ(reports.constLast().first, data.size()) << "scan must end at 100%"; - ASSERT_LE(reports.size(), 160) << "scan reports must be throttled"; -} - -TEST(OracleScanProgress, ScanWithoutCallbackStillParses) -{ - QJsonObject setObj; - setObj["code"] = "tst"; - setObj["name"] = "Test Set"; - setObj["type"] = "expansion"; - setObj["releaseDate"] = "2024-01-01"; - setObj["cards"] = QJsonArray(); - - QJsonObject root; - root["data"] = QJsonObject{{"TST", setObj}}; - - const QByteArray data = QJsonDocument(root).toJson(QJsonDocument::Compact); - - RawJson::ScanError error; - const QList ranges = RawJson::scanSetRanges(data, &error); - - ASSERT_FALSE(error.isError()) << error.message.toStdString(); - ASSERT_EQ(ranges.size(), 1); - ASSERT_EQ(ranges.first().code, "tst"); -} - -TEST_F(OracleImporterTest, ReadSetsFromByteArrayEmitsScanProgress) -{ - QJsonObject setObj; - setObj["code"] = "tst"; - setObj["name"] = "Test Set"; - setObj["type"] = "expansion"; - setObj["releaseDate"] = "2024-01-01"; - QJsonArray cards; - for (int i = 0; i < 40; ++i) { - QJsonObject card; - card["name"] = QString("Card %1").arg(i); - cards.append(card); - } - setObj["cards"] = cards; - - QJsonObject root; - root["data"] = QJsonObject{{"TST", setObj}}; - - const QByteArray data = QJsonDocument(root).toJson(QJsonDocument::Compact); - - QList> emissions; - QObject::connect(importer, &OracleImporter::dataReadProgress, - [&emissions](int bytesRead, int totalBytes) { emissions.append({bytesRead, totalBytes}); }); - - ASSERT_TRUE(importer->readSetsFromByteArray(data)); - ASSERT_FALSE(emissions.isEmpty()); - for (const auto &[bytesRead, totalBytes] : emissions) { - ASSERT_EQ(totalBytes, data.size()); - ASSERT_GE(bytesRead, 0); - ASSERT_LE(bytesRead, totalBytes); - } - ASSERT_EQ(emissions.constLast().first, data.size()); -} - -TEST_F(OracleImporterTest, DisablingProgressReportingSuppressesScanEmissions) -{ - QJsonObject setObj; - setObj["code"] = "tst"; - setObj["name"] = "Test Set"; - setObj["type"] = "expansion"; - setObj["releaseDate"] = "2024-01-01"; - setObj["cards"] = QJsonArray(); - QJsonObject root; - root["data"] = QJsonObject{{"TST", setObj}}; - const QByteArray data = QJsonDocument(root).toJson(QJsonDocument::Compact); - - int emissions = 0; - QObject::connect(importer, &OracleImporter::dataReadProgress, [&emissions](int, int) { ++emissions; }); - - importer->setProgressReporting(false); - ASSERT_TRUE(importer->readSetsFromByteArray(data)); - ASSERT_EQ(emissions, 0); - - importer->setProgressReporting(true); - ASSERT_TRUE(importer->readSetsFromByteArray(data)); - ASSERT_GT(emissions, 0); -} - -int main(int argc, char **argv) -{ - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/tests/password_hash_test.cpp b/tests/password_hash_test.cpp index 2b8f8bdb7..38d9b6315 100644 --- a/tests/password_hash_test.cpp +++ b/tests/password_hash_test.cpp @@ -1,9 +1,25 @@ #include "gtest/gtest.h" -#include +#include +#include #include +RNG_Abstract *rng; + namespace { +class PasswordHashTest : public ::testing::Test +{ +protected: + void SetUp() override + { + rng = new RNG_SFMT; + } + + void TearDown() override + { + delete rng; + } +}; TEST(PasswordHashTest, RegressionTest) { @@ -13,29 +29,6 @@ TEST(PasswordHashTest, RegressionTest) QString hash = PasswordHasher::computeHash(password, salt); ASSERT_EQ(hash, salt + expected) << "The computed hash value remains the same"; } - -TEST(PasswordHashTest, SaltUsesAlphanumericCharset) -{ - static const char alphanum[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; - const QString salt = PasswordHasher::generateRandomSalt(); - ASSERT_EQ(salt.size(), 16); - for (const QChar &c : salt) { - ASSERT_NE(strchr(alphanum, c.toLatin1()), nullptr); - } -} - -TEST(PasswordHashTest, SaltsAreUnique) -{ - const QString salt1 = PasswordHasher::generateRandomSalt(); - const QString salt2 = PasswordHasher::generateRandomSalt(); - ASSERT_NE(salt1, salt2); -} - -TEST(PasswordHashTest, TokenHasExpectedLength) -{ - const QString token = PasswordHasher::generateActivationToken(); - ASSERT_EQ(token.size(), 16); -} } // namespace int main(int argc, char **argv) diff --git a/tests/settings/settings_defaults_test.cpp b/tests/settings/settings_defaults_test.cpp index eda778ae9..6c79d5227 100644 --- a/tests/settings/settings_defaults_test.cpp +++ b/tests/settings/settings_defaults_test.cpp @@ -238,12 +238,6 @@ TEST_F(SettingsDefaultsTest, Tabs_ModerationOpen_Default) ASSERT_EQ(s.getTabModerationOpen(), false); } -TEST_F(SettingsDefaultsTest, Tabs_CardArtRulesOpen_Default) -{ - TabsSettings s(settingsPath, nullptr); - ASSERT_EQ(s.getTabCardArtRulesOpen(), false); -} - // --- ChatSettings --- TEST_F(SettingsDefaultsTest, Chat_Mention_Default) @@ -300,12 +294,6 @@ TEST_F(SettingsDefaultsTest, Chat_RoomHistory_Default) ASSERT_EQ(s.getRoomHistory(), true); } -TEST_F(SettingsDefaultsTest, Chat_IgnoreAllPrivateMessages_Default) -{ - ChatSettings s(settingsPath, nullptr); - ASSERT_EQ(s.getIgnoreAllPrivateMessages(), false); -} - // --- PersonalSettings --- TEST_F(SettingsDefaultsTest, Personal_Lang_Default)