diff --git a/.ci/Arch/Dockerfile b/.ci/Arch/Dockerfile
index b08e568f3..36cf5c4ae 100644
--- a/.ci/Arch/Dockerfile
+++ b/.ci/Arch/Dockerfile
@@ -8,13 +8,10 @@ RUN pacman --sync --refresh --sysupgrade --needed --noconfirm \
gtest \
mariadb-libs \
ninja \
- openssl \
protobuf \
qt6-base \
- qt6-declarative \
qt6-imageformats \
qt6-multimedia \
- qt6-shadertools \
qt6-svg \
qt6-tools \
qt6-translations \
diff --git a/.ci/Debian12/Dockerfile b/.ci/Debian12/Dockerfile
index fc756aac2..202405b84 100644
--- a/.ci/Debian12/Dockerfile
+++ b/.ci/Debian12/Dockerfile
@@ -15,14 +15,11 @@ RUN apt-get update && \
libprotobuf-dev \
libqt6multimedia6 \
libqt6sql6-mysql \
- libssl-dev \
ninja-build \
protobuf-compiler \
- qt6-declarative-dev \
qt6-image-formats-plugins \
qt6-l10n-tools \
qt6-multimedia-dev \
- qt6-shadertools-dev \
qt6-svg-dev \
qt6-tools-dev \
qt6-tools-dev-tools \
diff --git a/.ci/Debian13/Dockerfile b/.ci/Debian13/Dockerfile
index bdecb56df..d7ab6ac86 100644
--- a/.ci/Debian13/Dockerfile
+++ b/.ci/Debian13/Dockerfile
@@ -16,14 +16,11 @@ RUN apt-get update && \
libprotobuf-dev \
libqt6multimedia6 \
libqt6sql6-mysql \
- libssl-dev \
ninja-build \
protobuf-compiler \
- qt6-declarative-dev \
qt6-image-formats-plugins \
qt6-l10n-tools \
qt6-multimedia-dev \
- qt6-shadertools-dev \
qt6-svg-dev \
qt6-tools-dev \
qt6-tools-dev-tools \
diff --git a/.ci/Fedora43/Dockerfile b/.ci/Fedora43/Dockerfile
index 463da5a51..27570cf99 100644
--- a/.ci/Fedora43/Dockerfile
+++ b/.ci/Fedora43/Dockerfile
@@ -7,9 +7,8 @@ RUN dnf install -y \
git \
mariadb-devel \
ninja-build \
- openssl-devel \
protobuf-devel \
- qt6-{qtdeclarative,qtmultimedia,qtshadertools,qtsvg,qttools,qtwebsockets}-devel \
+ qt6-{qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \
qt6-qtimageformats \
rpm-build \
xz-devel \
diff --git a/.ci/Fedora44/Dockerfile b/.ci/Fedora44/Dockerfile
index 62238e760..e6c8da7f3 100644
--- a/.ci/Fedora44/Dockerfile
+++ b/.ci/Fedora44/Dockerfile
@@ -7,9 +7,8 @@ RUN dnf install -y \
git \
mariadb-devel \
ninja-build \
- openssl-devel \
protobuf-devel \
- qt6-{qtdeclarative,qtmultimedia,qtshadertools,qtsvg,qttools,qtwebsockets}-devel \
+ qt6-{qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \
qt6-qtimageformats \
rpm-build \
xz-devel \
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 715997474..809b2e43a 100644
--- a/.ci/Ubuntu24.04/Dockerfile
+++ b/.ci/Ubuntu24.04/Dockerfile
@@ -15,14 +15,11 @@ RUN apt-get update && \
libprotobuf-dev \
libqt6multimedia6 \
libqt6sql6-mysql \
- libssl-dev \
ninja-build \
protobuf-compiler \
- qt6-declarative-dev \
qt6-image-formats-plugins \
qt6-l10n-tools \
qt6-multimedia-dev \
- qt6-shadertools-dev \
qt6-svg-dev \
qt6-tools-dev \
qt6-tools-dev-tools \
diff --git a/.ci/Ubuntu26.04/Dockerfile b/.ci/Ubuntu26.04/Dockerfile
index 96dd10763..7b0cd389f 100644
--- a/.ci/Ubuntu26.04/Dockerfile
+++ b/.ci/Ubuntu26.04/Dockerfile
@@ -16,14 +16,11 @@ RUN apt-get update && \
libprotobuf-dev \
libqt6multimedia6 \
libqt6sql6-mysql \
- libssl-dev \
ninja-build \
protobuf-compiler \
- qt6-declarative-dev \
qt6-image-formats-plugins \
qt6-l10n-tools \
qt6-multimedia-dev \
- qt6-shadertools-dev \
qt6-svg-dev \
qt6-tools-dev \
qt6-tools-dev-tools \
diff --git a/.ci/compile.sh b/.ci/compile.sh
index f20432893..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"
@@ -327,32 +324,4 @@ if [[ $MAKE_PACKAGE ]]; then
BUILD_DIR="$BUILD_DIR" .ci/name_build.sh "$PACKAGE_SUFFIX"
echo "::endgroup::"
fi
-
- if [[ $RUNNER_OS == Windows ]]; then
- echo "::group::Check installer for build-tree artifacts"
- cd "$BUILD_DIR"
- package="$(find . -maxdepth 1 -type f -name 'Cockatrice-*.exe' -print -quit)"
- if [[ ! $package ]]; then
- echo "::error file=$0::Could not find installer to inspect"
- exit 1
- fi
- seven_zip="$(command -v 7z || true)"
- if [[ ! $seven_zip ]]; then
- seven_zip="/c/Program Files/7-Zip/7z.exe"
- fi
- if [[ ! -f $seven_zip ]]; then
- echo "::warning file=$0::7-Zip not found, skipping installer content check"
- else
- echo "Inspecting $package"
- # Fail the build if the installer contains any path left behind by the MSBuild or
- # Qt AUTOMOC tooling (build-tree artifacts must live in the build dir, not the install)
- if "$seven_zip" l "$package" |
- grep -E "_autogen|\.dir[\\/]|\.tlog|(^|[\\/])x64[\\/]|(^|[\\/])\.qt[\\/]|(^|[\\/])\.qsb[\\/]|(^|[\\/])\.lupdate[\\/]|CMakeFiles"; then
- echo "::error file=$0::Installer contains build-tree artifacts"
- exit 1
- fi
- echo "Installer content is clean"
- fi
- echo "::endgroup::"
- fi
fi
diff --git a/.ci/release_template.md b/.ci/release_template.md
index 23b475150..ac78a193a 100644
--- a/.ci/release_template.md
+++ b/.ci/release_template.md
@@ -12,9 +12,9 @@ Available pre-compiled binaries for installation:
• Windows 10+
macOS
- • macOS 15+ Sequoia
- • macOS 14+ Sonoma
- • macOS 13+ Ventura (x86)
+ • macOS 15+ Sequoia Apple M
+ • macOS 14+ Sonoma Apple M
+ • macOS 13+ Ventura Intel
Linux
• Ubuntu 26.04 LTS Resolute Racoon
@@ -24,10 +24,10 @@ Available pre-compiled binaries for installation:
• Fedora 44
• Fedora 43
- General Linux support is available via a flatpak package hosted at Flathub (x86 & ARM)!
- Thanks to courtesy of @FFY00, the app is also available in Arch Linux's official extra repository.
+We are also packaged in Arch Linux's official extra repository, courtesy of @FFY00.
+General Linux support is available via a flatpak package at Flathub!
- We maintain a Docker image for "Servatrice" in GHCR (x86 & ARM). You can docker pull it or use our Docker Compose files!
+We provide a Docker image for "Servatrice" in GHCR. You can docker pull it or use our Docker Compose files!
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
deleted file mode 100644
index 75fbc59f1..000000000
--- a/.github/workflows/codeql.yml
+++ /dev/null
@@ -1,95 +0,0 @@
-# GitHub Docs on Code Scanning:
-# https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning
-# https://docs.github.com/en/code-security/how-tos/find-and-fix-code-vulnerabilities/manage-your-configuration
-# https://docs.github.com/en/code-security/reference/code-scanning/workflow-configuration-options
-#
-# CodeQL Docs:
-# https://codeql.github.com/docs/
-
-name: CodeQL
-
-permissions:
- contents: read
- security-events: write # needed to post results
-
-on:
- push:
- branches:
- - master
- pull_request:
-
-# Cancel earlier, unfinished runs of this workflow on the same branch
-concurrency:
- group: "${{ github.workflow }} @ ${{ github.ref_name }}"
- cancel-in-progress: true
-
-jobs:
- analyze:
- name: Analyze (${{ matrix.language }})
- runs-on: ubuntu-latest
-
- strategy:
- fail-fast: false
- matrix:
- include:
- # https://codeql.github.com/docs/codeql-overview/supported-languages-and-frameworks/
- - language: cpp
- build-mode: manual
- - language: actions
- build-mode: none
-
- steps:
- - name: "Checkout repository"
- uses: actions/checkout@v7
-
- - name: "Initialize CodeQL"
- uses: github/codeql-action/init@v4
- with:
- languages: ${{ matrix.language }}
- build-mode: ${{ matrix.build-mode }}
- # https://docs.github.com/en/code-security/reference/code-scanning/codeql/codeql-queries/c-cpp-built-in-queries
- # https://docs.github.com/en/code-security/reference/code-scanning/codeql/codeql-queries/actions-built-in-queries
- queries: security-extended
- dependency-caching: true
-
- - name: "[C++] Install dependencies"
- if: matrix.language == 'cpp' && matrix.build-mode == 'manual'
- shell: bash
- env:
- DEBIAN_FRONTEND: noninteractive
- run: |
- sudo apt-get update
- sudo apt-get install -y --no-install-recommends \
- cmake \
- g++ \
- libgl-dev \
- liblzma-dev \
- libmariadb-dev-compat \
- libprotobuf-dev \
- ninja-build \
- protobuf-compiler \
- qt6-declarative-dev \
- qt6-multimedia-dev \
- qt6-shadertools-dev \
- qt6-svg-dev \
- qt6-tools-dev \
- qt6-tools-dev-tools \
- qt6-websockets-dev
-
-# Minimize dependency install
-# Add ccache usage for faster compilation, (install ccache dep, actions/cache step + append DUSE_CCACHE=1 in cmake config, CCACHE env values)
-
- - name: "[C++] Configure CMake"
- if: matrix.language == 'cpp' && matrix.build-mode == 'manual'
- shell: bash
- run: cmake -S . -B build -G Ninja -DWITH_SERVER=1 -DCMAKE_BUILD_TYPE=Release
-
- - name: "[C++] Build application"
- if: matrix.language == 'cpp' && matrix.build-mode == 'manual'
- shell: bash
- run: cmake --build build
-
- - name: "Perform CodeQL Analysis"
- uses: github/codeql-action/analyze@v4
- with:
- category: "/language:${{ matrix.language }}"
diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml
index bd528f245..09fabfbc9 100644
--- a/.github/workflows/desktop-build.yml
+++ b/.github/workflows/desktop-build.yml
@@ -34,7 +34,7 @@ on:
- 'vcpkg.json'
- 'vcpkg' # needed to match submodule bumps (gitlink)
-# Cancel earlier, unfinished runs of this workflow on the same branch (unless on tag --> release)
+# Cancel earlier, unfinished runs of this workflow on the same branch (unless on release)
concurrency:
group: "${{ github.workflow }} @ ${{ github.ref_name }}"
cancel-in-progress: ${{ github.ref_type != 'tag' }}
@@ -42,7 +42,7 @@ concurrency:
jobs:
configure:
name: Configure
- runs-on: ubuntu-slim # https://github.com/actions/runner-images/blob/main/images/ubuntu-slim/ubuntu-slim-Readme.md
+ runs-on: ubuntu-slim
outputs:
tag: ${{ steps.configure.outputs.tag }}
sha: ${{ steps.configure.outputs.sha }}
@@ -146,13 +146,13 @@ jobs:
name: ${{ matrix.distro }} ${{ matrix.version }}
needs: configure
- runs-on: ubuntu-latest # https://github.com/actions/runner-images
+ runs-on: ubuntu-latest
continue-on-error: ${{ matrix.allow-failure == 'yes' }}
timeout-minutes: 70
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 }}
@@ -161,11 +161,14 @@ jobs:
uses: actions/checkout@v7
- name: "Restore compiler cache (ccache)"
- id: restore_ccache
+ id: ccache_restore
uses: actions/cache/restore@v6
+ env:
+ BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
with:
- key: ccache-${{ matrix.distro }}${{ matrix.version }}
+ key: ccache-${{ matrix.distro }}${{ matrix.version }}-${{ env.BRANCH_NAME }}
path: ${{ env.CACHE }}
+ restore-keys: ccache-${{ matrix.distro }}${{ matrix.version }}-
- name: "Build ${{ matrix.distro }} ${{ matrix.version }} Docker image"
shell: bash
@@ -176,12 +179,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
@@ -204,10 +203,10 @@ jobs:
# Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342
- name: "Delete remote compiler cache (ccache)"
- if: github.ref == 'refs/heads/master' && steps.restore_ccache.outputs.cache-hit
+ if: github.ref == 'refs/heads/master' && steps.ccache_restore.outputs.cache-hit
continue-on-error: true
env:
- CACHE_PRIMARY_KEY: ${{ steps.restore_ccache.outputs.cache-primary-key }}
+ CACHE_PRIMARY_KEY: ${{ steps.ccache_restore.outputs.cache-primary-key }}
GH_TOKEN: ${{ github.token }}
run: |
if gh cache delete --repo "$GITHUB_REPOSITORY" "$CACHE_PRIMARY_KEY"; then
@@ -218,7 +217,7 @@ jobs:
if: github.ref == 'refs/heads/master'
uses: actions/cache/save@v6
with:
- key: ${{ steps.restore_ccache.outputs.cache-primary-key }}
+ key: ${{ steps.ccache_restore.outputs.cache-primary-key }}
path: ${{ env.CACHE }}
- name: "Upload artifact"
@@ -263,77 +262,74 @@ jobs:
matrix:
include:
- os: macOS
- target: 13 # EOL 2025-09-15
- runner: macos-15-intel # https://github.com/actions/runner-images/blob/main/images/macos/macos-15-Readme.md
- # macos-26-intel is very slow and fails in CPack during DMG config if not increasing Finder timeout
+ target: 13
+ runner: macos-15-intel
ccache_eviction_age: 7d
cmake_generator: Ninja
make_package: 1
override_target: 13
package_suffix: "-macOS13_Intel"
- qt_version: 6.11.*
- qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets
+ qt_version: 6.11.0
+ qt_modules: qtimageformats qtmultimedia qtwebsockets
soc: Intel
type: Release
use_ccache: 1
- xcode: "26.3"
+ xcode: "16.4"
- os: macOS
- target: 14 # EOL 2026-??
- runner: macos-26 # https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md
+ target: 14
+ runner: macos-14
ccache_eviction_age: 7d
cmake_generator: Ninja
make_package: 1
- override_target: 14
package_suffix: "-macOS14"
- qt_version: 6.11.*
- qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets
+ qt_version: 6.11.0
+ qt_modules: qtimageformats qtmultimedia qtwebsockets
soc: Apple
type: Release
use_ccache: 1
- xcode: "26.6"
+ xcode: "15.4"
- os: macOS
target: 15
- runner: macos-26 # https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md
+ runner: macos-15
ccache_eviction_age: 7d
cmake_generator: Ninja
make_package: 1
- override_target: 15
package_suffix: "-macOS15"
- qt_version: 6.11.*
- qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets
+ qt_version: 6.11.0
+ qt_modules: qtimageformats qtmultimedia qtwebsockets
soc: Apple
type: Release
use_ccache: 1
- xcode: "26.6"
+ xcode: "16.4"
- os: macOS
- target: 26
- runner: macos-26 # https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md
+ target: 15
+ runner: macos-15
ccache_eviction_age: 7d
cmake_generator: Ninja
- qt_version: 6.11.*
- qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets
+ qt_version: 6.11.0
+ qt_modules: qtimageformats qtmultimedia qtwebsockets
soc: Apple
type: Debug
use_ccache: 1
- xcode: "26.6"
+ xcode: "16.4"
- os: Windows
target: 10
- runner: windows-2025 # https://github.com/actions/runner-images/blob/main/images/windows/Windows2025-VS2026-Readme.md
+ runner: windows-2025
cmake_generator: "Visual Studio 18 2026"
cmake_generator_platform: x64
make_package: 1
package_suffix: "-Win10"
- qt_version: 6.11.*
- qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets
+ qt_version: 6.11.0
+ qt_modules: qtimageformats qtmultimedia qtwebsockets
type: Release
name: ${{ matrix.os }} ${{ matrix.target }}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }}
@@ -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"
@@ -352,6 +348,7 @@ jobs:
- name: "[Windows] Add msbuild to PATH"
if: matrix.os == 'Windows'
+ id: add-msbuild
uses: microsoft/setup-msbuild@v3
with:
msbuild-architecture: x64
@@ -362,54 +359,54 @@ jobs:
- name: "[macOS] Restore compiler cache (ccache)"
if: matrix.os == 'macOS' && matrix.use_ccache == 1
- id: restore_ccache
+ id: ccache_restore
uses: actions/cache/restore@v6
+ env:
+ BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
with:
- key: ccache-${{ matrix.runner }}_${{ matrix.override_target }}-Xcode${{ matrix.xcode }}
+ key: ccache-${{ matrix.runner }}-${{ matrix.soc }}-${{ matrix.type }}-${{ env.BRANCH_NAME }}
path: ${{ env.CCACHE_DIR }}
+ restore-keys: ccache-${{ matrix.runner }}-${{ matrix.soc }}-${{ matrix.type }}-
- - name: "[macOS] Install aqtinstall"
- if: matrix.os == 'macOS'
+ - name: "Install aqtinstall"
run: pipx install aqtinstall
# Resolve given wildcard versions (e.g. Qt 6.6.*) to latest version via aqtinstall to avoid stale caches on new releases
- - name: "[macOS] Resolve latest Qt from ${{ matrix.qt_version }} input"
- if: matrix.os == 'macOS'
+ - name: "Resolve latest Qt patch version"
env:
QT_VERSION: ${{ matrix.qt_version }}
id: resolve_qt_version
shell: bash
run: .ci/resolve_latest_aqt_qt_version.sh "$QT_VERSION"
- - name: "[macOS] Restore thin Qt ${{ steps.resolve_qt_version.outputs.version }}"
+ - name: "[macOS] Restore thin Qt ${{ steps.resolve_qt_version.outputs.version }} libraries"
if: matrix.os == 'macOS'
id: restore_qt
uses: actions/cache/restore@v6
with:
- key: Qt-${{ steps.resolve_qt_version.outputs.version }}-macOS-${{ matrix.soc }}-${{ matrix.qt_modules }}-thin
+ key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }}
path: ${{ github.workspace }}/Qt
# Using jurplel/install-qt-action to install Qt without using brew
# Qt build using vcpkg either just fails or takes too long to build
- - name: "[macOS] Install fat Qt ${{ matrix.qt_version }}"
+ - name: "[macOS] Install fat Qt ${{ steps.resolve_qt_version.outputs.version }}"
if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true'
uses: jurplel/install-qt-action@v4
with:
cache: false
- # cache-key-prefix: Qt
- dir: ${{ github.workspace }} # thinning script depends on this location
+ dir: ${{ github.workspace }}
modules: ${{ matrix.qt_modules }}
- version: ${{ matrix.qt_version }}
+ version: ${{ steps.resolve_qt_version.outputs.version }}
- name: "[macOS] Create thin Qt libraries"
if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true'
run: .ci/thin_macos_qtlib.sh
- name: "[macOS] Cache thin Qt libraries"
- if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true' && github.ref == 'refs/heads/master'
+ if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true'
uses: actions/cache/save@v6
with:
- key: ${{ steps.restore_qt.outputs.cache-primary-key }}
+ key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }}
path: ${{ github.workspace }}/Qt
- name: "[Windows] Install Qt ${{ matrix.qt_version }}"
@@ -418,10 +415,9 @@ jobs:
with:
# Qt 6.11.0 only works with aqtinstall directly from git until aqtinstall 3.4 is released
aqtsource: git+https://github.com/miurahr/aqtinstall.git
- cache: ${{ github.ref == 'refs/heads/master' }}
- cache-key-prefix: Qt
+ cache: true
modules: ${{ matrix.qt_modules }}
- version: ${{ matrix.qt_version }}
+ version: ${{ steps.resolve_qt_version.outputs.version }}
- name: "[Windows] Install NSIS"
if: matrix.os == 'Windows'
@@ -453,28 +449,28 @@ jobs:
PACKAGE_SUFFIX: '${{ matrix.package_suffix }}'
TARGET_MACOS_VERSION: ${{ matrix.override_target }}
USE_CCACHE: ${{ matrix.use_ccache }}
- VCPKG_BINARY_SOURCES: "clear;files,${{ steps.vcpkg-cache.outputs.path }},${{ case(github.ref == 'refs/heads/master', 'readwrite', 'read') }}"
+ VCPKG_BINARY_SOURCES: 'clear;files,${{ steps.vcpkg-cache.outputs.path }},readwrite'
VCPKG_DISABLE_METRICS: 1
VCPKG_FEATURE_FLAGS: dependencygraph
run: .ci/compile.sh --server --test --vcpkg
# Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342
- name: "[macOS] Delete remote compiler cache (ccache)"
- if: matrix.os == 'macOS' && matrix.use_ccache == 1 && github.ref == 'refs/heads/master' && steps.restore_ccache.outputs.cache-hit
+ if: matrix.os == 'macOS' && matrix.use_ccache == 1 && github.ref == 'refs/heads/master' && steps.ccache_restore.outputs.cache-hit
continue-on-error: true
env:
- CACHE_PRIMARY_KEY: ${{ steps.restore_ccache.outputs.cache-primary-key }}
+ CACHE_PRIMARY_KEY: ${{ steps.ccache_restore.outputs.cache-primary-key }}
GH_TOKEN: ${{ github.token }}
run: |
if gh cache delete --repo "$GITHUB_REPOSITORY" "$CACHE_PRIMARY_KEY"; then
echo "Cache deleted successfully"
fi
- - name: "[macOS] Cache updated compiler cache (ccache)"
+ - name: "[macOS] Save updated compiler cache (ccache)"
if: matrix.os == 'macOS' && matrix.use_ccache == 1 && github.ref == 'refs/heads/master'
uses: actions/cache/save@v6
with:
- key: ${{ steps.restore_ccache.outputs.cache-primary-key }}
+ key: ${{ steps.ccache_restore.outputs.cache-primary-key }}
path: ${{ env.CCACHE_DIR }}
- name: "[macOS] Sign app bundle"
diff --git a/.github/workflows/desktop-lint.yml b/.github/workflows/desktop-lint.yml
index 93ba79464..5f31ea59c 100644
--- a/.github/workflows/desktop-lint.yml
+++ b/.github/workflows/desktop-lint.yml
@@ -1,8 +1,5 @@
name: Code Style (C++)
-permissions:
- contents: read
-
on:
# Push trigger not needed for linting, we do not allow direct pushes to master
pull_request:
diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml
index 967d94c58..5384c9e64 100644
--- a/.github/workflows/docker-release.yml
+++ b/.github/workflows/docker-release.yml
@@ -1,8 +1,8 @@
-name: Build Docker
+name: Build Docker Image
permissions:
- contents: read # needed to checkout repo
- packages: write # needed for interacting with GHCR
+ contents: read
+ packages: write
on:
push:
@@ -13,10 +13,7 @@ on:
- master
paths:
- '.github/workflows/docker-release.yml'
- - '.dockerignore'
- 'Dockerfile'
- - 'docker-compose.yml'
- - 'docker-compose.yml.windows'
release:
types:
- released # publishing of stable releases
@@ -26,38 +23,36 @@ concurrency:
group: "${{ github.workflow }} @ ${{ github.ref_name }}"
cancel-in-progress: ${{ github.event_name != 'release' }}
-env:
- GHCR_IMAGE: ghcr.io/cockatrice/servatrice
- OCI_DESCRIPTION: Server for Cockatrice, a cross-platform virtual tabletop for multiplayer card games
- OCI_TITLE: Servatrice
- OCI_URL: https://cockatrice.github.io/
-
jobs:
- # Create one platform-specific image and publish its OCI image manifest per matrix job
- build:
- name: "Servatrice (${{ matrix.label }})"
- if: github.repository_owner == 'Cockatrice'
- runs-on: ${{ matrix.runner }}
-
- strategy:
- fail-fast: false
- matrix:
- include:
- - label: x86
- platform: linux/amd64
- runner: ubuntu-latest # https://github.com/actions/runner-images
-
- - label: arm
- platform: linux/arm64
- runner: ubuntu-24.04-arm # https://github.com/actions/runner-images/blob/main/images/ubuntu/Ubuntu2404-Arm64-Readme.md, replace with "ubuntu-latest-arm" once available
-
- env:
- CACHE_SCOPE: servatrice-${{ matrix.label }}
-
+ docker:
+ name: amd64 & arm64
+ if: ${{ github.repository_owner == 'Cockatrice' }}
+ runs-on: ubuntu-latest
+
steps:
- name: "Checkout"
uses: actions/checkout@v7
+ - name: "Docker metadata"
+ id: metadata
+ uses: docker/metadata-action@v6
+ env:
+ DOCKER_METADATA_ANNOTATIONS_LEVELS: index # needed for GHCR
+ with:
+ annotations: |
+ org.opencontainers.image.title=Servatrice
+ org.opencontainers.image.url=https://cockatrice.github.io/
+ org.opencontainers.image.description=Server for Cockatrice, a cross-platform virtual tabletop for multiplayer card games
+ images: |
+ ghcr.io/cockatrice/servatrice
+ labels: |
+ org.opencontainers.image.title=Servatrice
+ org.opencontainers.image.url=https://cockatrice.github.io/
+ org.opencontainers.image.description=Server for Cockatrice, a cross-platform virtual tabletop for multiplayer card games
+
+ - name: "Set up QEMU"
+ uses: docker/setup-qemu-action@v4
+
- name: "Set up Docker buildx"
uses: docker/setup-buildx-action@v4
@@ -66,117 +61,18 @@ jobs:
id: login
uses: docker/login-action@v4
with:
+ password: ${{ github.token }}
registry: ghcr.io
username: ${{ github.actor }}
- password: ${{ github.token }}
- # Don't push for non-release triggers
- - name: "Build image"
- if: steps.login.outcome != 'success'
+ - name: "Build and push Docker image"
uses: docker/build-push-action@v7
with:
- cache-from: type=gha,scope=${{ env.CACHE_SCOPE }}
- cache-to: ${{ case(github.ref == 'refs/heads/master', format('type=gha,mode=max,scope={0}', env.CACHE_SCOPE), '') }}
+ annotations: ${{ steps.metadata.outputs.annotations }}
+ cache-from: type=gha,scope=servatrice
+ cache-to: type=gha,mode=max,scope=servatrice
context: .
- platforms: ${{ matrix.platform }}
- push: false
-
- # Add OCI labels and push single-platform image by digest (without tags)
- - name: "Build image and push by digest"
- if: steps.login.outcome == 'success'
- id: build
- uses: docker/build-push-action@v7
- with:
- cache-from: type=gha,scope=${{ env.CACHE_SCOPE }}
- cache-to: type=gha,mode=max,scope=${{ env.CACHE_SCOPE }}
- context: .
- labels: |
- org.opencontainers.image.description=${{ env.OCI_DESCRIPTION }}
- org.opencontainers.image.title=${{ env.OCI_TITLE }}
- org.opencontainers.image.url=${{ env.OCI_URL }}
- outputs: type=image,name=${{ env.GHCR_IMAGE }},name-canonical=true,push=true,push-by-digest=true
- platforms: ${{ matrix.platform }}
- provenance: mode=max # Do not pass secrets as build arguments with this option
- sbom: true
-
- - name: "Export digest"
- if: steps.login.outcome == 'success'
- env:
- DIGEST: ${{ steps.build.outputs.digest }}
- run: |
- mkdir -p "$RUNNER_TEMP/digests"
- touch "$RUNNER_TEMP/digests/${DIGEST#sha256:}"
-
- - name: "Upload digest"
- if: steps.login.outcome == 'success'
- uses: actions/upload-artifact@v7
- with:
- archive: false
- if-no-files-found: error
- name: digest-${{ matrix.label }}
- path: ${{ runner.temp }}/digests/*
- retention-days: 1
-
-
- # Create an OCI image index from the platform-specific image manifests
- index:
- name: "Publish multi-platform Servatrice image"
- if: github.repository_owner == 'Cockatrice' && github.event_name == 'release' && github.event.release.prerelease == false
- needs: build
- runs-on: ubuntu-slim # https://github.com/actions/runner-images/blob/main/images/ubuntu-slim/ubuntu-slim-Readme.md
-
- steps:
- - name: "Download digests"
- uses: actions/download-artifact@v8
- with:
- path: ${{ runner.temp }}/digests
- pattern: digest-*
- merge-multiple: true
-
- - name: "Login to GitHub Container Registry (GHCR)"
- uses: docker/login-action@v4
- with:
- registry: ghcr.io
- username: ${{ github.actor }}
- password: ${{ github.token }}
-
- - name: "Docker metadata"
- id: metadata
- uses: docker/metadata-action@v6
- with:
- images: ${{ env.GHCR_IMAGE }}
- flavor: |
- latest=auto
- tags: |
- type=ref,event=tag # if semver, also: type=semver,pattern={{version}} / {{major}}.{{minor}}
-
- # Add OCI annotations to image index and publish tags
- - name: "Create image index"
- env:
- DOCKER_TAGS: ${{ steps.metadata.outputs.tags }}
- working-directory: ${{ runner.temp }}/digests
- run: |
- TAG_ARGS=()
- while IFS= read -r tag; do
- TAG_ARGS+=(--tag "$tag")
- done <<< "$DOCKER_TAGS"
-
- DIGEST_ARGS=()
- for digest in *; do
- DIGEST_ARGS+=("$GHCR_IMAGE@sha256:$digest")
- done
-
- docker buildx imagetools create \
- --prefer-index=true \
- --annotation "index:org.opencontainers.image.description=$OCI_DESCRIPTION" \
- --annotation "index:org.opencontainers.image.title=$OCI_TITLE" \
- --annotation "index:org.opencontainers.image.url=$OCI_URL" \
- "${TAG_ARGS[@]}" \
- "${DIGEST_ARGS[@]}"
-
- - name: "Inspect images"
- env:
- GITHUB_TAG: ${{ github.ref_name }}
- run: |
- docker buildx imagetools inspect "$GHCR_IMAGE:latest"
- docker buildx imagetools inspect "$GHCR_IMAGE:$GITHUB_TAG"
+ labels: ${{ steps.metadata.outputs.labels }}
+ platforms: linux/amd64,linux/arm64
+ push: ${{ steps.login.outcome == 'success' }}
+ tags: ${{ steps.metadata.outputs.tags }}
diff --git a/.github/workflows/documentation-build.yml b/.github/workflows/documentation-build.yml
index 419cbfbfb..4b9ca79ab 100644
--- a/.github/workflows/documentation-build.yml
+++ b/.github/workflows/documentation-build.yml
@@ -1,8 +1,5 @@
name: Generate Docs
-permissions:
- contents: read # write permission to the destination repo come from 'deploy_key'
-
on:
pull_request:
paths:
@@ -14,11 +11,6 @@ on:
- published # publishing of stable releases and pre-releases
workflow_dispatch:
-# Cancel earlier, unfinished runs of this workflow on the same branch (unless on release)
-concurrency:
- group: "${{ github.workflow }} @ ${{ github.ref_name }}"
- cancel-in-progress: ${{ github.event_name != 'release' }}
-
env:
COCKATRICE_REF: ${{ github.ref_name }} # tag name if the commit is tagged, otherwise branch name
diff --git a/.github/workflows/translations-pull.yml b/.github/workflows/translations-pull.yml
index 71b0b4c22..a3db5f86d 100644
--- a/.github/workflows/translations-pull.yml
+++ b/.github/workflows/translations-pull.yml
@@ -1,9 +1,5 @@
name: Update Translations
-permissions:
- contents: read
- pull-requests: write
-
on:
pull_request:
paths:
diff --git a/.github/workflows/translations-push.yml b/.github/workflows/translations-push.yml
index 41a7aef40..c4d3f61fb 100644
--- a/.github/workflows/translations-push.yml
+++ b/.github/workflows/translations-push.yml
@@ -1,9 +1,5 @@
name: Update Translation Source
-permissions:
- contents: read
- pull-requests: write
-
on:
pull_request:
paths:
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 293e25dd9..27fecc979 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -5,23 +5,23 @@
# 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)
+# Early detect ccache
option(USE_CCACHE "Cache the build results with ccache" ON)
# Treat warnings as errors (Debug builds only)
option(WARNING_AS_ERROR "Treat warnings as errors in debug builds" ON)
# Check for translation updates
option(UPDATE_TRANSLATIONS "Update translations on compile" OFF)
-# Compile Cockatrice
-option(WITH_CLIENT "Build Cockatrice client" ON)
-# Compile Oracle
-option(WITH_ORACLE "Build Cockatrice card database tool (Oracle)" ON)
-# Compile Servatrice
-option(WITH_SERVER "Build Cockatrice server (Servatrice)" OFF)
+# Compile servatrice
+option(WITH_SERVER "build servatrice" OFF)
+# Compile cockatrice
+option(WITH_CLIENT "build cockatrice" ON)
+# Compile oracle
+option(WITH_ORACLE "build oracle" ON)
# Compile tests
-option(TEST "Build tests" OFF)
+option(TEST "build tests" OFF)
# Use vcpkg regardless of OS
option(USE_VCPKG "Use vcpkg regardless of OS" OFF)
@@ -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()
@@ -293,7 +281,7 @@ if(UNIX)
if(CPACK_GENERATOR STREQUAL "RPM")
set(CPACK_RPM_PACKAGE_LICENSE "GPLv2")
set(CPACK_RPM_MAIN_COMPONENT "cockatrice")
- set(CPACK_RPM_PACKAGE_REQUIRES "protobuf, qt6-qtimageformats, qt6-qtmultimedia, qt6-qtsvg, qt6-qttools")
+ set(CPACK_RPM_PACKAGE_REQUIRES "protobuf, qt6-qttools, qt6-qtsvg, qt6-qtmultimedia, qt6-qtimageformats")
set(CPACK_RPM_PACKAGE_GROUP "Amusements/Games")
set(CPACK_RPM_PACKAGE_URL "http://github.com/Cockatrice/Cockatrice")
# stop directories from making package conflicts
@@ -311,7 +299,7 @@ if(UNIX)
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
set(CPACK_DEBIAN_PACKAGE_SECTION "games")
set(CPACK_DEBIAN_PACKAGE_HOMEPAGE "http://github.com/Cockatrice/Cockatrice")
- set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt6multimedia6, libqt6svg6, qt6-image-formats-plugins, qt6-qpa-plugins")
+ set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt6multimedia6, libqt6svg6, qt6-qpa-plugins, qt6-image-formats-plugins")
set(CPACK_DEBIAN_PACKAGE_RECOMMENDS "libqt6sql6-mysql") # for connecting servatrice to a mysql db
endif()
endif()
diff --git a/Dockerfile b/Dockerfile
index 7d3deb5fb..7c5c773c9 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -3,47 +3,35 @@ FROM ubuntu:26.04 AS build
ARG DEBIAN_FRONTEND=noninteractive
-RUN apt-get update \
- && apt-get install -y --no-install-recommends \
- build-essential \
- cmake \
- ninja-build \
- file \
- g++ \
- git \
- libmariadb-dev-compat \
- libprotobuf-dev \
- libqt6sql6-mysql \
- libssl-dev \
- qt6-websockets-dev \
- protobuf-compiler \
- qt6-tools-dev \
- qt6-tools-dev-tools
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ build-essential \
+ cmake \
+ file \
+ g++ \
+ git \
+ libmariadb-dev-compat \
+ libprotobuf-dev \
+ libqt6sql6-mysql \
+ qt6-websockets-dev \
+ protobuf-compiler \
+ qt6-tools-dev \
+ qt6-tools-dev-tools
WORKDIR /src
COPY . .
-RUN cmake \
- -S . \
- -B build \
- -G Ninja \
- -DWITH_CLIENT=0 \
- -DWITH_ORACLE=0 \
- -DWITH_SERVER=1 \
- && cmake --build build \
- && cmake --install build
+RUN mkdir build && cd build && \
+ cmake .. -DWITH_SERVER=1 -DWITH_CLIENT=0 -DWITH_ORACLE=0 && \
+ make -j$(nproc) && \
+ make install
# -------- Runtime Stage (clean) --------
FROM ubuntu:26.04
-ARG DEBIAN_FRONTEND=noninteractive
-
-RUN apt-get update \
- && apt-get install -y --no-install-recommends \
- libprotobuf32t64 \
- libqt6sql6-mysql \
- libqt6websockets6 \
- libssl3 \
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ libprotobuf32t64 \
+ libqt6sql6-mysql \
+ libqt6websockets6 \
&& 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/FindQtRuntime.cmake b/cmake/FindQtRuntime.cmake
index 971c9094d..8a3050813 100644
--- a/cmake/FindQtRuntime.cmake
+++ b/cmake/FindQtRuntime.cmake
@@ -18,17 +18,14 @@ if(WITH_CLIENT)
Multimedia
Network
PrintSupport
- ShaderTools
Svg
WebSockets
Widgets
Xml
- Quick
- QuickWidgets
)
endif()
if(WITH_ORACLE)
- set(_ORACLE_NEEDED Concurrent Network Svg Widgets Xml)
+ set(_ORACLE_NEEDED Concurrent Network Svg Widgets)
endif()
if(TEST)
# Union of Qt modules required across all test targets (independent of application targets).
diff --git a/cmake/NSIS.template.in b/cmake/NSIS.template.in
index b3cbcece8..5af116470 100644
--- a/cmake/NSIS.template.in
+++ b/cmake/NSIS.template.in
@@ -387,14 +387,19 @@ SectionEnd
Section "un.Application" UnSecApplication
SetShellVarContext all
+ RMDir /r "$INSTDIR\plugins"
+ RMDir /r "$INSTDIR\sounds"
+ RMDir /r "$INSTDIR\themes"
+ RMDir /r "$INSTDIR\translations"
+ Delete "$INSTDIR\*.exe"
+ Delete "$INSTDIR\*.dll"
+ Delete "$INSTDIR\qt.conf"
+ Delete "$INSTDIR\qdebug.txt"
+ Delete "$INSTDIR\servatrice.sql"
+ Delete "$INSTDIR\servatrice.ini.example"
+ RMDir "$INSTDIR"
- ; Remove the entire application directory so any file that is not part of
- ; the installed payload (e.g. build-tree artifacts such as *.dir folders,
- ; *_autogen and *.tlog files from a build) cannot survive between an
- ; uninstall and a fresh reinstall.
- RMDir /r "$INSTDIR"
-
- RMDir /r "$SMPROGRAMS\Cockatrice"
+ RMDir "$SMPROGRAMS\Cockatrice"
DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice"
SectionEnd
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 9b31310e6..56f5b89f9 100644
--- a/cockatrice/CMakeLists.txt
+++ b/cockatrice/CMakeLists.txt
@@ -15,9 +15,6 @@ set(cockatrice_SOURCES
src/client/network/update/client/client_update_checker.cpp
src/client/network/update/client/release_channel.cpp
src/client/network/update/card_spoiler/spoiler_background_updater.cpp
- src/client/latency_graph_widget.cpp
- src/client/latency_status_widget.cpp
- src/client/lag_monitor.cpp
src/client/sound_engine.cpp
src/client/settings/cache_settings.cpp
src/client/settings/card_counter_settings.cpp
@@ -45,14 +42,9 @@ set(cockatrice_SOURCES
src/interface/widgets/dialogs/dlg_load_deck_from_website.cpp
src/interface/widgets/dialogs/dlg_load_remote_deck.cpp
src/interface/widgets/dialogs/dlg_local_game_options.cpp
- src/interface/widgets/dialogs/dlg_login_prompt.cpp
src/interface/widgets/dialogs/dlg_manage_sets.cpp
- src/interface/widgets/dialogs/dlg_my_reports.cpp
src/interface/widgets/dialogs/dlg_register.cpp
- src/interface/widgets/dialogs/dlg_report_user.cpp
src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp
- src/interface/widgets/dialogs/dlg_share_deck.cpp
- src/interface/widgets/dialogs/dlg_shared_decks_preview.cpp
src/interface/widgets/dialogs/dlg_settings.cpp
src/interface/widgets/dialogs/dlg_startup_card_check.cpp
src/interface/widgets/dialogs/dlg_tip_of_the_day.cpp
@@ -60,9 +52,6 @@ set(cockatrice_SOURCES
src/interface/widgets/dialogs/dlg_view_log.cpp
src/interface/widgets/dialogs/override_printing_warning.cpp
src/interface/widgets/dialogs/tip_of_the_day.cpp
- src/interface/widgets/deck_share/deck_share_utils.cpp
- src/interface/widgets/deck_share/shared_deck_preview_widget.cpp
- src/interface/widgets/deck_share/share_bar_widget.cpp
src/filters/deck_filter_string.cpp
src/filters/filter_builder.cpp
src/filters/filter_tree_model.cpp
@@ -169,11 +158,8 @@ set(cockatrice_SOURCES
src/interface/palette_editor/palette_grid_widget.cpp
src/interface/palette_editor/palette_editor_dialog.cpp
src/interface/widgets/cards/additional_info/color_identity_widget.cpp
- src/interface/widgets/cards/additional_info/deck_color_identity.cpp
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
@@ -222,7 +208,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
@@ -239,7 +224,6 @@ set(cockatrice_SOURCES
src/interface/widgets/general/display/charts/bars/segmented_bar_widget.cpp
src/interface/widgets/general/display/charts/pies/color_pie.cpp
src/interface/widgets/general/home_styled_button.cpp
- src/interface/widgets/general/home_tab_button_color.h
src/interface/widgets/general/home_widget.cpp
src/interface/widgets/general/layout_containers/flow_widget.cpp
src/interface/widgets/general/layout_containers/overlap_control_widget.cpp
@@ -255,10 +239,6 @@ set(cockatrice_SOURCES
src/interface/widgets/printing_selector/printing_selector_card_selection_widget.cpp
src/interface/widgets/printing_selector/printing_selector_card_sorting_widget.cpp
src/interface/widgets/printing_selector/set_name_and_collectors_number_display_widget.cpp
- src/interface/widgets/playmat/playmat_collection_dialog.cpp
- src/interface/widgets/playmat/playmat_collection_dialog.h
- src/interface/widgets/playmat/playmat_preview_widget.cpp
- src/interface/widgets/playmat/playmat_settings_dialog.cpp
src/interface/widgets/quick_settings/settings_button_widget.cpp
src/interface/widgets/quick_settings/settings_popup_widget.cpp
src/interface/widgets/replay/replay_manager.cpp
@@ -280,7 +260,6 @@ set(cockatrice_SOURCES
src/interface/widgets/server/user/user_context_menu.cpp
src/interface/widgets/server/user/user_info_box.cpp
src/interface/widgets/server/user/user_info_connection.cpp
- src/interface/widgets/server/user/user_list_dialog.cpp
src/interface/widgets/server/user/user_list_manager.cpp
src/interface/widgets/server/user/user_list_painter.cpp
src/interface/widgets/server/user/user_list_panel_widget.cpp
@@ -298,8 +277,6 @@ set(cockatrice_SOURCES
src/interface/widgets/settings_page/user_interface_settings_page.cpp
src/interface/widgets/utility/custom_line_edit.cpp
src/interface/widgets/utility/get_text_with_max.cpp
- src/interface/widgets/utility/report_utils.cpp
- src/interface/widgets/utility/report_utils.h
src/interface/widgets/utility/sequence_edit.cpp
src/interface/widgets/utility/visibility_change_listener.cpp
src/interface/widgets/utility/visibility_change_listener.h
@@ -324,8 +301,6 @@ set(cockatrice_SOURCES
src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.cpp
src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_item_widget.cpp
src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp
- src/interface/widgets/visual_deck_storage/deck_preview/public_deck_preview_widget.cpp
- src/interface/widgets/visual_deck_storage/remote_public_decks_model.cpp
src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.cpp
src/interface/widgets/visual_deck_storage/visual_deck_storage_model.cpp
src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.cpp
@@ -391,14 +366,10 @@ set(cockatrice_SOURCES
src/interface/widgets/tabs/tab_card_art_rules.cpp
src/interface/widgets/tabs/tab_deck_editor.cpp
src/interface/widgets/tabs/tab_deck_storage.cpp
- src/interface/widgets/tabs/tab_developer.cpp
src/interface/widgets/tabs/tab_game.cpp
src/interface/widgets/tabs/tab_home.cpp
src/interface/widgets/tabs/tab_logs.cpp
src/interface/widgets/tabs/tab_message.cpp
- src/interface/widgets/tabs/tab_moderation.cpp
- src/interface/widgets/tabs/tab_public_decks.cpp
- src/interface/widgets/tabs/tab_report.cpp
src/interface/widgets/tabs/tab_replays.cpp
src/interface/widgets/tabs/tab_room.cpp
src/interface/widgets/tabs/tab_server.cpp
@@ -412,28 +383,6 @@ set(cockatrice_SOURCES
src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.cpp
src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp
src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp
- src/interface/widgets/onboarding/banner_shader_config.h
- src/interface/widgets/onboarding/brand_colors.h
- src/interface/widgets/onboarding/first_run_wizard.cpp
- src/interface/widgets/onboarding/first_run_wizard.h
- src/interface/widgets/onboarding/first_run_wizard_page.cpp
- src/interface/widgets/onboarding/first_run_wizard_page.h
- src/interface/widgets/onboarding/pages/account_setup_page.cpp
- src/interface/widgets/onboarding/pages/account_setup_page.h
- src/interface/widgets/onboarding/pages/card_database_setup_page.cpp
- src/interface/widgets/onboarding/pages/card_database_setup_page.h
- src/interface/widgets/onboarding/pages/finish_page.cpp
- src/interface/widgets/onboarding/pages/finish_page.h
- src/interface/widgets/onboarding/pages/preferences_setup_page.cpp
- src/interface/widgets/onboarding/pages/preferences_setup_page.h
- src/interface/widgets/onboarding/pages/theme_setup_page.cpp
- src/interface/widgets/onboarding/pages/theme_setup_page.h
- src/interface/widgets/onboarding/pages/welcome_page.cpp
- src/interface/widgets/onboarding/pages/welcome_page.h
- src/interface/widgets/onboarding/shader_banner_widget.cpp
- src/interface/widgets/onboarding/shader_banner_widget.h
- src/interface/widgets/onboarding/step_indicator_widget.cpp
- src/interface/widgets/onboarding/step_indicator_widget.h
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.cpp
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.h
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.cpp
@@ -454,8 +403,6 @@ set(cockatrice_SOURCES
src/interface/intents/intent_login.h
src/interface/intents/intent_open_server_room_by_name.cpp
src/interface/intents/intent_open_server_room_by_name.h
- src/interface/intents/intent_open_shared_deck.cpp
- src/interface/intents/intent_open_shared_deck.h
src/interface/intents/url_parser.cpp
src/interface/intents/url_parser.h
src/interface/widgets/server/user/user_info_popup.cpp
@@ -532,31 +479,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"
- PREFIX
- "/onboarding/shaders"
- BASE
- "src/interface/widgets/onboarding/shaders"
- FILES
- src/interface/widgets/onboarding/shaders/brand_banner.frag
- src/interface/widgets/onboarding/shaders/brand_plate.frag
-)
-
-qt6_add_resources(
- cockatrice
- "onboarding_qml"
- PREFIX
- "/onboarding/qml"
- BASE
- "src/interface/widgets/onboarding/qml"
- FILES
- src/interface/widgets/onboarding/qml/BrandBanner.qml
-)
-
target_link_libraries(
cockatrice
PUBLIC libcockatrice_card
@@ -668,35 +590,18 @@ if(WIN32)
set(qtconf_dest_dir .)
install(
- DIRECTORY "$/"
+ DIRECTORY "${CMAKE_BINARY_DIR}/${PROJECT_NAME}/${CMAKE_BUILD_TYPE}/"
DESTINATION ./
FILES_MATCHING
PATTERN "*.dll"
- PATTERN "*.pdb" EXCLUDE
- PATTERN "*.dir*" EXCLUDE
- PATTERN "*_autogen*" EXCLUDE
- PATTERN "*.tlog*" EXCLUDE
- PATTERN "CMakeFiles*" EXCLUDE
- PATTERN "x64*" EXCLUDE
- PATTERN ".qt*" EXCLUDE
- PATTERN ".qsb*" EXCLUDE
- PATTERN ".lupdate*" EXCLUDE
)
install(
DIRECTORY "${CMAKE_BINARY_DIR}/cockatrice/"
DESTINATION ./
FILES_MATCHING
+ PATTERN "CMakeFiles" EXCLUDE
PATTERN "*.ini"
- PATTERN "CMakeFiles*" EXCLUDE
- PATTERN "*.dir*" EXCLUDE
- PATTERN "*_autogen*" EXCLUDE
- PATTERN "*.tlog*" EXCLUDE
- PATTERN "*.pdb" EXCLUDE
- PATTERN "x64*" EXCLUDE
- PATTERN ".qt*" EXCLUDE
- PATTERN ".qsb*" EXCLUDE
- PATTERN ".lupdate*" EXCLUDE
)
# Qt plugins: audio, iconengines, imageformats, multimedia, platforms, printsupport, styles, tls
@@ -749,6 +654,10 @@ Data = Resources\")
"
COMPONENT Runtime
)
+
+ if(OPENSSL_FOUND)
+ install(FILES ${OPENSSL_INCLUDE_DIRS} DESTINATION ./)
+ endif()
endif()
if(Qt6LinguistTools_FOUND)
diff --git a/cockatrice/cockatrice.qrc b/cockatrice/cockatrice.qrc
index 14cf15b2f..9c34929b7 100644
--- a/cockatrice/cockatrice.qrc
+++ b/cockatrice/cockatrice.qrc
@@ -2,7 +2,6 @@
resources/cardback.svg
resources/cockatrice.svg
- resources/cockatrice-logo-white.svg
resources/hand.svg
resources/hr.jpg
@@ -63,8 +62,6 @@
resources/icons/mana/W.svg
resources/backgrounds/home.png
- resources/backgrounds/home-dark.png
- resources/backgrounds/home-light.png
resources/backgrounds/card_triplet.svg
resources/backgrounds/placeholder_printing_selector.svg
@@ -365,8 +362,6 @@
resources/usericons/pawn_single.svg
resources/usericons/pawn_double.svg
- resources/usericons/pawn_dev_single.svg
- resources/usericons/pawn_dev_double.svg
resources/usericons/pawn_donator_single.svg
resources/usericons/pawn_donator_double.svg
resources/usericons/pawn_judge_single.svg
diff --git a/cockatrice/resources/backgrounds/home-dark.png b/cockatrice/resources/backgrounds/home-dark.png
deleted file mode 100644
index 68f48e2c2..000000000
Binary files a/cockatrice/resources/backgrounds/home-dark.png and /dev/null differ
diff --git a/cockatrice/resources/backgrounds/home-light.png b/cockatrice/resources/backgrounds/home-light.png
deleted file mode 100644
index eaaaba932..000000000
Binary files a/cockatrice/resources/backgrounds/home-light.png and /dev/null differ
diff --git a/cockatrice/resources/backgrounds/home.png b/cockatrice/resources/backgrounds/home.png
index eaaaba932..68f48e2c2 100644
Binary files a/cockatrice/resources/backgrounds/home.png and b/cockatrice/resources/backgrounds/home.png differ
diff --git a/cockatrice/resources/cockatrice-logo-white.svg b/cockatrice/resources/cockatrice-logo-white.svg
deleted file mode 100644
index b3b31077f..000000000
--- a/cockatrice/resources/cockatrice-logo-white.svg
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
diff --git a/cockatrice/resources/cockatrice.svg b/cockatrice/resources/cockatrice.svg
index 89ba62dcf..d2e22da31 100644
--- a/cockatrice/resources/cockatrice.svg
+++ b/cockatrice/resources/cockatrice.svg
@@ -2,20 +2,20 @@
diff --git a/cockatrice/resources/countries/cn.svg b/cockatrice/resources/countries/cn.svg
index 45b608127..f510cf049 100644
--- a/cockatrice/resources/countries/cn.svg
+++ b/cockatrice/resources/countries/cn.svg
@@ -52,7 +52,8 @@
id="defs8">
-
+
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/resources/usericons/pawn_dev_double.svg b/cockatrice/resources/usericons/pawn_dev_double.svg
deleted file mode 100644
index 57ed5c2da..000000000
--- a/cockatrice/resources/usericons/pawn_dev_double.svg
+++ /dev/null
@@ -1,343 +0,0 @@
-
-
-
-
diff --git a/cockatrice/resources/usericons/pawn_dev_single.svg b/cockatrice/resources/usericons/pawn_dev_single.svg
deleted file mode 100644
index f7c4e7018..000000000
--- a/cockatrice/resources/usericons/pawn_dev_single.svg
+++ /dev/null
@@ -1,211 +0,0 @@
-
-
-
-
diff --git a/cockatrice/src/client/lag_monitor.cpp b/cockatrice/src/client/lag_monitor.cpp
deleted file mode 100644
index 383d64644..000000000
--- a/cockatrice/src/client/lag_monitor.cpp
+++ /dev/null
@@ -1,63 +0,0 @@
-#include "lag_monitor.h"
-
-#include
-#include
-#include
-
-LagMonitor::LagMonitor(QObject *parent) : QObject(parent)
-{
- qApp->installEventFilter(this);
-
- timer = new QTimer(this);
- timer->setInterval(TICK_INTERVAL_MS);
- connect(timer, &QTimer::timeout, this, &LagMonitor::checkTick);
- tickClock.start();
- timer->start();
-}
-
-QList LagMonitor::recentStalls() const
-{
- return stalls;
-}
-
-void LagMonitor::clearStalls()
-{
- stalls.clear();
-}
-
-bool LagMonitor::eventFilter(QObject *obj, QEvent *event)
-{
- if (event->type() == QEvent::ApplicationStateChange) {
- // The transition may span a suspend or an arbitrary unfocused period;
- // discard the gap so it cannot be mistaken for a stall.
- tickClock.restart();
- }
- return QObject::eventFilter(obj, event);
-}
-
-void LagMonitor::checkTick()
-{
- recordGap(tickClock.restart());
-}
-
-void LagMonitor::recordGap(qint64 gapMs)
-{
- if (gapMs <= STALL_THRESHOLD_MS) {
- return;
- }
-
- if (gapMs > MAX_PLAUSIBLE_STALL_MS) {
- qCDebug(LagMonitorLog, "Ignoring implausible %lld ms gap (likely suspend)", static_cast(gapMs));
- return;
- }
-
- const StallRecord record{.timestampMsSinceEpoch = QDateTime::currentMSecsSinceEpoch(), .durationMs = gapMs};
-
- stalls.append(record);
- while (stalls.size() > MAX_RECORDED_STALLS) {
- stalls.removeFirst();
- }
-
- qCWarning(LagMonitorLog, "Event loop stalled for %lld ms (threshold: %d ms)", static_cast(gapMs),
- STALL_THRESHOLD_MS);
-}
diff --git a/cockatrice/src/client/lag_monitor.h b/cockatrice/src/client/lag_monitor.h
deleted file mode 100644
index 9fdf6b283..000000000
--- a/cockatrice/src/client/lag_monitor.h
+++ /dev/null
@@ -1,87 +0,0 @@
-/**
- * @file lag_monitor.h
- * @ingroup Client
- */
-
-#ifndef LAG_MONITOR_H
-#define LAG_MONITOR_H
-
-#include
-#include
-#include
-#include
-#include
-
-inline Q_LOGGING_CATEGORY(LagMonitorLog, "lag_monitor");
-
-class QEvent;
-class QTimer;
-
-/**
- * @brief Detects main-thread event loop stalls ("UI freezes") from the inside.
- *
- * A timer is expected to fire every TICK_INTERVAL_MS of wall time. When the
- * observed gap greatly exceeds that interval, some other task blocked the
- * event loop for roughly the overshooting duration. This is what separates
- * "my client froze" from "the network is lagging" in user reports.
- *
- * Gaps that span an application state change (suspend, minimize, focus
- * loss) are discarded, and implausibly huge gaps are dropped, so operating
- * system power events do not fabricate stalls. This handling is load-bearing
- * on Windows, where the monotonic clock used by Qt counts sleep time.
- *
- * Healthy operation costs one timer wakeup per tick and two integer
- * comparisons. Allocations happen only when a stall is actually recorded.
- */
-class LagMonitor : public QObject
-{
- Q_OBJECT
-
-public:
- struct StallRecord
- {
- qint64 timestampMsSinceEpoch = 0; ///< when the stalled period ended
- qint64 durationMs = 0; ///< approximate length of the freeze; measured tick to tick, so it can exceed the true
- ///< stall by up to TICK_INTERVAL_MS
- };
-
- static constexpr int TICK_INTERVAL_MS = 500;
- static constexpr int STALL_THRESHOLD_MS = 2000;
- static constexpr int MAX_RECORDED_STALLS = 32;
-
- /// Gaps beyond this are treated as suspend artifacts rather than stalls.
- static constexpr qint64 MAX_PLAUSIBLE_STALL_MS = 600000;
-
- explicit LagMonitor(QObject *parent = nullptr);
-
- /**
- * @brief Stalls recorded during this session, oldest first.
- *
- * Intended consumers are log output and the diagnostics export. The list
- * holds at most MAX_RECORDED_STALLS entries.
- */
- QList recentStalls() const;
-
- void clearStalls();
-
- /**
- * @brief Feeds a measured tick-to-tick gap through the detection logic.
- *
- * Split out of checkTick so threshold, plausibility, and trim behavior
- * stay unit-testable without real timing.
- */
- void recordGap(qint64 gapMs);
-
-protected:
- bool eventFilter(QObject *obj, QEvent *event) override;
-
-private slots:
- void checkTick();
-
-private:
- QTimer *timer;
- QElapsedTimer tickClock; ///< monotonic clock, so wall clock steps do not fabricate stalls
- QList stalls;
-};
-
-#endif
diff --git a/cockatrice/src/client/latency_graph_widget.cpp b/cockatrice/src/client/latency_graph_widget.cpp
deleted file mode 100644
index 46d6ccdd3..000000000
--- a/cockatrice/src/client/latency_graph_widget.cpp
+++ /dev/null
@@ -1,51 +0,0 @@
-/**
- * @file latency_graph_widget.cpp
- * @ingroup Client
- */
-
-#include "latency_graph_widget.h"
-
-#include
-
-LatencyGraphWidget::LatencyGraphWidget(QWidget *parent) : QWidget(parent)
-{
-}
-
-void LatencyGraphWidget::setSamples(const QList &samplesMs)
-{
- samples = samplesMs;
- update();
-}
-
-void LatencyGraphWidget::paintEvent(QPaintEvent * /* event */)
-{
- if (samples.isEmpty()) {
- return;
- }
-
- QPainter painter(this);
-
- // Heights are relative to the window's own worst sample (floored at
- // MinScaleMs) so the shape of the variance stays readable even when every
- // value is small.
- qint64 heightScaleMs = MinScaleMs;
- for (int sample : samples) {
- heightScaleMs = qMax(heightScaleMs, static_cast(sample));
- }
-
- const qreal widthPerBar = static_cast(width()) / samples.size();
- for (int i = 0; i < samples.size(); ++i) {
- const qreal heightRatio = qBound(0.0, static_cast(samples.at(i)) / heightScaleMs, 1.0);
- const qreal barHeight = heightRatio * height();
-
- // Colors follow an absolute quality ramp: a steady good ping stays
- // green no matter how uniform the window is.
- const qreal colorRatio = qBound(0.0, static_cast(samples.at(i)) / ColorScaleMs, 1.0);
- QColor color;
- color.setHsv(qRound(120.0 * (1.0 - colorRatio)), 255, 255);
-
- const QRectF bar(static_cast(i) * widthPerBar + 1.0, static_cast(height()) - barHeight,
- qMax(1.0, widthPerBar - 2.0), barHeight);
- painter.fillRect(bar, color);
- }
-}
diff --git a/cockatrice/src/client/latency_graph_widget.h b/cockatrice/src/client/latency_graph_widget.h
deleted file mode 100644
index 4f6f38f8b..000000000
--- a/cockatrice/src/client/latency_graph_widget.h
+++ /dev/null
@@ -1,43 +0,0 @@
-/**
- * @file latency_graph_widget.h
- * @ingroup Client
- */
-
-#ifndef LATENCY_GRAPH_WIDGET_H
-#define LATENCY_GRAPH_WIDGET_H
-
-#include
-#include
-
-/**
- * @brief Bar graph of recent network round-trip samples.
- *
- * Draws one bar per sample, oldest on the left. Bar height is relative to the
- * window's own scale so the shape of the variance stays readable, while bar
- * color maps each sample onto an absolute quality ramp (green at rest through
- * red at ColorScaleMs) so a steady good ping never looks alarming. Size
- * agnostic: the status bar embeds a small instance while the latency detail
- * popup shows a large one.
- */
-class LatencyGraphWidget : public QWidget
-{
- Q_OBJECT
-public:
- explicit LatencyGraphWidget(QWidget *parent = nullptr);
-
- /// Sample in milliseconds that maps to a fully red bar.
- static constexpr qint64 ColorScaleMs = 500;
-
- void setSamples(const QList &samplesMs);
-
-protected:
- void paintEvent(QPaintEvent *event) override;
-
-private:
- /// Floor of the vertical scale in milliseconds. Keeps small windows readable.
- static constexpr qint64 MinScaleMs = 100;
-
- QList samples;
-};
-
-#endif
diff --git a/cockatrice/src/client/latency_status_widget.cpp b/cockatrice/src/client/latency_status_widget.cpp
deleted file mode 100644
index 779c726eb..000000000
--- a/cockatrice/src/client/latency_status_widget.cpp
+++ /dev/null
@@ -1,111 +0,0 @@
-/**
- * @file latency_status_widget.cpp
- * @ingroup Client
- */
-
-#include "latency_status_widget.h"
-
-#include "latency_graph_widget.h"
-
-#include
-#include
-#include
-#include
-
-LatencyStatusWidget::LatencyStatusWidget(QWidget *parent) : QWidget(parent)
-{
- pingLabel = new QLabel(this);
- pingLabel->setAccessibleName(tr("Ping"));
-
- latencyGraph = new LatencyGraphWidget(this);
- latencyGraph->setFixedSize(90, 14);
-
- auto *layout = new QHBoxLayout(this);
- layout->setContentsMargins(0, 0, 0, 0);
- layout->setSpacing(4);
- layout->addWidget(latencyGraph);
- layout->addWidget(pingLabel);
-
- // Clicking anywhere in the area opens the detail view.
- for (QObject *child : QList{pingLabel, latencyGraph}) {
- child->installEventFilter(this);
- }
- setCursor(Qt::PointingHandCursor);
-
- hide();
-}
-
-void LatencyStatusWidget::updateData(const LatencyTracker::Stats &stats, const QList &samplesMs)
-{
- latestSamples = samplesMs;
- latencyGraph->setSamples(samplesMs);
- if (popup && popup->isVisible() && detailGraph) {
- detailGraph->setSamples(samplesMs);
- }
-
- if (stats.sampleCount == 0) {
- hide();
- return;
- }
-
- const QString statsStr = statsText(stats);
-
- pingLabel->setText(tr("Ping: %1 ms").arg(stats.lastMs));
- pingLabel->setToolTip(statsStr);
- pingLabel->setAccessibleDescription(statsStr);
- if (popup && popup->isVisible() && detailLabel) {
- detailLabel->setText(statsStr);
- }
- show();
-}
-
-bool LatencyStatusWidget::eventFilter(QObject *watched, QEvent *event)
-{
- if ((watched == pingLabel || watched == latencyGraph) && event->type() == QEvent::MouseButtonPress) {
- togglePopup();
- return true;
- }
- return QWidget::eventFilter(watched, event);
-}
-
-void LatencyStatusWidget::togglePopup()
-{
- if (!popup) {
- popup = new QWidget(this, Qt::Popup | Qt::FramelessWindowHint);
- auto *layout = new QVBoxLayout(popup);
- layout->setContentsMargins(8, 8, 8, 8);
-
- detailLabel = new QLabel(popup);
- detailLabel->setAccessibleName(tr("Connection latency details"));
- detailLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
-
- detailGraph = new LatencyGraphWidget(popup);
- detailGraph->setFixedSize(280, 80);
-
- layout->addWidget(detailLabel, 0, Qt::AlignLeft);
- layout->addWidget(detailGraph, 0, Qt::AlignHCenter);
- }
-
- if (popup->isVisible()) {
- popup->hide();
- return;
- }
-
- // Qt::Popup closes itself on any outside click, so just position and show.
- if (latestSamples.isEmpty()) {
- return;
- }
- detailGraph->setSamples(latestSamples);
- detailLabel->setText(pingLabel->toolTip());
- popup->adjustSize();
- const QPoint anchor = mapToGlobal(QPoint(width() / 2, 0));
- popup->move(anchor.x() - popup->width() / 2, anchor.y() - popup->height() - 6);
- popup->show();
-}
-
-QString LatencyStatusWidget::statsText(const LatencyTracker::Stats &stats) const
-{
- return tr("Connection quality over the last %n sample(s):", "", stats.sampleCount) + "\n" +
- tr("Last: %1 ms").arg(stats.lastMs) + "\n" + tr("Median: %1 ms").arg(stats.medianMs) + "\n" +
- tr("95th percentile: %1 ms").arg(stats.p95Ms) + "\n" + tr("Maximum: %1 ms").arg(stats.maxMs);
-}
diff --git a/cockatrice/src/client/latency_status_widget.h b/cockatrice/src/client/latency_status_widget.h
deleted file mode 100644
index d9e1d130c..000000000
--- a/cockatrice/src/client/latency_status_widget.h
+++ /dev/null
@@ -1,50 +0,0 @@
-/**
- * @file latency_status_widget.h
- * @ingroup Client
- */
-
-#ifndef LATENCY_STATUS_WIDGET_H
-#define LATENCY_STATUS_WIDGET_H
-
-#include
-#include
-#include
-
-class QLabel;
-class LatencyGraphWidget;
-
-/**
- * @brief Status bar presentation of server round-trip health.
- *
- * Combines the textual "Ping" readout with a small LatencyGraphWidget
- * sparkline of the rolling sample window. Clicking anywhere in the area opens
- * a popup with a larger graph and the numeric statistics. It closes on any
- * outside click. Stays hidden while disconnected or before any samples exist.
- * Owns all latency display state so MainWindow only needs to forward one
- * signal here.
- */
-class LatencyStatusWidget : public QWidget
-{
- Q_OBJECT
-public:
- explicit LatencyStatusWidget(QWidget *parent = nullptr);
-
-public slots:
- void updateData(const LatencyTracker::Stats &stats, const QList &samplesMs);
-
-protected:
- bool eventFilter(QObject *watched, QEvent *event) override;
-
-private:
- void togglePopup();
- QString statsText(const LatencyTracker::Stats &stats) const;
-
- QLabel *pingLabel = nullptr;
- LatencyGraphWidget *latencyGraph = nullptr;
- QWidget *popup = nullptr;
- LatencyGraphWidget *detailGraph = nullptr;
- QLabel *detailLabel = nullptr;
- QList latestSamples;
-};
-
-#endif
diff --git a/cockatrice/src/client/network/connection_controller/remote_connection_controller.cpp b/cockatrice/src/client/network/connection_controller/remote_connection_controller.cpp
index c1598bd25..4e425fb66 100644
--- a/cockatrice/src/client/network/connection_controller/remote_connection_controller.cpp
+++ b/cockatrice/src/client/network/connection_controller/remote_connection_controller.cpp
@@ -1,6 +1,5 @@
#include "remote_connection_controller.h"
-#include "../../../interface/pixel_map_generator.h"
#include "../../settings/cache_settings.h"
#include "../interface/widgets/dialogs/dlg_connect.h"
#include "../interface/widgets/dialogs/dlg_forgot_password_challenge.h"
@@ -45,8 +44,6 @@ void ConnectionController::wireClientSignals()
connect(remoteClient, &RemoteClient::statusChanged, this, &ConnectionController::onStatusChanged);
- connect(remoteClient, &AbstractClient::pingStatsUpdated, this, &ConnectionController::pingStatsUpdated);
-
connect(remoteClient, &RemoteClient::userInfoChanged, this, &ConnectionController::onUserInfoReceived,
Qt::BlockingQueuedConnection);
@@ -181,7 +178,7 @@ void ConnectionController::onServerShutdownEvent(const Event_ServerShutdown &eve
"games will be lost.\nReason for shutdown: %1",
"", event.minutes())
.arg(QString::fromStdString(event.reason())));
- serverShutdownMessageBox.setIconPixmap(themePixmap(QStringLiteral("cockatrice")).scaled(64, 64));
+ serverShutdownMessageBox.setIconPixmap(QPixmap("theme:cockatrice").scaled(64, 64));
serverShutdownMessageBox.setText(tr("Scheduled server shutdown"));
serverShutdownMessageBox.setWindowModality(Qt::ApplicationModal);
serverShutdownMessageBox.setVisible(true);
@@ -299,15 +296,6 @@ void ConnectionController::onLoginError(int r,
return;
}
- case Response::RespPasswordChangeRequired: {
- QMessageBox::information(
- dialogParent, tr("Password Change Required"),
- tr("An administrator has reset your password. Please contact your server administrator to obtain "
- "your temporary password, then log in and change it via Account -> Change Password."));
- remoteClient->disconnectFromServer();
- return;
- }
-
case Response::RespServerFull: {
QMessageBox::critical(dialogParent, tr("Server Full"),
tr("The server has reached its maximum user capacity, please check back later."));
diff --git a/cockatrice/src/client/network/connection_controller/remote_connection_controller.h b/cockatrice/src/client/network/connection_controller/remote_connection_controller.h
index bae99a3e0..7486bc81a 100644
--- a/cockatrice/src/client/network/connection_controller/remote_connection_controller.h
+++ b/cockatrice/src/client/network/connection_controller/remote_connection_controller.h
@@ -54,10 +54,6 @@ signals:
// action enable/disable logic
void statusChanged(ClientStatus status);
- // Forwarded from AbstractClient::pingStatsUpdated. See that signal for the
- // meaning of the parameters.
- void pingStatsUpdated(const LatencyTracker::Stats &stats, const QList &samplesMs);
-
private slots:
// Slots wired directly to RemoteClient signals
void onStatusChanged(ClientStatus status);
diff --git a/cockatrice/src/client/network/interfaces/deck_stats_interface.cpp b/cockatrice/src/client/network/interfaces/deck_stats_interface.cpp
index 42292b2aa..8689a19e9 100644
--- a/cockatrice/src/client/network/interfaces/deck_stats_interface.cpp
+++ b/cockatrice/src/client/network/interfaces/deck_stats_interface.cpp
@@ -72,7 +72,7 @@ void DeckStatsInterface::copyDeckWithoutTokens(const DeckList &source, DeckList
{
auto copyIfNotAToken = [&destination](const auto node, const auto card) {
CardInfoPtr dbCard = CardDatabaseManager::query()->getCardInfo(card->getName());
- if (dbCard && !dbCard->getIsToken() && node->getName() != DECK_ZONE_MAYBEBOARD) {
+ if (dbCard && !dbCard->getIsToken()) {
DecklistCardNode *addedCard = destination.addCard(card->getName(), node->getName(), -1);
addedCard->setNumber(card->getNumber());
}
diff --git a/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp b/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp
index 627b7fe34..5dc77fa2c 100644
--- a/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp
+++ b/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp
@@ -99,7 +99,7 @@ void TappedOutInterface::copyDeckSplitMainAndSide(const DeckList &source, DeckLi
{
auto copyMainOrSide = [&mainboard, &sideboard](const auto node, const auto card) {
CardInfoPtr dbCard = CardDatabaseManager::query()->getCardInfo(card->getName());
- if (!dbCard || dbCard->getIsToken() || node->getName() == DECK_ZONE_MAYBEBOARD) {
+ if (!dbCard || dbCard->getIsToken()) {
return;
}
diff --git a/cockatrice/src/client/settings/cache_settings.h b/cockatrice/src/client/settings/cache_settings.h
index 7a90d82f1..23cdb4dbf 100644
--- a/cockatrice/src/client/settings/cache_settings.h
+++ b/cockatrice/src/client/settings/cache_settings.h
@@ -58,33 +58,33 @@ signals:
void themeChanged();
private:
- QSettings *settings = nullptr;
- ShortcutsSettings *shortcutsSettings = nullptr;
- CardDatabaseSettings *cardDatabaseSettings = nullptr;
- ServersSettings *serversSettings = nullptr;
- MessageSettings *messageSettings = nullptr;
- GameFiltersSettings *gameFiltersSettings = nullptr;
- LayoutsSettings *layoutsSettings = nullptr;
- DownloadSettings *downloadSettings = nullptr;
- RecentsSettings *recentsSettings = nullptr;
- CardOverrideSettings *cardOverrideSettings = nullptr;
- DebugSettings *debugSettings = nullptr;
- CardCounterSettings *cardCounterSettings = nullptr;
- TabsSettings *tabsSettings = nullptr;
- SoundSettings *soundSettings = nullptr;
- GameSettings *gameSettings = nullptr;
- ChatSettings *chatSettings = nullptr;
- CacheStorageSettings *cacheStorageSettings = nullptr;
- UpdatesSettings *updatesSettings = nullptr;
- PersonalSettings *personalSettings = nullptr;
- CardsDisplaySettings *cardsDisplaySettings = nullptr;
- InterfaceSettings *interfaceSettings = nullptr;
- DeckEditorSettings *deckEditorSettings = nullptr;
- PathsSettings *pathsSettings = nullptr;
- VisualDeckStorageSettings *visualDeckStorageSettings = nullptr;
- AppearanceSettings *appearanceSettings = nullptr;
- NetworkSettings *networkSettings = nullptr;
- CommanderBracketSettings *commanderBracketSettings = nullptr;
+ QSettings *settings;
+ ShortcutsSettings *shortcutsSettings;
+ CardDatabaseSettings *cardDatabaseSettings;
+ ServersSettings *serversSettings;
+ MessageSettings *messageSettings;
+ GameFiltersSettings *gameFiltersSettings;
+ LayoutsSettings *layoutsSettings;
+ DownloadSettings *downloadSettings;
+ RecentsSettings *recentsSettings;
+ CardOverrideSettings *cardOverrideSettings;
+ DebugSettings *debugSettings;
+ CardCounterSettings *cardCounterSettings;
+ TabsSettings *tabsSettings;
+ SoundSettings *soundSettings;
+ GameSettings *gameSettings;
+ ChatSettings *chatSettings;
+ CacheStorageSettings *cacheStorageSettings;
+ UpdatesSettings *updatesSettings;
+ PersonalSettings *personalSettings;
+ CardsDisplaySettings *cardsDisplaySettings;
+ InterfaceSettings *interfaceSettings;
+ DeckEditorSettings *deckEditorSettings;
+ PathsSettings *pathsSettings;
+ VisualDeckStorageSettings *visualDeckStorageSettings;
+ AppearanceSettings *appearanceSettings;
+ NetworkSettings *networkSettings;
+ CommanderBracketSettings *commanderBracketSettings;
QString themeName;
diff --git a/cockatrice/src/client/settings/shortcuts_settings.h b/cockatrice/src/client/settings/shortcuts_settings.h
index f4ebc204e..95155b8d1 100644
--- a/cockatrice/src/client/settings/shortcuts_settings.h
+++ b/cockatrice/src/client/settings/shortcuts_settings.h
@@ -786,10 +786,6 @@ private:
ShortcutGroup::Tabs)},
{"Tabs/aTabLogs",
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Logs"), parseSequenceString(""), ShortcutGroup::Tabs)},
- {"Tabs/aTabReport",
- ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Report Queue"), parseSequenceString(""), ShortcutGroup::Tabs)},
- {"Tabs/aTabModeration",
- ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Moderation"), parseSequenceString(""), ShortcutGroup::Tabs)},
};
};
diff --git a/cockatrice/src/client/sound_engine.cpp b/cockatrice/src/client/sound_engine.cpp
index 96cafa3d3..18de2264d 100644
--- a/cockatrice/src/client/sound_engine.cpp
+++ b/cockatrice/src/client/sound_engine.cpp
@@ -94,7 +94,7 @@ QStringMap &SoundEngine::getAvailableThemes()
QDir dir;
availableThemes.clear();
- // Load themes from user profile dir
+ // load themes from user profile dir
dir.setPath(SettingsCache::instance().getDataPath() + "/sounds");
@@ -104,7 +104,7 @@ QStringMap &SoundEngine::getAvailableThemes()
}
}
- // Load themes from Cockatrice system dir
+ // load themes from cockatrice system dir
dir.setPath(qApp->applicationDirPath() +
#ifdef Q_OS_MAC
"/../Resources/sounds"
diff --git a/cockatrice/src/filters/deck_filter_string.cpp b/cockatrice/src/filters/deck_filter_string.cpp
index a2b7519a8..4abb8210c 100644
--- a/cockatrice/src/filters/deck_filter_string.cpp
+++ b/cockatrice/src/filters/deck_filter_string.cpp
@@ -43,12 +43,6 @@ NumericValue <- [0-9]+
static std::once_flag init;
-// The peglib parser is a single permanent object, so the rule actions below cannot see
-// per-instance state. The card language that the nested [[card name]] search matches
-// against is passed through this thread-local context, which is live only while a
-// DeckFilterString is being parsed, and copied into the nested FilterString closures.
-thread_local CardSearchLanguage deckSearchLanguageContext;
-
static void setupParserRules()
{
// plumbing
@@ -122,7 +116,7 @@ static void setupParserRules()
// actual functionality
search["DeckContentQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
- auto cardFilter = FilterString(std::any_cast(sv[0]), deckSearchLanguageContext);
+ auto cardFilter = FilterString(std::any_cast(sv[0]));
auto numberMatcher = sv.size() > 1 ? std::any_cast(sv[1]) : [](int count) { return count > 0; };
return [=](const DeckSearchData &data) -> bool {
@@ -192,7 +186,7 @@ DeckFilterString::DeckFilterString()
_error = "Not initialized";
}
-DeckFilterString::DeckFilterString(const QString &expr, const CardSearchLanguage &searchLanguage)
+DeckFilterString::DeckFilterString(const QString &expr)
{
QByteArray ba = expr.simplified().toUtf8();
@@ -205,8 +199,6 @@ DeckFilterString::DeckFilterString(const QString &expr, const CardSearchLanguage
return;
}
- deckSearchLanguageContext = searchLanguage;
-
search.set_logger([&](size_t /*ln*/, size_t col, const std::string &msg) {
_error = QString("Error at position %1: %2").arg(col).arg(QString::fromStdString(msg));
});
diff --git a/cockatrice/src/filters/deck_filter_string.h b/cockatrice/src/filters/deck_filter_string.h
index 5b1419004..90a6a17eb 100644
--- a/cockatrice/src/filters/deck_filter_string.h
+++ b/cockatrice/src/filters/deck_filter_string.h
@@ -12,7 +12,6 @@
#include
#include
#include
-#include
inline Q_LOGGING_CATEGORY(DeckFilterStringLog, "deck_filter_string");
@@ -36,7 +35,7 @@ class DeckFilterString
{
public:
DeckFilterString();
- explicit DeckFilterString(const QString &expr, const CardSearchLanguage &searchLanguage = {});
+ explicit DeckFilterString(const QString &expr);
bool check(const DeckSearchData &data) const
{
return filter(data);
diff --git a/cockatrice/src/filters/filter_builder.cpp b/cockatrice/src/filters/filter_builder.cpp
index f109fbcd3..785f753e7 100644
--- a/cockatrice/src/filters/filter_builder.cpp
+++ b/cockatrice/src/filters/filter_builder.cpp
@@ -1,6 +1,5 @@
#include "filter_builder.h"
-#include "../interface/pixel_map_generator.h"
#include "../interface/widgets/utility/custom_line_edit.h"
#include
@@ -22,7 +21,7 @@ FilterBuilder::FilterBuilder(QWidget *parent) : QWidget(parent)
typeCombo->addItem(CardFilter::typeName(static_cast(i)), QVariant(i));
}
- QPushButton *ok = new QPushButton(themePixmap(QStringLiteral("icons/increment")), QString());
+ QPushButton *ok = new QPushButton(QPixmap("theme:icons/increment"), QString());
ok->setObjectName("ok");
ok->setMaximumSize(20, 20);
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..95460011f 100644
--- a/cockatrice/src/game/game_event_handler.cpp
+++ b/cockatrice/src/game/game_event_handler.cpp
@@ -285,10 +285,6 @@ void GameEventHandler::eventGameStateChanged(const Event_GameStateChanged &event
emit playerJoined(prop);
}
player->processPlayerInfo(playerInfo);
- // Extract playmat from player properties for opponent display
- if (prop.has_playmat_params()) {
- player->setPlaymatFromProperties(prop);
- }
if (player->getPlayerInfo()->getLocal()) {
emit localPlayerDeckSelected(player, playerId, playerInfo);
} else {
@@ -355,11 +351,6 @@ void GameEventHandler::eventPlayerPropertiesChanged(const Event_PlayerProperties
const ServerInfo_PlayerProperties &prop = event.player_properties();
emit playerPropertiesChanged(prop, eventPlayerId);
- // Update playmat from player properties
- if (prop.has_playmat_params()) {
- player->setPlaymatFromProperties(prop);
- }
-
const auto contextType = static_cast(getPbExtension(context));
switch (contextType) {
case GameEventContext::READY_START: {
@@ -430,13 +421,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..485e2fc5c 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()) {
@@ -258,22 +250,6 @@ void PlayerLogic::setDeck(const DeckList &_deck)
emit deckChanged();
}
-void PlayerLogic::setPlaymatFromProperties(const ServerInfo_PlayerProperties &props)
-{
- if (props.has_playmat_params() && !props.playmat_params().card_name().empty()) {
- const auto &pp = props.playmat_params();
- remotePlaymatCard = {QString::fromStdString(pp.card_name()), QString::fromStdString(pp.card_provider_id())};
- remotePlaymatParams = {qBound(0.0, pp.margin_pct_l(), 0.95), qBound(0.0, pp.margin_pct_r(), 0.95),
- qBound(0.0, pp.vertical_offset(), 1.0), qBound(0.1, pp.zoom(), 4.0)};
- hasRemotePlaymat = true;
- } else {
- remotePlaymatCard = CardRef{};
- remotePlaymatParams = PlaymatParams{};
- hasRemotePlaymat = false;
- }
- emit playmatChanged();
-}
-
CounterState *PlayerLogic::addCounter(const ServerInfo_Counter &counter)
{
return addCounter(counter.id(), QString::fromStdString(counter.name()),
diff --git a/cockatrice/src/game/player/player_logic.h b/cockatrice/src/game/player/player_logic.h
index 6923b3afe..a89cb6eed 100644
--- a/cockatrice/src/game/player/player_logic.h
+++ b/cockatrice/src/game/player/player_logic.h
@@ -17,7 +17,6 @@
#include "../zones/table_zone_logic.h"
#include "player_event_handler.h"
#include "player_info.h"
-#include "player_manager.h"
#include
#include
@@ -73,8 +72,6 @@ signals:
const QList &cardList,
bool withWritePermission);
void deckChanged();
- /** @brief Emitted when the remote playmat (card/params) is updated from player properties. */
- void playmatChanged();
void newCardAdded(AbstractCardItem *card);
void requestCardMenuUpdate(const CardItem *card);
void counterAdded(CounterState *state);
@@ -229,20 +226,6 @@ public:
void setZoneId(int _zoneId);
- void setPlaymatFromProperties(const ServerInfo_PlayerProperties &props);
- const CardRef &getRemotePlaymatCard() const
- {
- return remotePlaymatCard;
- }
- const PlaymatParams &getRemotePlaymatParams() const
- {
- return remotePlaymatParams;
- }
- bool getHasRemotePlaymat() const
- {
- return hasRemotePlaymat;
- }
-
private:
AbstractGame *game;
PlayerInfo *playerInfo;
@@ -260,11 +243,6 @@ private:
bool dialogSemaphore;
QList cardsToDelete;
-
- // Playmat from player properties (for opponent display)
- CardRef remotePlaymatCard;
- PlaymatParams remotePlaymatParams;
- bool hasRemotePlaymat = false;
};
class AnnotationDialog : public QInputDialog
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_card_item.cpp b/cockatrice/src/game_graphics/board/abstract_card_item.cpp
index 3969b7d03..1410d0c80 100644
--- a/cockatrice/src/game_graphics/board/abstract_card_item.cpp
+++ b/cockatrice/src/game_graphics/board/abstract_card_item.cpp
@@ -1,7 +1,6 @@
#include "abstract_card_item.h"
#include "../../client/settings/cache_settings.h"
-#include "../../interface/card_localization.h"
#include "../../interface/card_picture_loader/card_picture_loader.h"
#include "../game_scene.h"
#include "../z_values.h"
@@ -27,8 +26,6 @@ AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::displayCardNamesChanged, this,
[this] { update(); });
- connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this,
- [this] { update(); });
refreshCardInfo();
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::roundCardCornersChanged, this,
@@ -174,7 +171,7 @@ void AbstractCardItem::paintPicture(QPainter *painter, const QSizeF &translatedS
if (SettingsCache::instance().debug().getShowCardId()) {
prefix = "#" + QString::number(id) + " ";
}
- nameStr = prefix + CardLocalization::displayName(getCardInfo());
+ nameStr = prefix + cardRef.name;
}
painter->drawText(QRectF(3 * scaleFactor, 3 * scaleFactor, translatedSize.width() - 6 * scaleFactor,
translatedSize.height() - 6 * scaleFactor),
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/arrow_item.h b/cockatrice/src/game_graphics/board/arrow_item.h
index 76a2d5d6c..21f991b77 100644
--- a/cockatrice/src/game_graphics/board/arrow_item.h
+++ b/cockatrice/src/game_graphics/board/arrow_item.h
@@ -4,7 +4,6 @@
#include "../../game/board/arrow_data.h"
#include "../animated_item.h"
#include "arrow_target.h"
-#include "graphics_item_type.h"
#include
#include
@@ -49,14 +48,6 @@ protected:
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
public:
- enum
- {
- Type = typeArrow
- };
- [[nodiscard]] int type() const override
- {
- return Type;
- }
ArrowItem(QSharedPointer _data, ArrowTarget *_startItem, ArrowTarget *_targetItem);
~ArrowItem() 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/board/graphics_item_type.h b/cockatrice/src/game_graphics/board/graphics_item_type.h
index afac7881f..7eac132b0 100644
--- a/cockatrice/src/game_graphics/board/graphics_item_type.h
+++ b/cockatrice/src/game_graphics/board/graphics_item_type.h
@@ -16,8 +16,7 @@ enum GraphicsItemType
typeZone = QGraphicsItem::UserType + 3,
typePlayerTarget = QGraphicsItem::UserType + 4,
typeDeckViewCardContainer = QGraphicsItem::UserType + 5,
- typeOther = QGraphicsItem::UserType + 6,
- typeArrow = QGraphicsItem::UserType + 7
+ typeOther = QGraphicsItem::UserType + 6
};
#endif // COCKATRICE_GRAPHICS_ITEM_TYPE_H
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/deckview/deck_view_container.cpp b/cockatrice/src/game_graphics/deckview/deck_view_container.cpp
index bc07ac183..69941040f 100644
--- a/cockatrice/src/game_graphics/deckview/deck_view_container.cpp
+++ b/cockatrice/src/game_graphics/deckview/deck_view_container.cpp
@@ -9,21 +9,17 @@
#include "../../interface/widgets/dialogs/dlg_load_deck_from_website.h"
#include "../../interface/widgets/dialogs/dlg_load_remote_deck.h"
#include "../../interface/widgets/tabs/tab_game.h"
-#include "../../interface/widgets/visual_deck_storage/visual_deck_storage_widget.h"
#include "deck_view.h"
#include
#include
#include
-#include
#include
#include
-#include
#include
#include
#include
#include
-#include
#include
#include
@@ -104,9 +100,6 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent)
connect(&SettingsCache::instance().visualDeckStorage(), &VisualDeckStorageSettings::visualDeckStorageInGameChanged,
this, &DeckViewContainer::setVisualDeckStorageExists);
- connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::playmatSettingsChanged, this,
- &DeckViewContainer::onPlaymatSettingsChanged);
-
switchToDeckSelectView();
}
@@ -284,8 +277,6 @@ void DeckViewContainer::loadDeckFromFile(const QString &filePath)
void DeckViewContainer::loadDeckFromDeckList(const DeckList &deck)
{
- currentDeck = deck;
-
QString deckString = deck.writeToString_Native();
if (deckString.length() > MAX_FILE_LENGTH) {
@@ -298,52 +289,6 @@ void DeckViewContainer::loadDeckFromDeckList(const DeckList &deck)
PendingCommand *pend = parentGame->getGame()->getGameEventHandler()->prepareGameCommand(cmd);
connect(pend, &PendingCommand::finished, this, &DeckViewContainer::deckSelectFinished);
parentGame->getGame()->getGameEventHandler()->sendGameCommand(pend, playerId);
-
- resolveAndSendPlaymat();
-}
-
-void DeckViewContainer::resolveAndSendPlaymat()
-{
- if (currentDeck.getCardRefList().isEmpty() && currentDeck.getPlaymat().card.isEmpty()) {
- return;
- }
-
- const auto &settings = SettingsCache::instance().userInterface();
- const auto fallbackBehavior = static_cast(settings.getPlaymatFallbackBehavior());
-
- QList fallbackList = settings.getPlaymatFallbackList();
-
- // In random mode with 2+ entries, remove the last-resolved mat to avoid repeats.
- if (fallbackBehavior == PlaymatFallbackModeRandom && fallbackList.size() > 1) {
- fallbackList.removeAll(lastResolvedPlaymat);
- }
-
- const PlaymatInfo resolved =
- resolvePlaymatForDeck(currentDeck, fallbackList, static_cast(settings.getPlaymatMode()),
- fallbackBehavior, playmatRotationIndex);
-
- lastResolvedPlaymat = resolved;
-
- Command_SetPlaymat playmatCmd;
- auto *pp = playmatCmd.mutable_playmat_params();
- pp->set_card_name(resolved.card.name.toStdString());
- pp->set_card_provider_id(resolved.card.providerId.toStdString());
- pp->set_margin_pct_l(resolved.params.marginPctL);
- pp->set_margin_pct_r(resolved.params.marginPctR);
- pp->set_vertical_offset(resolved.params.verticalOffset);
- pp->set_zoom(resolved.params.zoom);
- PendingCommand *playmatPend = parentGame->getGame()->getGameEventHandler()->prepareGameCommand(playmatCmd);
- parentGame->getGame()->getGameEventHandler()->sendGameCommand(playmatPend, playerId);
-}
-
-void DeckViewContainer::onPlaymatSettingsChanged()
-{
- resolveAndSendPlaymat();
-}
-
-void DeckViewContainer::advancePlaymatRotation()
-{
- playmatRotationIndex++;
}
void DeckViewContainer::loadRemoteDeck()
@@ -434,10 +379,6 @@ void DeckViewContainer::sideboardPlanChanged()
*/
void DeckViewContainer::sendReadyStartCommand(bool ready)
{
- if (ready) {
- resolveAndSendPlaymat();
- }
-
Command_ReadyStart cmd;
cmd.set_ready(ready);
parentGame->getGame()->getGameEventHandler()->sendGameCommand(cmd, playerId);
@@ -475,7 +416,6 @@ void DeckViewContainer::setSideboardLocked(bool locked)
void DeckViewContainer::setDeck(const DeckList &deck)
{
- currentDeck = deck;
deckView->setDeck(deck);
switchToDeckLoadedView();
}
\ No newline at end of file
diff --git a/cockatrice/src/game_graphics/deckview/deck_view_container.h b/cockatrice/src/game_graphics/deckview/deck_view_container.h
index b5317c39a..ec024bace 100644
--- a/cockatrice/src/game_graphics/deckview/deck_view_container.h
+++ b/cockatrice/src/game_graphics/deckview/deck_view_container.h
@@ -57,9 +57,6 @@ private:
VisualDeckStorageWidget *visualDeckStorageWidget;
TabGame *parentGame;
int playerId;
- int playmatRotationIndex = 0; ///< Per-match cursor for round-robin playmat mode.
- DeckList currentDeck; ///< Cached deck for live settings re-resolution.
- PlaymatInfo lastResolvedPlaymat; ///< Tracks last sent playmat to avoid repeats in random mode.
void tryCreateVisualDeckStorageWidget();
void sendReadyStartCommand(bool ready);
@@ -78,7 +75,6 @@ private slots:
void sideboardLockButtonClicked();
void updateSideboardLockButtonText();
void refreshShortcuts();
- void onPlaymatSettingsChanged();
signals:
void newCardAdded(AbstractCardItem *card);
void notIdle();
@@ -91,8 +87,6 @@ public:
void setSideboardLocked(bool locked);
void setDeck(const DeckList &deck);
void setVisualDeckStorageExists(bool exists);
- void advancePlaymatRotation();
- void resolveAndSendPlaymat();
public slots:
void loadDeckFromFile(const QString &filePath);
diff --git a/cockatrice/src/game_graphics/dialogs/dlg_create_token.cpp b/cockatrice/src/game_graphics/dialogs/dlg_create_token.cpp
index 6d8ad0534..b311d2ebd 100644
--- a/cockatrice/src/game_graphics/dialogs/dlg_create_token.cpp
+++ b/cockatrice/src/game_graphics/dialogs/dlg_create_token.cpp
@@ -16,13 +16,11 @@
#include
#include
#include
-#include
#include
#include
#include
#include
#include
-#include
#include
#include
#include
@@ -90,17 +88,6 @@ DlgCreateToken::DlgCreateToken(const QStringList &_predefinedTokens, QWidget *pa
cardDatabaseDisplayModel = new TokenDisplayModel(this);
cardDatabaseDisplayModel->setSourceModel(cardDatabaseModel);
- const auto applyCardSearchLanguage = [this]() {
- const CardsDisplaySettings &cardsDisplay = SettingsCache::instance().cardsDisplay();
- cardDatabaseDisplayModel->setSearchLanguage(CardSearchLanguage{
- cardsDisplay.getCardLang(), static_cast(cardsDisplay.getCardSearchLanguage())});
- };
- applyCardSearchLanguage();
- connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this,
- applyCardSearchLanguage);
- connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardSearchLanguageChanged, this,
- applyCardSearchLanguage);
-
chooseTokenFromAllRadioButton = new QRadioButton(tr("Show &all tokens"));
connect(chooseTokenFromAllRadioButton, &QRadioButton::toggled, this, &DlgCreateToken::actChooseTokenFromAll);
chooseTokenFromDeckRadioButton = new QRadioButton(tr("Show tokens from this &deck"));
diff --git a/cockatrice/src/game_graphics/game_scene.cpp b/cockatrice/src/game_graphics/game_scene.cpp
index 17af7618b..4d3144ad4 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.
+ disconnect(this);
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();
}
@@ -262,27 +252,17 @@ void GameScene::adjustPlayerRotation(int rotationAdjustment)
*/
void GameScene::rearrange()
{
- if (rearranging) {
- needsReArrange = true;
- return;
- }
- rearranging = true;
- do {
- needsReArrange = false;
+ int firstPlayerIndex = 0;
+ auto playersPlaying = collectActivePlayers(firstPlayerIndex);
+ playersPlaying = rotatePlayers(playersPlaying, firstPlayerIndex);
- int firstPlayerIndex = 0;
- auto playersPlaying = collectActivePlayers(firstPlayerIndex);
- playersPlaying = rotatePlayers(playersPlaying, firstPlayerIndex);
+ int columns = determineColumnCount(playersPlaying.size());
+ QSizeF sceneSize = computeSceneSizeAndPlayerLayout(playersPlaying, columns);
- int columns = determineColumnCount(playersPlaying.size());
- QSizeF sceneSize = computeSceneSizeAndPlayerLayout(playersPlaying, columns);
+ phasesToolbar->setHeight(sceneSize.height());
+ setSceneRect(0, 0, sceneSize.width(), sceneSize.height());
- phasesToolbar->setHeight(sceneSize.height());
- setSceneRect(0, 0, sceneSize.width(), sceneSize.height());
-
- processViewSizeChange(viewSize);
- } while (needsReArrange);
- rearranging = false;
+ processViewSizeChange(viewSize);
}
// ---------- View Size ----------
@@ -479,14 +459,8 @@ void GameScene::resizeColumnsAndPlayers(const QList &minWidthByColumn, qr
qreal extraWidthPerColumn = (newWidth - minWidth) / playersByColumn.size();
qreal newx = phasesToolbar->getWidth();
- // Snapshot the columns: resizing a player's table can synchronously trigger
- // GameScene::rearrange (table width -> sizeChanged -> updateBoundingRect ->
- // sizeChanged -> rearrange), and rearrange rebuilds playersByColumn. Iterating
- // the live container across that re-entrant call would use invalidated iterators.
- const QList> columns = playersByColumn;
-
- for (int col = 0; col < columns.size(); ++col) {
- for (PlayerGraphicsItem *player : columns[col]) {
+ for (int col = 0; col < playersByColumn.size(); ++col) {
+ for (PlayerGraphicsItem *player : playersByColumn[col]) {
player->processSceneSizeChange(minWidthByColumn[col] + extraWidthPerColumn);
player->setPos(newx, player->y());
}
@@ -669,10 +643,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 +761,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 +781,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..7f01bf1f5 100644
--- a/cockatrice/src/game_graphics/game_scene.h
+++ b/cockatrice/src/game_graphics/game_scene.h
@@ -54,11 +54,7 @@ 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
/**
* @brief Updates which card is currently hovered based on scene coordinates.
diff --git a/cockatrice/src/game_graphics/game_view.cpp b/cockatrice/src/game_graphics/game_view.cpp
index ed6355157..b768c8317 100644
--- a/cockatrice/src/game_graphics/game_view.cpp
+++ b/cockatrice/src/game_graphics/game_view.cpp
@@ -114,7 +114,6 @@ void GameView::startRubberBand(const QPointF &_selectionOrigin)
}
selectionOrigin = _selectionOrigin;
- previousBandRect = QRect();
rubberBand->setGeometry(QRect(mapFromScene(selectionOrigin), QSize(0, 0)));
rubberBand->show();
}
@@ -129,17 +128,7 @@ void GameView::resizeRubberBand(const QPointF &cursorPoint, int selectedCount)
QPoint cursor = cursorPoint.toPoint();
QRect rect = QRect(mapFromScene(selectionOrigin), cursor).normalized();
-
rubberBand->setGeometry(rect);
- if (viewport()) {
- // Repaint the union of the previous and current band rects: the vacated
- // strip of a child widget is not reliably invalidated on all platforms
- // (notably macOS), leaving stale pixels under the selection.
- QRect dirty = previousBandRect.isNull() ? rect : previousBandRect.united(rect);
- dirty.adjust(-1, -1, 1, 1);
- viewport()->update(dirty);
- previousBandRect = rect;
- }
if (!SettingsCache::instance().userInterface().getShowDragSelectionCount()) {
dragCountLabel->hide();
@@ -182,13 +171,7 @@ void GameView::stopRubberBand()
return;
}
- // Same rationale as resizeRubberBand: repaint the last known band area
- // since hiding a child widget doesn't reliably invalidate its region.
rubberBand->hide();
- if (viewport() && !previousBandRect.isNull()) {
- viewport()->update(previousBandRect.adjusted(-1, -1, 1, 1));
- previousBandRect = QRect();
- }
dragCountLabel->hide();
}
diff --git a/cockatrice/src/game_graphics/game_view.h b/cockatrice/src/game_graphics/game_view.h
index a23655513..3f6b60dbc 100644
--- a/cockatrice/src/game_graphics/game_view.h
+++ b/cockatrice/src/game_graphics/game_view.h
@@ -27,7 +27,6 @@ private:
QWidget *tallyContainer;
QGridLayout *tallyLayout;
QPointF selectionOrigin;
- QRect previousBandRect; ///< Last rubber-band rect for targeted repaint
QList cachedTallyRows; ///< Cached entries to avoid redundant rebuilds
QSize rebuildTallyLabels(const QList &entries);
diff --git a/cockatrice/src/game_graphics/hand_counter.cpp b/cockatrice/src/game_graphics/hand_counter.cpp
index 8dcbcfdaa..35989ff38 100644
--- a/cockatrice/src/game_graphics/hand_counter.cpp
+++ b/cockatrice/src/game_graphics/hand_counter.cpp
@@ -1,6 +1,5 @@
#include "hand_counter.h"
-#include "../interface/pixel_map_generator.h"
#include "zones/card_zone.h"
#include
@@ -33,8 +32,7 @@ void HandCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*op
QSize translatedSize = painter->combinedTransform().mapRect(boundingRect()).size().toSize();
QPixmap cachedPixmap;
if (!QPixmapCache::find("handCounter" + QString::number(translatedSize.width()), &cachedPixmap)) {
- cachedPixmap =
- themePixmap(QStringLiteral("hand")).scaled(translatedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
+ cachedPixmap = QPixmap("theme:hand").scaled(translatedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
QPixmapCache::insert("handCounter" + QString::number(translatedSize.width()), cachedPixmap);
}
resetPainterTransform(painter);
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..2831f3393 100644
--- a/cockatrice/src/game_graphics/player/player_graphics_item.cpp
+++ b/cockatrice/src/game_graphics/player/player_graphics_item.cpp
@@ -1,10 +1,6 @@
#include "player_graphics_item.h"
#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"
#include "../board/counter_general.h"
@@ -17,9 +13,6 @@
#include "player_dialogs.h"
#include
-#include
-#include
-#include
#include
PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player)
@@ -35,10 +28,6 @@ PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player)
connect(player, &PlayerLogic::counterAdded, this, &PlayerGraphicsItem::onCounterAdded);
connect(player, &PlayerLogic::counterRemoved, this, &PlayerGraphicsItem::onCounterRemoved);
- connect(player, &PlayerLogic::deckChanged, this, &PlayerGraphicsItem::updatePlaymat);
- connect(player, &PlayerLogic::playmatChanged, this, &PlayerGraphicsItem::updatePlaymat);
- connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::playmatVisibilityChanged, this,
- [this](int) { updatePlaymat(); });
playerMenu = new PlayerMenu(this);
@@ -78,9 +67,6 @@ PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player)
connect(tableZoneGraphicsItem, &TableZone::sizeChanged, this, &PlayerGraphicsItem::updateBoundingRect);
- connect(this, &PlayerGraphicsItem::playmatChanged, tableZoneGraphicsItem, &TableZone::onPlaymatChanged);
- connect(this, &PlayerGraphicsItem::playmatChanged, stackZoneGraphicsItem, &StackZone::onPlaymatChanged);
-
updateBoundingRect();
rearrangeZones();
@@ -126,6 +112,7 @@ void PlayerGraphicsItem::initializeZones()
rfgZoneGraphicsItem->setPos(base + QPointF(0, 2 * h + h2 + 10));
tableZoneGraphicsItem = new TableZone(player->getTableZone(), mirrored, this);
+ connect(tableZoneGraphicsItem, &TableZone::sizeChanged, this, &PlayerGraphicsItem::updateBoundingRect);
connect(this, &PlayerGraphicsItem::mirroredChanged, tableZoneGraphicsItem, &TableZone::setMirrored);
stackZoneGraphicsItem =
@@ -168,61 +155,10 @@ qreal PlayerGraphicsItem::getMinimumWidth() const
return result;
}
-void PlayerGraphicsItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *)
+void PlayerGraphicsItem::paint(QPainter * /*painter*/,
+ const QStyleOptionGraphicsItem * /*option*/,
+ QWidget * /*widget*/)
{
- if (!hasPlaymat || playmatPixmap.isNull()) {
- return;
- }
-
- // Calculate the combined bounding rect of stack + table zones
- QPointF stackPos = stackZoneGraphicsItem->pos();
- QPointF tablePos = tableZoneGraphicsItem->pos();
- QSizeF stackSize = stackZoneGraphicsItem->boundingRect().size();
- QSizeF tableSize = tableZoneGraphicsItem->boundingRect().size();
-
- // Combined area: from stack left edge to table right edge
- double combinedLeft = qMin(stackPos.x(), tablePos.x());
- double combinedTop = qMin(stackPos.y(), tablePos.y());
- double combinedRight = qMax(stackPos.x() + stackSize.width(), tablePos.x() + tableSize.width());
- double combinedBottom = qMax(stackPos.y() + stackSize.height(), tablePos.y() + tableSize.height());
-
- QRectF combinedArea(combinedLeft, combinedTop, combinedRight - combinedLeft, combinedBottom - combinedTop);
-
- const QRectF srcRect = PlaymatUtils::computeArtSourceRect(playmatPixmap.size(), playmatParams);
- const QRectF dstRect = PlaymatUtils::coverFitRect(combinedArea, srcRect.size());
-
- painter->save();
- painter->setClipRect(combinedArea);
- painter->setRenderHint(QPainter::SmoothPixmapTransform, true);
-
- // Render from a down-scaled copy of the art so the full-resolution source
- // pixmap is never re-sampled at a tiny device size (also much cheaper than
- // scaling it on every frame).
- const QPixmap scaledPixmap = scaledPlaymatFor(srcRect, painter->worldTransform().mapRect(dstRect).size());
- painter->drawPixmap(dstRect, scaledPixmap, QRectF(scaledPixmap.rect()));
-
- painter->restore();
-
- if (!playmatAttribution.isEmpty()) {
- paintArtAttribution(*painter, combinedArea, playmatAttribution, Qt::AlignRight | Qt::AlignBottom, 0.8);
- }
-}
-
-QPixmap PlayerGraphicsItem::scaledPlaymatFor(const QRectF &srcRect, const QSizeF &deviceDstSize)
-{
- // Bucket the render size so the source pixmap is re-scaled at most once per
- // zoom step instead of once per frame.
- constexpr int bucketSize = 32;
- const QSize target = QSize(qMax(1, qRound(deviceDstSize.width() / bucketSize) * bucketSize),
- qMax(1, qRound(deviceDstSize.height() / bucketSize) * bucketSize))
- .boundedTo(srcRect.toAlignedRect().size());
-
- if (scaledPlaymatKey != target) {
- const QPixmap crop = playmatPixmap.copy(srcRect.toAlignedRect());
- scaledPlaymatPixmap = crop.scaled(target, Qt::KeepAspectRatio, Qt::SmoothTransformation);
- scaledPlaymatKey = target;
- }
- return scaledPlaymatPixmap;
}
void PlayerGraphicsItem::processSceneSizeChange(int newPlayerWidth)
@@ -252,8 +188,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();
}
});
@@ -367,100 +303,3 @@ void PlayerGraphicsItem::updateBoundingRect()
emit sizeChanged();
}
-
-void PlayerGraphicsItem::updatePlaymat()
-{
- int visibility = SettingsCache::instance().userInterface().getPlaymatVisibility();
-
- // "Don't use playmats" — never show
- if (visibility == PlaymatVisibilityNone) {
- clearPlaymat();
- return;
- }
-
- // "Show own playmat only" — hide playmats for remote players
- if (visibility == PlaymatVisibilityOwnOnly && !player->getPlayerInfo()->getLocal()) {
- clearPlaymat();
- return;
- }
-
- CardRef playmatCard;
- PlaymatParams params;
-
- if (player->getHasRemotePlaymat()) {
- // Prefer the server-confirmed playmat (updated by Command_SetPlaymat).
- playmatCard = player->getRemotePlaymatCard();
- params = player->getRemotePlaymatParams();
- } else if (player->getPlayerInfo()->getLocal()) {
- // Local player without a server broadcast yet: apply the full
- // settings-based resolution chain (mode, fallback list, behavior).
- const auto &settings = SettingsCache::instance().userInterface();
- const PlaymatInfo resolved = resolvePlaymatForDeck(
- player->getDeck(), settings.getPlaymatFallbackList(), static_cast(settings.getPlaymatMode()),
- static_cast(settings.getPlaymatFallbackBehavior()), 0);
- playmatCard = resolved.card;
- params = resolved.params;
- } else {
- // Opponent without a server broadcast: use the deck-embedded playmat.
- const DeckList &deck = player->getDeck();
- const PlaymatInfo &deckPlaymat = deck.getPlaymat();
- if (!deckPlaymat.card.isEmpty()) {
- playmatCard = deckPlaymat.card;
- params = deckPlaymat.params;
- }
- }
-
- if (playmatCard.isEmpty()) {
- clearPlaymat();
- return;
- }
-
- playmatParams = params;
- scaledPlaymatKey = QSize(); // the art crop depends on the params, drop any cached scale
-
- ExactCard card = CardDatabaseManager::query()->getCard(playmatCard);
- if (!card) {
- clearPlaymat();
- return;
- }
-
- playmatAttribution = buildArtAttribution(card);
-
- QPixmap fullRes;
- CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040));
-
- if (fullRes.isNull()) {
- disconnect(playmatPixmapConnection);
- CardInfo *cardInfo = card.getCardPtr().data();
- if (cardInfo) {
- playmatPixmapConnection =
- connect(cardInfo, &CardInfo::pixmapUpdated, this, &PlayerGraphicsItem::onPlaymatPixmapReady);
- }
- return;
- }
-
- if (!hasPlaymat) {
- hasPlaymat = true;
- emit playmatChanged(true);
- }
- playmatPixmap = CardArtUtils::rotateSidewaysLayoutArt(fullRes, card);
- update();
-}
-
-void PlayerGraphicsItem::clearPlaymat()
-{
- disconnect(playmatPixmapConnection);
- playmatAttribution.clear();
- if (hasPlaymat) {
- hasPlaymat = false;
- playmatPixmap = QPixmap();
- scaledPlaymatKey = QSize();
- emit playmatChanged(false);
- update();
- }
-}
-
-void PlayerGraphicsItem::onPlaymatPixmapReady()
-{
- updatePlaymat();
-}
diff --git a/cockatrice/src/game_graphics/player/player_graphics_item.h b/cockatrice/src/game_graphics/player/player_graphics_item.h
index e5ae59a61..d02234ded 100644
--- a/cockatrice/src/game_graphics/player/player_graphics_item.h
+++ b/cockatrice/src/game_graphics/player/player_graphics_item.h
@@ -11,7 +11,6 @@
#include "../game_scene.h"
#include
-#include
class HandZone;
class PileZone;
@@ -127,7 +126,6 @@ signals:
void playerCountChanged();
void mirroredChanged(bool isMirrored);
void cardInfoRequested(const CardRef &cardRef);
- void playmatChanged(bool hasPlaymat);
private:
PlayerLogic *player;
@@ -148,23 +146,9 @@ private:
bool mirrored;
bool handVisible = false;
- QPixmap playmatPixmap;
- QPixmap scaledPlaymatPixmap; // down-scaled copy of playmatPixmap for the current render size
- QSize scaledPlaymatKey; // size bucket scaledPlaymatPixmap was rendered for
- PlaymatParams playmatParams;
- QString playmatAttribution;
- bool hasPlaymat = false;
- QMetaObject::Connection playmatPixmapConnection;
-
private slots:
void updateBoundingRect();
void rearrangeZones();
- void clearPlaymat();
- void updatePlaymat();
- void onPlaymatPixmapReady();
-
-private:
- QPixmap scaledPlaymatFor(const QRectF &srcRect, const QSizeF &deviceDstSize);
};
#endif // COCKATRICE_PLAYER_GRAPHICS_ITEM_H
diff --git a/cockatrice/src/game_graphics/player/player_list_widget.cpp b/cockatrice/src/game_graphics/player/player_list_widget.cpp
index a7ec2e4a9..4268e1019 100644
--- a/cockatrice/src/game_graphics/player/player_list_widget.cpp
+++ b/cockatrice/src/game_graphics/player/player_list_widget.cpp
@@ -53,13 +53,13 @@ PlayerListWidget::PlayerListWidget(TabSupervisor *_tabSupervisor,
QWidget *parent)
: QTreeWidget(parent), tabSupervisor(_tabSupervisor), client(_client), game(_game), gameStarted(false)
{
- readyIcon = themePixmap(QStringLiteral("icons/ready_start"));
- notReadyIcon = themePixmap(QStringLiteral("icons/not_ready_start"));
- concededIcon = themePixmap(QStringLiteral("icons/conceded"));
+ readyIcon = QPixmap("theme:icons/ready_start");
+ notReadyIcon = QPixmap("theme:icons/not_ready_start");
+ concededIcon = QPixmap("theme:icons/conceded");
playerIcon = loadColorAdjustedPixmap("theme:icons/player");
judgeIcon = loadColorAdjustedPixmap("theme:icons/scales");
spectatorIcon = loadColorAdjustedPixmap("theme:icons/spectator");
- lockIcon = themePixmap(QStringLiteral("icons/lock"));
+ lockIcon = QPixmap("theme:icons/lock");
if (tabSupervisor) {
itemDelegate = new PlayerListItemDelegate(this);
@@ -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..184f96d62 100644
--- a/cockatrice/src/game_graphics/zones/stack_zone.cpp
+++ b/cockatrice/src/game_graphics/zones/stack_zone.cpp
@@ -31,22 +31,8 @@ QRectF StackZone::boundingRect() const
void StackZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
{
- if (playmatActive) {
- // Subtle overlay to distinguish stack zone from table zone (slightly darker)
- painter->fillRect(boundingRect(), QColor(0, 0, 0, 80));
- } else {
- QBrush brush = themeManager->getExtraBgBrush(ThemeManager::Stack, getLogic()->getPlayer()->getZoneId());
- painter->fillRect(boundingRect(), brush);
- }
-}
-
-void StackZone::onPlaymatChanged(bool active)
-{
- playmatActive = active;
- // See TableZone::onPlaymatChanged for the rationale. Translucent overlay
- // over a dynamic playmat should not be held in the device cache.
- setCacheMode(active ? QGraphicsItem::NoCache : QGraphicsItem::DeviceCoordinateCache);
- update();
+ QBrush brush = themeManager->getExtraBgBrush(ThemeManager::Stack, getLogic()->getPlayer()->getZoneId());
+ painter->fillRect(boundingRect(), brush);
}
void StackZone::handleDropEvent(const QList &dragItems,
@@ -57,14 +43,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/game_graphics/zones/stack_zone.h b/cockatrice/src/game_graphics/zones/stack_zone.h
index 96b3f96a6..147c3e2fc 100644
--- a/cockatrice/src/game_graphics/zones/stack_zone.h
+++ b/cockatrice/src/game_graphics/zones/stack_zone.h
@@ -15,13 +15,9 @@ class StackZone : public SelectZone
Q_OBJECT
private:
qreal zoneHeight;
- bool playmatActive = false;
private slots:
void updateBg();
-public slots:
- void onPlaymatChanged(bool active);
-
public:
StackZone(StackZoneLogic *_logic, int _zoneHeight, QGraphicsItem *parent);
/** @brief Resizes the stack zone height, e.g. when sharing vertical space with the command zone. */
diff --git a/cockatrice/src/game_graphics/zones/table_zone.cpp b/cockatrice/src/game_graphics/zones/table_zone.cpp
index 88e9abe6c..306e2927e 100644
--- a/cockatrice/src/game_graphics/zones/table_zone.cpp
+++ b/cockatrice/src/game_graphics/zones/table_zone.cpp
@@ -92,19 +92,14 @@ bool TableZone::isInverted() const
void TableZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
{
- if (playmatActive) {
- // Subtle overlay to distinguish table zone from stack zone
- painter->fillRect(boundingRect(), QColor(0, 0, 0, 60));
- } else {
- QBrush brush = themeManager->getExtraBgBrush(ThemeManager::Table, getLogic()->getPlayer()->getZoneId());
- painter->fillRect(boundingRect(), brush);
- }
+ QBrush brush = themeManager->getExtraBgBrush(ThemeManager::Table, getLogic()->getPlayer()->getZoneId());
+ painter->fillRect(boundingRect(), brush);
if (active) {
paintZoneOutline(painter);
} else {
// inactive player gets a darker table zone with a semi transparent black mask
- // this means if the user provides a custom background or playmat it will fade
+ // this means if the user provides a custom background it will fade
painter->fillRect(boundingRect(), FADE_MASK);
}
@@ -118,17 +113,6 @@ void TableZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*opti
paintLandDivider(painter);
}
-void TableZone::onPlaymatChanged(bool active)
-{
- playmatActive = active;
- // While a playmat is shown the zone paints a translucent overlay over the
- // dynamic playmat behind it. Keep it out of the device cache so the cached
- // pixels are never stale relative to the playmat (and to avoid compositing
- // artifacts of cached translucent content on some platforms).
- setCacheMode(active ? QGraphicsItem::NoCache : QGraphicsItem::DeviceCoordinateCache);
- update();
-}
-
/**
Render a soft outline around the edge of the TableZone.
diff --git a/cockatrice/src/game_graphics/zones/table_zone.h b/cockatrice/src/game_graphics/zones/table_zone.h
index 92915a2ed..1836c96ff 100644
--- a/cockatrice/src/game_graphics/zones/table_zone.h
+++ b/cockatrice/src/game_graphics/zones/table_zone.h
@@ -86,7 +86,6 @@ private:
*/
bool active = false;
bool mirrored = false;
- bool playmatActive = false;
[[nodiscard]] bool isInverted() const;
@@ -96,9 +95,6 @@ private slots:
*/
void updateBg();
-public slots:
- void onPlaymatChanged(bool active);
-
public slots:
/**
Reorganizes CardItems in the TableZone
@@ -188,17 +184,8 @@ public:
}
void setWidth(qreal _width)
{
- // The width is stored as an int; truncate to match the previous implicit conversion.
- const int newWidth = static_cast(_width);
- if (width == newWidth) {
- return;
- }
prepareGeometryChange();
- width = newWidth;
- // The parent player item's boundingRect (which clips the playmat painting) is
- // derived from this zone's size. Without this signal the playmat is cut off at
- // the stale boundingRect edge whenever the scene is resized wider.
- emit sizeChanged();
+ width = _width;
}
[[nodiscard]] qreal getWidth() const
{
diff --git a/cockatrice/src/game_graphics/zones/view_zone.cpp b/cockatrice/src/game_graphics/zones/view_zone.cpp
index 5bd5d262f..baf7b8b30 100644
--- a/cockatrice/src/game_graphics/zones/view_zone.cpp
+++ b/cockatrice/src/game_graphics/zones/view_zone.cpp
@@ -1,6 +1,5 @@
#include "view_zone.h"
-#include "../../client/settings/cache_settings.h"
#include "../../game/player/player_actions.h"
#include "../../game/player/player_logic.h"
#include "../../game/zones/view_zone_logic.h"
@@ -12,13 +11,11 @@
#include
#include
#include
-#include
#include
#include
#include
#include
#include
-#include
/**
* @param parent the parent QGraphicsWidget containing the reveal zone
@@ -256,10 +253,7 @@ ZoneViewZone::GridSize ZoneViewZone::positionCardsForDisplay(CardList &cards, Ca
void ZoneViewZone::setFilterString(const QString &_filterString)
{
- const CardsDisplaySettings &cardsDisplay = SettingsCache::instance().cardsDisplay();
- filterString = FilterString(
- _filterString, CardSearchLanguage{cardsDisplay.getCardLang(),
- static_cast(cardsDisplay.getCardSearchLanguage())});
+ filterString = FilterString(_filterString);
reorganizeCards();
}
diff --git a/cockatrice/src/game_graphics/zones/view_zone_widget.cpp b/cockatrice/src/game_graphics/zones/view_zone_widget.cpp
index fa6733413..17118e80d 100644
--- a/cockatrice/src/game_graphics/zones/view_zone_widget.cpp
+++ b/cockatrice/src/game_graphics/zones/view_zone_widget.cpp
@@ -21,7 +21,6 @@
#include
#include
#include
-#include
#include
namespace
@@ -63,7 +62,7 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player,
searchEdit.setPlaceholderText(tr("Search by card name (or search expressions)"));
searchEdit.setClearButtonEnabled(true);
searchEdit.addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition);
- auto help = searchEdit.addAction(themePixmap(QStringLiteral("icons/info")), QLineEdit::TrailingPosition);
+ auto help = searchEdit.addAction(QPixmap("theme:icons/info"), QLineEdit::TrailingPosition);
connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(&searchEdit); });
@@ -169,12 +168,6 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player,
}
connect(&searchEdit, &QLineEdit::textChanged, zone, &ZoneViewZone::setFilterString);
-
- const auto applyCardSearchLanguage = [this] { zone->setFilterString(searchEdit.text()); };
- connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this,
- applyCardSearchLanguage);
- connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardSearchLanguageChanged, this,
- applyCardSearchLanguage);
}
setLayout(vbox);
@@ -556,7 +549,7 @@ void ZoneViewWidget::initStyleOption(QStyleOption *option) const
{
QStyleOptionTitleBar *titleBar = qstyleoption_cast(option);
if (titleBar) {
- titleBar->icon = themePixmap(QStringLiteral("cockatrice"));
+ titleBar->icon = QPixmap("theme:cockatrice");
}
}
diff --git a/cockatrice/src/interface/card_localization.h b/cockatrice/src/interface/card_localization.h
deleted file mode 100644
index 0bfe4a764..000000000
--- a/cockatrice/src/interface/card_localization.h
+++ /dev/null
@@ -1,59 +0,0 @@
-#ifndef COCKATRICE_CARD_LOCALIZATION_H
-#define COCKATRICE_CARD_LOCALIZATION_H
-
-#include "../client/settings/cache_settings.h"
-
-#include
-#include
-#include
-
-namespace CardLocalization
-{
-/**
- * @brief The language code selected for localized card text and images.
- */
-inline QString displayLang()
-{
- return SettingsCache::instance().cardsDisplay().getCardLang();
-}
-
-/**
- * @brief Card name in the configured display language, falling back to English.
- * @param card The card to display.
- * @return The localized name, or an empty string for a null card.
- */
-inline QString displayName(const CardInfoPtr &card)
-{
- return card.isNull() ? QString() : card->getLocalizedName(displayLang());
-}
-
-/**
- * @brief Card rules text in the configured display language, falling back to English.
- * @param card The card to display.
- * @return The localized text, or an empty string for a null card.
- */
-inline QString displayText(const CardInfoPtr &card)
-{
- return card.isNull() ? QString() : card->getLocalizedText(displayLang());
-}
-
-/**
- * @brief Card name in the configured display language, falling back to English.
- * @param card The card to display.
- */
-inline QString displayName(const CardInfo &card)
-{
- return card.getLocalizedName(displayLang());
-}
-
-/**
- * @brief Card rules text in the configured display language, falling back to English.
- * @param card The card to display.
- */
-inline QString displayText(const CardInfo &card)
-{
- return card.getLocalizedText(displayLang());
-}
-} // namespace CardLocalization
-
-#endif // COCKATRICE_CARD_LOCALIZATION_H
\ No newline at end of file
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 8e34942e1..2f46e7941 100644
--- a/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp
+++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp
@@ -1,17 +1,15 @@
#include "card_picture_loader.h"
#include "../../client/settings/cache_settings.h"
-#include "../pixel_map_generator.h"
#include "card_picture_loader_cache_method.h"
#include "card_picture_loader_local_schemes.h"
#include
#include
#include
-#include
+#include
#include
#include
-#include
#include
#include
#include
@@ -21,7 +19,6 @@
#include
#include
#include
-#include
#include
#include
#include
@@ -39,12 +36,9 @@ CardPictureLoader::CardPictureLoader() : QObject(nullptr)
&CardPictureLoader::picsPathChanged);
connect(&SettingsCache::instance().downloads(), &DownloadSettings::picDownloadChanged, this,
&CardPictureLoader::picDownloadChanged);
- connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this,
- &CardPictureLoader::cardLangChanged);
- qRegisterMetaType("ExactCard");
+ qRegisterMetaType();
connect(worker, &CardPictureLoaderWorker::imageLoaded, this, &CardPictureLoader::imageLoaded);
- connect(worker, &CardPictureLoaderWorker::networkCacheCleared, this, &CardPictureLoader::networkCacheCleared);
statusBar = new CardPictureLoaderStatusBar(nullptr);
QMainWindow *mainWindow = qobject_cast(QApplication::activeWindow());
@@ -60,18 +54,7 @@ CardPictureLoader::CardPictureLoader() : QObject(nullptr)
CardPictureLoader::~CardPictureLoader()
{
- if (worker) {
- // Capture the thread first: shutdownThread() blocks until the worker has been freed by the
- // finished() -> deleteLater chain, after which the worker pointer must not be dereferenced.
- QThread *pictureLoaderThread = worker->workerThread();
- const bool stopped = worker->shutdownThread();
- worker = nullptr;
- // Deleting a QThread that is still running is undefined behaviour, so only free it once the
- // bounded wait in shutdownThread() confirmed that it stopped.
- if (stopped) {
- delete pictureLoaderThread;
- }
- }
+ worker->deleteLater();
}
void CardPictureLoader::getCardBackPixmap(QPixmap &pixmap, QSize size)
@@ -79,7 +62,7 @@ void CardPictureLoader::getCardBackPixmap(QPixmap &pixmap, QSize size)
QString backCacheKey = "_trice_card_back_" + QString::number(size.width()) + "x" + QString::number(size.height());
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
qCDebug(CardPictureLoaderLog) << "PictureLoader: cache miss for" << backCacheKey;
- QPixmap tmpPixmap = themePixmap(QStringLiteral("cardback"));
+ QPixmap tmpPixmap("theme:cardback");
if (tmpPixmap.isNull()) {
qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback'! Using fallback pixmap.";
@@ -100,7 +83,7 @@ void CardPictureLoader::getCardBackLoadingInProgressPixmap(QPixmap &pixmap, QSiz
"_trice_card_back_inprogress_" + QString::number(size.width()) + "x" + QString::number(size.height());
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
qCDebug(CardPictureLoaderCardBackCacheFailLog) << "PictureLoader: cache miss for" << backCacheKey;
- QPixmap tmpPixmap = themePixmap(QStringLiteral("cardback"));
+ QPixmap tmpPixmap("theme:cardback");
if (tmpPixmap.isNull()) {
qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback' for in-progress state! Using fallback.";
@@ -122,7 +105,7 @@ void CardPictureLoader::getCardBackLoadingFailedPixmap(QPixmap &pixmap, QSize si
"_trice_card_back_failed_" + QString::number(size.width()) + "x" + QString::number(size.height());
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
qCDebug(CardPictureLoaderCardBackCacheFailLog) << "PictureLoader: cache miss for" << backCacheKey;
- QPixmap tmpPixmap = themePixmap(QStringLiteral("cardback"));
+ QPixmap tmpPixmap("theme:cardback");
if (tmpPixmap.isNull()) {
qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback' for failed state! Using fallback.";
@@ -155,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()) {
@@ -222,49 +204,7 @@ void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image)
card.emitPixmapUpdated();
}
-void CardPictureLoader::deleteAllLocalOverrides(const ExactCard &card)
-{
- const QString picsRoot = SettingsCache::instance().paths().getPicsPath();
- if (picsRoot.isEmpty() || !card) {
- return;
- }
-
- QDir baseDir(picsRoot);
- if (!baseDir.cd("downloadedPics")) {
- return;
- }
-
- const QString name = card.getInfo().getCorrectedName();
-
- QString set, collector, uuid;
- auto printing = card.getPrinting();
- if (printing.getSet()) {
- set = printing.getSet()->getCorrectedShortName();
- collector = printing.getProperty("num");
- uuid = printing.getUuid();
- }
-
- for (const auto &scheme : CardPictureLoaderLocalSchemes::exportSchemes()) {
- QString rel = CardPictureLoaderLocalSchemes::expandPattern(scheme.pattern, name, set, collector, uuid);
-
- if (rel.isEmpty()) {
- continue;
- }
-
- rel += ".png";
- rel = QDir::cleanPath(rel);
-
- QString fullPath = baseDir.filePath(rel);
-
- if (QFile::exists(fullPath)) {
- QFile::remove(fullPath);
- }
- }
-}
-
-void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card,
- const QPixmap &pixmap,
- const bool allowOverwrite)
+void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card, const QPixmap &pixmap)
{
if (pixmap.isNull() || !card) {
return;
@@ -324,9 +264,8 @@ void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card,
QFileInfo outInfo(baseDir.filePath(relativePath));
- // Automatic cache writes (FILESYSTEM_CACHE) must never clobber an explicit user override.
- // Only the explicit override paths pass allowOverwrite == true.
- if (!allowOverwrite && outInfo.exists()) {
+ // Do not overwrite existing files
+ if (outInfo.exists()) {
return;
}
@@ -347,122 +286,6 @@ void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card,
}
}
-void CardPictureLoader::installPrintingOverrideOnLoad(const ExactCard &originalCard, const ExactCard &overrideCard)
-{
- // Overriding a card with itself is the reset case, not a real override: every code path below
- // would re-enter itself through emitPixmapUpdated(). Reject it outright.
- if (originalCard == overrideCard) {
- return;
- }
-
- CardInfoPtr cardPtr = overrideCard.getCardPtr();
- if (!cardPtr) {
- return;
- }
-
- // Heap-allocate so the lambda can capture it before the connection is made
- auto *connectionHandle = new QMetaObject::Connection;
-
- *connectionHandle =
- connect(cardPtr.data(), &CardInfo::pixmapUpdated, cardPtr.data(),
- [originalCard, overrideCard, connectionHandle, this](const PrintingInfo &printing) {
- // All printings share the same CardInfo, so ignore updates triggered by any
- // other printing (e.g., the original card re-loading from disk).
- if (printing != overrideCard.getPrinting()) {
- return;
- }
-
- QPixmap pixmap;
- if (QPixmapCache::find(overrideCard.getPixmapCacheKey(), &pixmap) && !pixmap.isNull()) {
- // The override art has resolved — persist it and reflect it immediately.
- // Retire the connection before emitting so the refresh can't re-enter.
- saveCardImageToLocalStorage(originalCard, pixmap, /*allowOverwrite=*/true);
-
- QObject::disconnect(*connectionHandle);
- delete connectionHandle;
-
- QPixmapCache::clear();
- originalCard.emitPixmapUpdated();
- return;
- }
-
- // The art could not be resolved. Keep the connection armed so a late resolution
- // still lands, and surface a visible refusal instead of a silent no-op. An
- // override already on disk is left untouched and simply re-displayed.
- QPixmapCache::clear();
- if (!hasLocalOverrides(originalCard)) {
- QPixmap refusedPixmap;
- getCardBackLoadingFailedPixmap(refusedPixmap, QSize(480, 672));
- QPixmapCache::insert(originalCard.getPixmapCacheKey(), refusedPixmap);
- }
- originalCard.emitPixmapUpdated();
- });
-
- // Now enqueue; if the image is already loading (deduplicated in the worker),
- // the signal will still fire when it completes
- CardPictureLoader::getInstance().worker->enqueueImageLoad(overrideCard);
-}
-
-void CardPictureLoader::installPrintingOverride(const ExactCard &originalCard, const ExactCard &overrideCard)
-{
- // Same guard as installPrintingOverrideOnLoad: self-override is the reset case.
- if (originalCard == overrideCard) {
- return;
- }
-
- QPixmap pixmap;
- const QString key = overrideCard.getPixmapCacheKey();
-
- if (QPixmapCache::find(key, &pixmap) && !pixmap.isNull()) {
- // Already cached — save immediately; the caller refreshes the card.
- saveCardImageToLocalStorage(originalCard, pixmap, /*allowOverwrite=*/true);
- return;
- }
-
- // Cache miss or previously failed load — enqueue load and wait for the signal.
- installPrintingOverrideOnLoad(originalCard, overrideCard);
-}
-
-bool CardPictureLoader::hasLocalOverrides(const ExactCard &card)
-{
- const QString picsRoot = SettingsCache::instance().paths().getPicsPath();
- if (picsRoot.isEmpty() || !card) {
- return false;
- }
-
- QDir baseDir(picsRoot);
- if (!baseDir.cd("downloadedPics")) {
- return false;
- }
-
- const QString name = card.getInfo().getCorrectedName();
-
- QString set, collector, uuid;
- const PrintingInfo printing = card.getPrinting();
- if (printing.getSet()) {
- set = printing.getSet()->getCorrectedShortName();
- collector = printing.getProperty("num");
- uuid = printing.getUuid();
- }
-
- for (const auto &scheme : CardPictureLoaderLocalSchemes::exportSchemes()) {
- QString rel = CardPictureLoaderLocalSchemes::expandPattern(scheme.pattern, name, set, collector, uuid);
-
- if (rel.isEmpty()) {
- continue;
- }
-
- rel += ".png";
- rel = QDir::cleanPath(rel);
-
- if (QFile::exists(baseDir.filePath(rel))) {
- return true;
- }
- }
-
- return false;
-}
-
void CardPictureLoader::clearPixmapCache()
{
QPixmapCache::clear();
@@ -470,17 +293,7 @@ void CardPictureLoader::clearPixmapCache()
void CardPictureLoader::clearNetworkCache()
{
- // During teardown the worker is released before this singleton, so a queued clear may still
- // arrive with no worker left to run it.
- CardPictureLoaderWorker *worker = getInstance().worker;
- if (!worker) {
- return;
- }
- // The disk cache and redirect cache are owned by the worker thread, so the clear has to run
- // there. Invoke it asynchronously to keep the GUI responsive while the worker may be walking
- // the user's picture directories or recursively deleting the cache directory; callers that
- // need to know when it is done can listen for networkCacheCleared().
- QMetaObject::invokeMethod(worker, &CardPictureLoaderWorker::clearNetworkCache, Qt::QueuedConnection);
+ getInstance().worker->clearNetworkCache();
}
void CardPictureLoader::cacheCardPixmaps(const QList &cards)
@@ -512,11 +325,31 @@ void CardPictureLoader::picsPathChanged()
QPixmapCache::clear();
}
-void CardPictureLoader::cardLangChanged()
+bool CardPictureLoader::hasCustomArt()
{
- // Localized images are fetched via a different URL, but the in-memory
- // pixmap cache is keyed by card name/uuid, so drop everything cached
- // (including failure timestamps) to force a reload in the new language.
- QPixmapCache::clear();
- failedAt.clear();
+ auto picsPath = SettingsCache::instance().paths().getPicsPath();
+ QDirIterator it(picsPath, QDir::Dirs | QDir::NoDotAndDotDot);
+
+ // Check if there is at least one non-directory file in the pics path, other
+ // than in the "downloadedPics" subdirectory.
+ while (it.hasNext()) {
+#if (QT_VERSION >= QT_VERSION_CHECK(6, 3, 0))
+ QFileInfo dir(it.nextFileInfo());
+#else
+ // nextFileInfo() is only available in Qt 6.3+, for previous versions, we build
+ // the QFileInfo from a QString which requires more system calls.
+ QFileInfo dir(it.next());
+#endif
+
+ if (it.fileName() == "downloadedPics") {
+ continue;
+ }
+
+ QDirIterator subIt(it.filePath(), QDir::Files, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
+ if (subIt.hasNext()) {
+ return true;
+ }
+ }
+
+ return false;
}
diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader.h b/cockatrice/src/interface/card_picture_loader/card_picture_loader.h
index 6224befbf..5c3ac84a3 100644
--- a/cockatrice/src/interface/card_picture_loader/card_picture_loader.h
+++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader.h
@@ -97,17 +97,10 @@ public:
static void cacheCardPixmaps(const QList &cards);
/**
- * @brief Check if a local override image already exists for the card.
- * @param card The card to check.
- * @return True if the card has at least one locally stored override image.
+ * @brief Check if the user has custom card art in the picsPath directory.
+ * @return True if any custom art exists.
*/
- static bool hasLocalOverrides(const ExactCard &card);
-
- /**
- * @brief Removes all locally stored override images for the card.
- * @param card The card to remove the override images of.
- */
- static void deleteAllLocalOverrides(const ExactCard &card);
+ static bool hasCustomArt();
/**
* @brief Clears the in-memory QPixmap cache for all cards.
@@ -117,9 +110,6 @@ public:
public slots:
/**
* @brief Clears the network disk cache of the worker.
- *
- * The clear runs on the worker thread, so this returns before it has completed; connect to
- * networkCacheCleared() to act once it is done.
*/
static void clearNetworkCache();
@@ -130,13 +120,7 @@ public slots:
* @param image Loaded QImage.
*/
void imageLoaded(const ExactCard &card, const QImage &image);
- void saveCardImageToLocalStorage(const ExactCard &card, const QPixmap &pixmap, bool allowOverwrite = false);
- void installPrintingOverride(const ExactCard &originalCard, const ExactCard &overrideCard);
- void installPrintingOverrideOnLoad(const ExactCard &originalCard, const ExactCard &overrideCard);
-
-signals:
- /** @brief Emitted after the worker has finished clearing the network and redirect caches. */
- void networkCacheCleared();
+ void saveCardImageToLocalStorage(const ExactCard &card, const QPixmap &pixmap);
private slots:
/**
@@ -150,12 +134,6 @@ private slots:
* Clears the QPixmap cache to reload images.
*/
void picsPathChanged();
-
- /**
- * @brief Triggered when the card language setting changes.
- * Clears the in-memory picture caches so images reload in the new language.
- */
- void cardLangChanged();
};
#endif
diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp
index 12165e8f3..39621839a 100644
--- a/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp
+++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp
@@ -94,14 +94,10 @@ QImage CardPictureLoaderLocal::tryLoadCardImageFromDisk(const QString &setName,
candidatePaths << picsPath + "/downloadedPics/" + setName + "/" + nameVariant;
}
- // Non-set-folder export schemes (e.g., Name_Set_Collector) write straight into
- // downloadedPics/; check there as a fallback so local overrides round-trip.
- candidatePaths << picsPath + "/downloadedPics/" + nameVariant;
-
for (const QString &path : candidatePaths) {
QFileInfo fileInfo(path);
QDir dir = fileInfo.dir();
- QString baseName = fileInfo.completeBaseName();
+ QString baseName = fileInfo.fileName();
if (!dir.exists()) {
continue;
@@ -109,8 +105,7 @@ QImage CardPictureLoaderLocal::tryLoadCardImageFromDisk(const QString &setName,
QStringList files = dir.entryList(QDir::Files);
for (const QString &file : files) {
- QFileInfo fi(file);
- if (fi.completeBaseName() != baseName) {
+ if (!file.startsWith(baseName)) {
continue;
}
diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp
index 0fd6a42d5..d288236d2 100644
--- a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp
+++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp
@@ -16,16 +16,13 @@
#include
#include
-static constexpr int MAX_REQUESTS_PER_SEC = DownloadSettings::DEFAULT_HOST_REQUEST_LIMIT;
-static constexpr int MIN_HOST_QUOTA = DownloadSettings::MIN_HOST_REQUEST_LIMIT;
-static constexpr qint64 QUOTA_RECOVER_MS = 60000; ///< Idle time before a reduced quota starts recovering
-static constexpr int DISPATCH_INTERVAL_MS = 100; ///< Pacing between individual network requests
-static constexpr qint64 QUOTA_RESET_INTERVAL_MS = 1000; ///< Interval at which the request quota resets
-static constexpr int THREAD_SHUTDOWN_WAIT_MS = 5000; ///< Bounded wait for the worker thread to stop at exit
+static constexpr int MAX_REQUESTS_PER_SEC = 10;
+static constexpr int MIN_HOST_QUOTA = 1; ///< Floor for the per-host request allowance
+static constexpr qint64 QUOTA_RECOVER_MS = 60000; ///< Idle time before a reduced quota starts recovering
CardPictureLoaderWorker::CardPictureLoaderWorker()
: QObject(nullptr), picDownload(SettingsCache::instance().downloads().getPicDownload()),
- hostRequestLimits(SettingsCache::instance().downloads().getHostRequestLimits())
+ requestQuota(MAX_REQUESTS_PER_SEC)
{
networkManager = new QNetworkAccessManager(this);
// We need a timeout to ensure requests don't hang indefinitely in case of
@@ -60,59 +57,20 @@ CardPictureLoaderWorker::CardPictureLoaderWorker()
localLoader = new CardPictureLoaderLocal(this);
pictureLoaderThread = new QThread;
- // The worker object frees itself once its thread finishes, so no event loop is left
- // running and the QThread is never destroyed while still executing.
- connect(pictureLoaderThread, &QThread::finished, this, &QObject::deleteLater);
pictureLoaderThread->start(QThread::LowPriority);
moveToThread(pictureLoaderThread);
- // QTimer value members are not QObject children, so moveToThread on the worker doesn't move
- // them. They must live in the worker's thread to be started from the slot code that runs there.
- requestTimer.moveToThread(pictureLoaderThread);
- dispatchTimer.moveToThread(pictureLoaderThread);
-
connect(this, &CardPictureLoaderWorker::imageLoadEnqueued, this, &CardPictureLoaderWorker::handleImageLoadEnqueued);
connect(&requestTimer, &QTimer::timeout, this, &CardPictureLoaderWorker::resetRequestQuota);
- requestTimer.setInterval(static_cast(QUOTA_RESET_INTERVAL_MS));
-
- connect(&dispatchTimer, &QTimer::timeout, this, &CardPictureLoaderWorker::dispatchQueuedRequest);
- dispatchTimer.setInterval(DISPATCH_INTERVAL_MS);
-
- connect(&SettingsCache::instance().downloads(), &DownloadSettings::hostRequestLimitsChanged, this,
- [this] { hostRequestLimits = SettingsCache::instance().downloads().getHostRequestLimits(); });
+ requestTimer.setInterval(1000);
+ requestTimer.start();
}
CardPictureLoaderWorker::~CardPictureLoaderWorker()
{
saveRedirectCache();
-}
-
-bool CardPictureLoaderWorker::shutdownThread()
-{
- // The finished() -> deleteLater chain (wired in the constructor) frees this worker as soon as
- // its event loop exits, so nothing - not even a member read - may run once wait() returns.
- // QThread::quit() and QThread::wait() are thread-safe and may be called from the owning thread.
- QThread *thread = pictureLoaderThread;
- if (!thread) {
- return true;
- }
- thread->quit();
- // Only an unbounded wait() would guarantee the thread stops, but this runs from a function-local
- // static destructor after main() has returned, with no UI left to interrupt a worker stuck in a
- // slow slot or on a stalled filesystem. Bound the wait and leave such a thread to the OS rather
- // than hanging the process forever.
- if (!thread->wait(THREAD_SHUTDOWN_WAIT_MS)) {
- qCWarning(CardPictureLoaderWorkerLog) << "Picture loader worker thread did not stop within"
- << THREAD_SHUTDOWN_WAIT_MS << "ms; leaving it to be torn down by the OS";
- return false;
- }
- return true;
-}
-
-QThread *CardPictureLoaderWorker::workerThread() const
-{
- return pictureLoaderThread;
+ pictureLoaderThread->deleteLater();
}
void CardPictureLoaderWorker::queueRequest(const QUrl &url, CardPictureLoaderWorkerWork *worker)
@@ -126,8 +84,8 @@ void CardPictureLoaderWorker::queueRequest(const QUrl &url, CardPictureLoaderWor
SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) ==
CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE &&
cache->metaData(url).isValid()) {
- // A request that will be served from the disk cache never touches the network and therefore
- // doesn't use up any of the rate limit, so it gets to skip the queue.
+ // If we hit a cached url, we get to make the request for free, since it won't contribute towards the
+ // rate-limit
makeRequest(url, worker);
return;
}
@@ -141,17 +99,7 @@ QNetworkReply *CardPictureLoaderWorker::makeRequest(const QUrl &url, CardPicture
// Check for cached redirects
QUrl cachedRedirect = getCachedRedirect(url);
if (!cachedRedirect.isEmpty()) {
- // The status bar still needs to reclaim this URL's widget even when we hand the request back
- // for a deferred retry instead of dispatching it onto the network.
emit imageRequestSucceeded(url);
- // The redirect target is a different host, which may itself be in 429 backoff; hand the
- // entry back to its worker so it waits the backoff out instead of dispatching straight
- // onto the backed-off host.
- if (CardPictureLoaderWorkerWork::rateLimiter().isRateLimited(cachedRedirect.host(),
- QDateTime::currentDateTime())) {
- worker->scheduleDeferredRetry(cachedRedirect.host());
- return nullptr;
- }
return makeRequest(cachedRedirect, worker);
}
@@ -159,261 +107,67 @@ QNetworkReply *CardPictureLoaderWorker::makeRequest(const QUrl &url, CardPicture
req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
req.setRawHeader("Accept", "image/avif,image/webp,image/apng,image/,/*;q=0.8");
- // Cached entries are served straight from the disk cache even when picture downloads are
- // enabled: re-fetching an already-cached image would burn the rate limit for nothing. Only a
- // genuine cache miss goes to the network, and only when downloads are enabled.
- bool useNetworkCache = !requestTouchesNetwork(url);
+ bool useNetworkCache =
+ !picDownload && static_cast(
+ SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) ==
+ CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE;
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute,
useNetworkCache ? QNetworkRequest::AlwaysCache : QNetworkRequest::AlwaysNetwork);
QNetworkReply *reply = networkManager->get(req);
- // Track in-flight replies per host so the unlocked fast path can bound how many requests it
- // issues at once, instead of creating replies that time out before Qt opens a connection.
- const QString host = url.host();
- hostInFlight.insert(host, hostInFlight.value(host) + 1);
-
- // Release the in-flight slot when the reply is destroyed, not when it emits `finished`, and use
- // the worker (not the work object) as the context object: a reply can go away without ever
- // finishing (aborted, or a work object deleted while a reply is still pending), and a connection
- // bound to that work object's lifetime would then never run, permanently shrinking the fast
- // path's concurrency until it wedges. This way the slot is released exactly once.
- connect(reply, &QObject::destroyed, this,
- [this, host] { hostInFlight.insert(host, qMax(0, hostInFlight.value(host) - 1)); });
-
- // Connect reply handling; the work object is the context so its handler dies with it.
- connect(reply, &QNetworkReply::finished, worker, [worker, reply] { worker->handleNetworkReply(reply); });
+ // Connect reply handling
+ connect(reply, &QNetworkReply::finished, worker, [reply, worker] { worker->handleNetworkReply(reply); });
return reply;
}
void CardPictureLoaderWorker::resetRequestQuota()
{
+ requestQuota = MAX_REQUESTS_PER_SEC;
+
QDateTime now = QDateTime::currentDateTime();
- for (auto it = hostRequestQuota.begin(); it != hostRequestQuota.end();) {
+ for (auto it = hostRequestQuota.begin(); it != hostRequestQuota.end(); ++it) {
if (!hostLast429.contains(it.key()) || now.msecsTo(hostLast429.value(it.key())) < -QUOTA_RECOVER_MS) {
- if (hostAllowanceCeiling(it.key()) == DownloadSettings::UNLIMITED_HOST_QUOTA) {
- // A developer-unlocked host that fell back after a 429 recovers towards the default
- // allowance; once it gets there it becomes unlocked (fast-path) again.
- if (it.value() + 1 >= DownloadSettings::DEFAULT_HOST_REQUEST_LIMIT) {
- it = hostRequestQuota.erase(it);
- continue;
- }
- it.value() += 1;
- } else {
- // Recover towards the host's effective allowance ceiling, which may be
- // lowered by the user's per-host request limits.
- it.value() = qMin(hostAllowanceCeiling(it.key()), it.value() + 1);
- }
+ it.value() = qMin(MAX_REQUESTS_PER_SEC, it.value() + 1);
}
- ++it;
}
- // Forget the per-second allowances; each host's allowance is re-seeded lazily from its
- // reduced sustained quota the first time it is dispatched in the new second, so a host that
- // enters the queue mid-second no longer falls through to a fresh full quota.
- hostQuotaRemaining.clear();
+ for (const auto &request : requestLoadQueue) {
+ const QString host = request.first.host();
+ hostQuotaRemaining.insert(host, hostRequestQuota.value(host, MAX_REQUESTS_PER_SEC));
+ }
- updateTimerState();
+ processQueuedRequests();
}
void CardPictureLoaderWorker::processQueuedRequests()
{
- // QTimer must be started from the thread it lives in; if this public slot is ever reached from
- // another thread, replay it on the worker's event loop instead of letting start() fail silently.
- if (thread() != QThread::currentThread()) {
- QMetaObject::invokeMethod(this, &CardPictureLoaderWorker::processQueuedRequests, Qt::QueuedConnection);
- return;
- }
- updateTimerState();
-}
-
-void CardPictureLoaderWorker::dispatchQueuedRequest()
-{
- if (requestLoadQueue.isEmpty()) {
- // All queued requests have been dispatched; stop the pacing timers.
- updateTimerState();
- return;
- }
-
- QDateTime now = QDateTime::currentDateTime();
- bool dispatched = false;
- // Set while an unlocked host still has queued work blocked only by the in-flight cap; the
- // timer must keep running so it gets another try as soon as a slot frees. A host blocked by
- // its 429 backoff instead waits for the next quota-reset tick to restart the dispatcher.
- bool unlockedCapped = false;
-
- // Unlocked hosts (developer cap UNLIMITED_HOST_QUOTA) skip the pacing and the per-host
- // allowance: dispatch their queued requests back-to-back, bounded by their 429 backoff and the
- // per-host in-flight cap so a large burst can't queue replies that time out before Qt opens a
- // connection for them.
- for (int i = 0; i < requestLoadQueue.size();) {
- const auto &request = requestLoadQueue.at(i);
- // Dispatch decisions must key on the host the request will actually go to, not the URL that
- // merely redirects to it: a redirect learned after this URL was queued would otherwise
- // bypass the in-flight cap and drain the whole queue onto the target host unchecked.
- const QUrl resolvedUrl = resolveCachedRedirect(request.first);
- const QString host = resolvedUrl.host();
- if (isUnlockedHost(host)) {
- if (CardPictureLoaderWorkerWork::rateLimiter().isRateLimited(host, now)) {
- ++i;
- continue;
- }
- if (hostInFlight.value(host) < MAX_IN_FLIGHT_PER_HOST) {
- makeRequest(resolvedUrl, request.second);
- requestLoadQueue.removeAt(i);
- dispatched = true;
- continue;
- }
- unlockedCapped = true;
- }
- ++i;
- }
-
- if (requestLoadQueue.isEmpty()) {
- dispatchTimer.stop();
- requestTimer.stop();
- return;
- }
-
- if (processSingleRequest()) {
- dispatched = true;
- }
-
- // Keep the timer running while there is progress to make or unlocked work waiting on a free
- // in-flight slot; otherwise no host has allowance left this second, so wait for the quota reset.
- if (!dispatched && !unlockedCapped) {
- dispatchTimer.stop();
- }
-}
-
-void CardPictureLoaderWorker::updateTimerState()
-{
- // Never restart an active timer: that would reset the pacing countdown and a burst of enqueues
- // could keep starving the dispatcher, so only (re)start a timer that has actually stopped.
- if (requestLoadQueue.isEmpty()) {
- dispatchTimer.stop();
- // Forget per-second allowances once nothing is pending: a stale zero would otherwise delay
- // the next single request by a full quota-reset interval.
- hostQuotaRemaining.clear();
- } else if (!dispatchTimer.isActive()) {
- dispatchTimer.start();
- }
-
- // The quota timer resets allowances every second and is also the only thing that heals a host
- // after a 429 (see resetRequestQuota). It must keep ticking while work is queued or a host is
- // still recovering below the ceiling, and only winds down once no host needs recovery anymore.
- // Keeping it alive during such idle periods lets reduced quotas recover as intended.
- bool hostRecovering = false;
- for (auto it = hostRequestQuota.cbegin(); it != hostRequestQuota.cend(); ++it) {
- if (it.value() < MAX_REQUESTS_PER_SEC) {
- hostRecovering = true;
- break;
- }
- }
-
- if (!requestLoadQueue.isEmpty() || hostRecovering) {
- if (!requestTimer.isActive()) {
- requestTimer.start();
- }
- } else if (requestTimer.isActive()) {
- requestTimer.stop();
+ while (requestQuota > 0 && processSingleRequest()) {
+ --requestQuota;
}
}
bool CardPictureLoaderWorker::processSingleRequest()
{
- QDateTime now = QDateTime::currentDateTime();
for (int i = 0; i < requestLoadQueue.size(); ++i) {
- // Copy the entry: takeAt(i) below erases within the list this reference points into.
- const auto request = requestLoadQueue.at(i);
- // Resolve cached redirects so the rate-limit and allowance arithmetic keys on the host the
- // request will actually hit (see resolveCachedRedirect).
- const QUrl resolvedUrl = resolveCachedRedirect(request.first);
- const QString host = resolvedUrl.host();
- // Don't dispatch requests to a host that is currently in its 429 backoff; hand the entry
- // back to its worker so it can wait the backoff out or fall through to another source,
- // instead of leaving it parked in the queue with no reply pending. Only applies to
- // requests that will actually touch the network: one that will be served from the disk
- // cache costs nothing and shouldn't wait out the 429.
- if (requestTouchesNetwork(request.first) &&
- CardPictureLoaderWorkerWork::rateLimiter().isRateLimited(host, now)) {
- // The queued URL is usually a cached-redirect target whose host differs from
- // cardToDownload.getCurrentUrl(), so scheduleDeferredRetry() (which waits out the
- // blocked host's deadline) is used instead of startNextPicDownload() looping on the
- // original host.
- auto entry = requestLoadQueue.takeAt(i);
- if (host != entry.first.host()) {
- // A cached redirect target is what is blocked, which the work object would not
- // discover from its own URL; wait out that specific host (with jitter) instead.
- entry.second->scheduleDeferredRetry(host);
- } else {
- entry.second->startNextPicDownload();
- }
- return true;
- }
- // Unlocked hosts are handled by dispatchQueuedRequest's fast path, bounded by the in-flight
- // cap; they must not fall through to the per-host allowance arithmetic below.
- if (isUnlockedHost(host)) {
- continue;
- }
- int ceiling = hostAllowanceCeiling(host);
- if (ceiling == DownloadSettings::UNLIMITED_HOST_QUOTA) {
- // A 429 dropped this unlocked host out of the fast path and installed a concrete
- // allowance; pace it against that allowance until the recovery loop unlocks it again.
- ceiling = hostRequestQuota.value(host, DownloadSettings::DEFAULT_HOST_REQUEST_LIMIT);
- }
- // Seed the allowance lazily so a host that enters the queue mid-second gets its reduced
- // per-host allowance, clamped against the ceiling so a lowered user cap applies from this
- // second onward.
- if (!hostQuotaRemaining.contains(host)) {
- hostQuotaRemaining.insert(host, qMin(ceiling, hostRequestQuota.value(host, ceiling)));
- }
- int allowance = hostQuotaRemaining.value(host);
+ const auto &request = requestLoadQueue.at(i);
+ QString host = request.first.host();
+ int allowance = hostQuotaRemaining.value(host, MAX_REQUESTS_PER_SEC);
if (allowance > 0) {
hostQuotaRemaining.insert(host, allowance - 1);
- auto entry = requestLoadQueue.takeAt(i);
- makeRequest(resolvedUrl, entry.second);
+ makeRequest(request.first, request.second);
+ requestLoadQueue.removeAt(i);
return true;
}
}
return false;
}
-bool CardPictureLoaderWorker::requestTouchesNetwork(const QUrl &url) const
-{
- bool useNetworkCache = static_cast(
- SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) ==
- CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE &&
- (cache->metaData(url).isValid() || !picDownload);
- return !useNetworkCache;
-}
-
-int CardPictureLoaderWorker::hostAllowanceCeiling(const QString &host) const
-{
- const int devCap = DownloadSettings::getDeveloperHostCaps().value(host, MAX_REQUESTS_PER_SEC);
- if (devCap == DownloadSettings::UNLIMITED_HOST_QUOTA && !hostRequestLimits.contains(host)) {
- return DownloadSettings::UNLIMITED_HOST_QUOTA;
- }
- const int requested = hostRequestLimits.value(host, devCap);
- return SettingsCache::instance().downloads().clampHostRequestLimit(host, requested);
-}
-
-bool CardPictureLoaderWorker::isUnlockedHost(const QString &host) const
-{
- return hostAllowanceCeiling(host) == DownloadSettings::UNLIMITED_HOST_QUOTA && !hostRequestQuota.contains(host);
-}
-
void CardPictureLoaderWorker::onHostRateLimited(const QString &host)
{
- const int ceiling = hostAllowanceCeiling(host);
- // An unlocked host has no per-host allowance to halve. Install one instead so it drops out of
- // the unlocked fast path and is paced like a throttled host; the recovery loop in
- // resetRequestQuota() then walks it back up and unlocks it again.
- const int base =
- ceiling == DownloadSettings::UNLIMITED_HOST_QUOTA ? DownloadSettings::DEFAULT_HOST_REQUEST_LIMIT : ceiling;
- hostRequestQuota.insert(host, qMax(MIN_HOST_QUOTA, hostRequestQuota.value(host, base) / 2));
+ hostRequestQuota.insert(host, qMax(MIN_HOST_QUOTA, hostRequestQuota.value(host, MAX_REQUESTS_PER_SEC) / 2));
hostLast429.insert(host, QDateTime::currentDateTime());
}
@@ -471,22 +225,6 @@ QUrl CardPictureLoaderWorker::getCachedRedirect(const QUrl &originalUrl) const
return {};
}
-QUrl CardPictureLoaderWorker::resolveCachedRedirect(const QUrl &url) const
-{
- // Follow the whole cached-redirect chain so dispatch keys on the host that is really hit. The
- // depth bound keeps a corrupt or self-referencing cache entry from spinning us forever.
- QUrl resolved = url;
- int depth = 0;
- while (depth++ < MAX_REDIRECT_CHAIN_DEPTH) {
- QUrl target = getCachedRedirect(resolved);
- if (target.isEmpty() || target == resolved) {
- break;
- }
- resolved = target;
- }
- return resolved;
-}
-
void CardPictureLoaderWorker::loadRedirectCache()
{
QSettings settings(cacheFilePath, QSettings::IniFormat);
@@ -539,5 +277,4 @@ void CardPictureLoaderWorker::clearNetworkCache()
{
networkManager->cache()->clear();
redirectCache.clear();
- emit networkCacheCleared();
}
diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.h b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.h
index 8b136f309..d1c519b7a 100644
--- a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.h
+++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.h
@@ -74,37 +74,10 @@ public:
*/
void onHostRateLimited(const QString &host);
- /**
- * @brief Stops the worker thread and reports whether it stopped.
- *
- * Called from the owning thread (CardPictureLoader) on its way out. QThread::quit() posts an
- * exit request to the worker's event loop and QThread::wait() blocks (bounded) until the loop
- * has returned and the thread finished. Only QThread members are touched here, so this method
- * is safe to call from the owning thread. The worker object itself is freed by the finished()
- * -> deleteLater chain (see the constructor); the QThread object is deleted afterwards by the
- * owner (CardPictureLoader::~CardPictureLoader), not by this method.
- *
- * @return true if the thread stopped within the timeout, false if it is still running (in
- * which case the owner must not delete the QThread).
- */
- [[nodiscard]] bool shutdownThread();
-
- /**
- * @brief Returns the worker's QThread.
- * @return The worker thread
- *
- * Only meaningful while the worker object is alive; capture it before calling shutdownThread().
- */
- QThread *workerThread() const;
+ /** @brief Clears the network cache and redirect cache. */
+ void clearNetworkCache();
public slots:
- /**
- * @brief Clears the network cache and redirect cache.
- *
- * Runs on the worker thread; invoke it via a queued call when coming from another thread,
- * since both caches are owned by the worker thread.
- */
- void clearNetworkCache();
/**
* @brief Makes a network request for the given URL using the specified worker.
* @param url URL to load
@@ -113,12 +86,9 @@ public slots:
*/
QNetworkReply *makeRequest(const QUrl &url, CardPictureLoaderWorkerWork *workThread);
- /** @brief Ensures the pacing and quota-reset timers reflect the current queue and recovery state. */
+ /** @brief Processes all queued requests respecting the request quota. */
void processQueuedRequests();
- /** @brief Chooses a request from the queue and starts it, respecting the quota and pacing. */
- void dispatchQueuedRequest();
-
/**
* @brief Processes a single queued request.
* @return true if a request was processed, false if queue is empty.
@@ -148,56 +118,18 @@ private:
bool picDownload; ///< Whether downloading images from network is enabled
QQueue> requestLoadQueue; ///< Queue of pending network requests
+ int requestQuota; ///< Remaining requests allowed per second
QTimer requestTimer; ///< Timer to reset the request quota
- QTimer dispatchTimer; ///< Timer pacing individual network requests
QHash hostRequestQuota; ///< Sustained per-host request allowance
- QHash hostRequestLimits; ///< User-set per-host request allowances
QHash hostQuotaRemaining; ///< Per-host allowance left in the current second
QHash hostLast429; ///< When each host was last rate limited
- QHash hostInFlight; ///< Network replies currently in flight, per host
-
- /** @brief Maximum concurrent in-flight network replies per host. */
- static constexpr int MAX_IN_FLIGHT_PER_HOST = 6;
-
- /** @brief Bound on how many cached-redirect hops dispatch resolution will follow. */
- static constexpr int MAX_REDIRECT_CHAIN_DEPTH = 10;
CardPictureLoaderLocal *localLoader; ///< Loader for local images
QSet currentlyLoading; ///< Deduplication: contains pixmapCacheKey currently being loaded
- /**
- * @brief Effective per-host allowance ceiling for a host.
- * @param host The host to look up
- * @return The allowance ceiling in requests/second, or DownloadSettings::UNLIMITED_HOST_QUOTA
- * when the developer unlocked the host and no user limit is set for it.
- */
- [[nodiscard]] int hostAllowanceCeiling(const QString &host) const;
-
- /**
- * @brief Whether a host may skip dispatch pacing and per-host allowance entirely.
- *
- * A host is unlocked while it has no user limit and no reduced allowance installed by a 429.
- * A 429 drops it out of the fast path until resetRequestQuota() walks the allowance back up.
- */
- [[nodiscard]] bool isUnlockedHost(const QString &host) const;
-
/** @brief Returns cached redirect URL for the given original URL, if available. */
[[nodiscard]] QUrl getCachedRedirect(const QUrl &originalUrl) const;
- /** @brief Whether a request for this URL would actually touch the network, rather than being served from the disk
- * cache. */
- [[nodiscard]] bool requestTouchesNetwork(const QUrl &url) const;
-
- /**
- * @brief Follows the cached-redirect chain to the URL that will actually be requested.
- * @param url The URL to resolve
- * @return The final URL after chasing cached redirects, or @p url itself if none lead elsewhere
- *
- * Dispatch decisions (unlocked-host fast path, 429 backoff, in-flight cap) must key on the host
- * a request really goes to, not the URL that merely redirects to it.
- */
- [[nodiscard]] QUrl resolveCachedRedirect(const QUrl &url) const;
-
/** @brief Loads redirect cache from disk. */
void loadRedirectCache();
@@ -207,9 +139,6 @@ private:
/** @brief Removes stale redirect entries older than TTL. */
void cleanStaleEntries();
- /** @brief Starts or stops the pacing and quota-reset timers to match the queue and recovery state. */
- void updateTimerState();
-
private slots:
/** @brief Resets the request quota for rate-limiting. */
void resetRequestQuota();
@@ -229,9 +158,6 @@ signals:
/** @brief Emitted when a network request successfully completes. */
void imageRequestSucceeded(const QUrl &url);
-
- /** @brief Emitted after clearNetworkCache() has finished clearing both caches. */
- void networkCacheCleared();
};
#endif // PICTURE_LOADER_WORKER_H
diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.cpp
index 19c6be469..66c56337c 100644
--- a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.cpp
+++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.cpp
@@ -22,13 +22,8 @@ static const QStringList MD5_BLACKLIST = {
"fbc7d763c08771c260b39e2115414eeb" // Current card back hash
};
-const ServerRateLimiter &CardPictureLoaderWorkerWork::rateLimiter()
-{
- return s_rateLimiter;
-}
-
-CardPictureLoaderWorkerWork::CardPictureLoaderWorkerWork(CardPictureLoaderWorker *worker, const ExactCard &toLoad)
- : QObject(worker), cardToDownload(CardPictureToLoad(toLoad)),
+CardPictureLoaderWorkerWork::CardPictureLoaderWorkerWork(const CardPictureLoaderWorker *worker, const ExactCard &toLoad)
+ : QObject(nullptr), cardToDownload(CardPictureToLoad(toLoad)),
picDownload(SettingsCache::instance().downloads().getPicDownload())
{
// Hook up signals to the orchestrator
@@ -173,7 +168,7 @@ void CardPictureLoaderWorkerWork::handleFailedReply(const QNetworkReply *reply)
<< "PictureLoader: [card: " << cardToDownload.getCard().getName()
<< " set: " << cardToDownload.getSetName() << "]: Too many requests from " << host
<< ", backing off until " << backoffUntil.toString(Qt::ISODate) << ", retrying the same url";
- scheduleDeferredRetry(host);
+ scheduleDeferredRetry();
} else {
qCWarning(CardPictureLoaderWorkerWorkLog).nospace()
<< "PictureLoader: [card: " << cardToDownload.getCard().getName()
@@ -278,16 +273,14 @@ QImage CardPictureLoaderWorkerWork::tryLoadImageFromReply(QNetworkReply *reply)
return imgReader.read();
}
-void CardPictureLoaderWorkerWork::scheduleDeferredRetry(const QString &preferredHost)
+void CardPictureLoaderWorkerWork::scheduleDeferredRetry()
{
QDateTime now = QDateTime::currentDateTime();
- // Prefer waiting on the server that is actually blocking the request: callers hand in the
- // rate-limited host when it differs from the current URL (e.g. a cached redirect target still
- // in backoff), otherwise fall back to the current URL's server so we retry the same source.
- QString waitHost = preferredHost.isEmpty() ? QUrl(cardToDownload.getCurrentUrl()).host() : preferredHost;
- QDateTime backoffUntil = s_rateLimiter.deadline(waitHost);
- if (!s_rateLimiter.isRateLimited(waitHost, now)) {
+ // Prefer waiting on the current URL's server so we retry the same source.
+ QString currentHost = QUrl(cardToDownload.getCurrentUrl()).host();
+ QDateTime backoffUntil = s_rateLimiter.deadline(currentHost);
+ if (!s_rateLimiter.isRateLimited(currentHost, now)) {
backoffUntil = s_rateLimiter.earliestDeadline(now);
}
diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.h b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.h
index 84a7dafdb..1e56a4373 100644
--- a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.h
+++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.h
@@ -10,7 +10,6 @@
#include
#include
#include
-#include
#include
#include
#include
@@ -37,36 +36,13 @@ class CardPictureLoaderWorkerWork : public QObject
public:
/**
* @brief Constructs a worker for downloading a specific card image.
- * @param worker The orchestrating CardPictureLoaderWorker; the work object becomes its child so
- * it is destroyed with the worker even if it never reaches concludeImageLoad().
+ * @param worker The orchestrating CardPictureLoaderWorker
* @param toLoad The ExactCard to download
*/
- explicit CardPictureLoaderWorkerWork(CardPictureLoaderWorker *worker, const ExactCard &toLoad);
+ explicit CardPictureLoaderWorkerWork(const CardPictureLoaderWorker *worker, const ExactCard &toLoad);
CardPictureToLoad cardToDownload; ///< The card and associated URLs to try downloading
- /** @brief Shared per-server 429 backoff state. */
- static const ServerRateLimiter &rateLimiter();
-
- /**
- * @brief Starts downloading the next URL for this card.
- *
- * Skips URLs whose server is currently in 429 backoff, either waiting the
- * backoff out or falling through to the other configured sources.
- */
- void startNextPicDownload();
-
- /**
- * @brief Schedules a deferred retry after the relevant server backoff expires.
- * @param preferredHost The server that is actually blocking the request, or an empty
- * string to use the current URL's server
- *
- * Waits on the blocking server's backoff deadline, otherwise on the earliest active
- * backoff. If no servers are in backoff, concludes with failure. Otherwise resets the
- * CardPictureToLoad indices and retries after the backoff period.
- */
- void scheduleDeferredRetry(const QString &preferredHost = {});
-
public slots:
/**
* @brief Handles a finished network reply for the card image.
@@ -79,6 +55,9 @@ private:
static ServerRateLimiter s_rateLimiter; ///< Shared per-server 429 backoff state
+ /** @brief Starts downloading the next URL for this card. */
+ void startNextPicDownload();
+
/** @brief Called when all URLs have been exhausted or download failed. */
void picDownloadFailed();
@@ -103,6 +82,16 @@ private:
*/
void concludeImageLoad(const QImage &image);
+ /**
+ * @brief Schedules a deferred retry after the relevant server backoff expires.
+ *
+ * Waits on the current URL's server when it is the reason we are blocked,
+ * otherwise on the earliest active backoff. If no servers are in backoff,
+ * concludes with failure. Otherwise resets the CardPictureToLoad indices and
+ * retries after the backoff period.
+ */
+ void scheduleDeferredRetry();
+
private slots:
/** @brief Updates the picDownload setting when it changes. */
void picDownloadChanged();
diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_to_load.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_to_load.cpp
index 4f933207e..5f4ff0bbd 100644
--- a/cockatrice/src/interface/card_picture_loader/card_picture_to_load.cpp
+++ b/cockatrice/src/interface/card_picture_loader/card_picture_to_load.cpp
@@ -282,15 +282,8 @@ QString CardPictureToLoad::transformUrl(const QString &urlTemplate) const
}
// language setting
- const QString cardLang = SettingsCache::instance().cardsDisplay().getCardLang();
- transformMap["!sflang!"] = cardLang;
-
- // The localized printing's own id is unknown, so Scryfall must resolve it by
- // its translated name (see populateSetUrls); expose that name for the
- // `/cards/named` template.
- if (cardLang != "en") {
- transformMap["!localizedName!"] = card.getInfo().getLocalizedName(cardLang);
- }
+ transformMap["!sflang!"] = QString(QCoreApplication::translate(
+ "PictureLoader", "en", "code for scryfall's language property, not available for all languages"));
QString transformedUrl = urlTemplate;
for (const QString &prop : transformMap.keys()) {
diff --git a/cockatrice/src/interface/deck_loader/deck_file_format.h b/cockatrice/src/interface/deck_loader/deck_file_format.h
index 3a25797ec..995de32c0 100644
--- a/cockatrice/src/interface/deck_loader/deck_file_format.h
+++ b/cockatrice/src/interface/deck_loader/deck_file_format.h
@@ -17,7 +17,7 @@ enum Format
PlainText,
/**
- * This is Cockatrice's native deck file format, and supports deck metadata such as banner cards and tags.
+ * This is cockatrice's native deck file format, and supports deck metadata such as banner cards and tags.
* Stored as .cod files.
*/
Cockatrice
diff --git a/cockatrice/src/interface/deck_loader/deck_loader.cpp b/cockatrice/src/interface/deck_loader/deck_loader.cpp
index f29b4eed2..39a0c1071 100644
--- a/cockatrice/src/interface/deck_loader/deck_loader.cpp
+++ b/cockatrice/src/interface/deck_loader/deck_loader.cpp
@@ -50,7 +50,7 @@ DeckLoader::loadFromFile(const QString &fileName, DeckFileFormat::Format fmt, bo
result = deckList.loadFromFile_Native(&file);
if (!result) {
qCInfo(DeckLoaderLog) << "Failed to load " << fileName
- << "as Cockatrice format; retrying as plain format";
+ << "as cockatrice format; retrying as plain format";
file.seek(0);
result = deckList.loadFromFile_Plain(&file, CardNameNormalizer());
fmt = DeckFileFormat::PlainText;
@@ -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 be0df311d..ac23e1ee0 100644
--- a/cockatrice/src/interface/deck_loader/deck_loader.h
+++ b/cockatrice/src/interface/deck_loader/deck_loader.h
@@ -131,7 +131,7 @@ public:
static void printDeckList(QPrinter *printer, const DeckList &deckList);
/**
- * Converts the given deck's file to the Cockatrice file format.
+ * Converts the given deck's file to the cockatrice file format.
* Uses the lastLoadInfo in the LoadedDeck to determine the current name of the file and where to save to.
* @param deck The deck to convert. Should have valid lastLoadInfo. Will update the lastLoadInfo.
* @return Whether the conversion succeeded.
@@ -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/intents/contexts/context_join_game.h b/cockatrice/src/interface/intents/contexts/context_join_game.h
index 2e5a88ea2..102e2a520 100644
--- a/cockatrice/src/interface/intents/contexts/context_join_game.h
+++ b/cockatrice/src/interface/intents/contexts/context_join_game.h
@@ -6,7 +6,6 @@ struct ContextJoinGame
{
ContextJoinRoom roomContext;
int gameId;
- bool asSpectator = false;
};
#endif // COCKATRICE_CONTEXT_JOIN_GAME_H
diff --git a/cockatrice/src/interface/intents/contexts/context_open_deck.h b/cockatrice/src/interface/intents/contexts/context_open_deck.h
deleted file mode 100644
index 03dca088e..000000000
--- a/cockatrice/src/interface/intents/contexts/context_open_deck.h
+++ /dev/null
@@ -1,14 +0,0 @@
-#ifndef COCKATRICE_CONTEXT_OPEN_DECK_H
-#define COCKATRICE_CONTEXT_OPEN_DECK_H
-
-#include "context_connect_to_server.h"
-
-#include
-
-struct ContextOpenDeck
-{
- ContextConnectToServer serverContext;
- QString shareToken;
-};
-
-#endif // COCKATRICE_CONTEXT_OPEN_DECK_H
diff --git a/cockatrice/src/interface/intents/intent.cpp b/cockatrice/src/interface/intents/intent.cpp
index db0d13b2c..c02a89f35 100644
--- a/cockatrice/src/interface/intents/intent.cpp
+++ b/cockatrice/src/interface/intents/intent.cpp
@@ -2,11 +2,10 @@
Intent::Intent(QObject *parent) : QObject(parent)
{
- // An intent is done as soon as it reports success, failure, or cancellation.
- // Deleting it also tears down its dependency chain and disconnects any signal wiring.
+ // An intent is done as soon as it reports success or failure. Deleting it
+ // also tears down its dependency chain and disconnects any signal wiring.
connect(this, &Intent::finished, this, &QObject::deleteLater);
connect(this, &Intent::failed, this, &QObject::deleteLater);
- connect(this, &Intent::cancelled, this, &QObject::deleteLater);
}
Intent::~Intent() = default;
@@ -28,7 +27,6 @@ void Intent::runDependency(Intent *dependency)
this->execute();
});
connect(dependency, &Intent::failed, this, &Intent::failed);
- connect(dependency, &Intent::cancelled, this, &Intent::cancelled);
dependency->execute();
}
@@ -48,11 +46,3 @@ void Intent::emitFailed(const QString &reason)
emit failed(reason);
}
}
-
-void Intent::emitCancelled()
-{
- if (!completed) {
- completed = true;
- emit cancelled();
- }
-}
diff --git a/cockatrice/src/interface/intents/intent.h b/cockatrice/src/interface/intents/intent.h
index 5d9fdd3d6..125900ecd 100644
--- a/cockatrice/src/interface/intents/intent.h
+++ b/cockatrice/src/interface/intents/intent.h
@@ -16,7 +16,6 @@ public:
signals:
void finished();
void failed(QString reason);
- void cancelled();
protected:
// --- Subclasses must implement these ---
@@ -30,7 +29,6 @@ protected:
// Emit the outcome exactly once; ignore late signals after the intent is done.
void emitFinished();
void emitFailed(const QString &reason);
- void emitCancelled();
private:
bool completed = false;
diff --git a/cockatrice/src/interface/intents/intent_join_server_game.cpp b/cockatrice/src/interface/intents/intent_join_server_game.cpp
index b22b200ed..fb9c4d5ce 100644
--- a/cockatrice/src/interface/intents/intent_join_server_game.cpp
+++ b/cockatrice/src/interface/intents/intent_join_server_game.cpp
@@ -19,15 +19,13 @@ bool IntentJoinServerGame::checkPrecondition() const
if (remoteClient->getStatus() != ClientStatus::StatusLoggedIn) {
return false;
}
- // serverName()/serverPort() reflect the server the client was configured
- // to connect to, which may differ from the actual TCP peer (e.g. when
- // connecting through a proxy), so compare those configured values. A link
- // naming the same host on another port is a different server and must not
- // reuse the session there.
- if (remoteClient->serverName().compare(context->roomContext.serverContext.hostname, Qt::CaseInsensitive) != 0) {
+ // peerPort() reflects the actual TCP peer, which may differ from the
+ // configured server port (e.g. when connecting through a proxy), so only
+ // the hostname is compared here.
+ if (remoteClient->peerName() != context->roomContext.serverContext.hostname) {
return false;
}
- if (QString::number(remoteClient->serverPort()) != context->roomContext.serverContext.port) {
+ if (QString::number(remoteClient->peerPort()) != context->roomContext.serverContext.port) {
return false;
}
@@ -57,7 +55,7 @@ bool IntentJoinServerGame::tryJoinGame(TabRoom *room)
return false;
}
- if (room->getGameSelector()->joinGameById(context->gameId, context->asSpectator)) {
+ if (room->getGameSelector()->joinGameById(context->gameId)) {
emitFinished();
return true;
}
diff --git a/cockatrice/src/interface/intents/intent_login.cpp b/cockatrice/src/interface/intents/intent_login.cpp
index 7beb63e1d..ff871fd03 100644
--- a/cockatrice/src/interface/intents/intent_login.cpp
+++ b/cockatrice/src/interface/intents/intent_login.cpp
@@ -1,14 +1,9 @@
#include "intent_login.h"
#include "../../client/settings/cache_settings.h"
-#include "../widgets/dialogs/dlg_login_prompt.h"
#include "libcockatrice/settings/servers_settings.h"
-#include
-
-IntentGetLoginCredentials::IntentGetLoginCredentials(ContextConnectToServer *_context,
- bool _promptForMissingCredentials)
- : Intent(), context(_context), promptForMissingCredentials(_promptForMissingCredentials)
+IntentGetLoginCredentials::IntentGetLoginCredentials(ContextConnectToServer *_context) : Intent(), context(_context)
{
}
@@ -34,46 +29,5 @@ void IntentGetLoginCredentials::onPreconditionSatisfied()
void IntentGetLoginCredentials::onPreconditionNotSatisfied()
{
- // MainWindow::applyStartupDestination runs this intent on every launch for
- // users whose startup tab is Server / Server Room; keep that path quiet, as
- // it was before the link-driven sign-in dialog existed.
- if (!promptForMissingCredentials) {
- emitFailed(tr("No saved credentials for this server"));
- return;
- }
-
- // No credentials saved for the target server: ask the user for them. They
- // opt into saving them so later links to the same server connect directly.
- const QString serverText = context->hostname + ":" + context->port;
- DlgLoginPrompt dialog(serverText);
- // ApplicationModal: the dialog has no parent (the intent is not a widget),
- // so WindowModal would not actually block any other window.
- dialog.setWindowModality(Qt::ApplicationModal);
-
- if (dialog.exec() != QDialog::Accepted) {
- emitCancelled();
- return;
- }
-
- context->username = dialog.username();
- context->password = dialog.password();
-
- if (dialog.savePassword() && !context->username.isEmpty()) {
- ServersSettings &servers = SettingsCache::instance().servers();
- // The host may already be saved under a friendly name (e.g. a public-server
- // list entry) with no credentials; reuse that name instead of overwriting
- // it with the raw hostname when addNewServer updates the entry in place.
- QString saveName = context->hostname;
- const int existingIndex = servers.findServerIndex(context->hostname, context->port);
- if (existingIndex >= 0) {
- saveName =
- servers.getValue(QString("saveName%1").arg(existingIndex), "server", "server_details").toString();
- if (saveName.isEmpty()) {
- saveName = context->hostname;
- }
- }
- servers.addNewServer(saveName, context->hostname, context->port, context->username, context->password, true);
- }
-
- emitFinished();
+ emitFailed(tr("No saved credentials for this server"));
}
diff --git a/cockatrice/src/interface/intents/intent_login.h b/cockatrice/src/interface/intents/intent_login.h
index 8ffd91a0a..c7fec92b7 100644
--- a/cockatrice/src/interface/intents/intent_login.h
+++ b/cockatrice/src/interface/intents/intent_login.h
@@ -9,10 +9,7 @@ class IntentGetLoginCredentials : public Intent
Q_OBJECT
public:
- // When promptForMissingCredentials is false (the default) a server without
- // saved credentials fails silently; only intent chains from cockatrice://
- // links opt into the interactive sign-in dialog.
- explicit IntentGetLoginCredentials(ContextConnectToServer *_context, bool _promptForMissingCredentials = false);
+ IntentGetLoginCredentials(ContextConnectToServer *_context);
protected:
bool checkPrecondition() const override;
@@ -21,7 +18,6 @@ protected:
private:
ContextConnectToServer *context;
- bool promptForMissingCredentials;
};
#endif // COCKATRICE_INTENT_LOGIN_H
diff --git a/cockatrice/src/interface/intents/intent_open_shared_deck.cpp b/cockatrice/src/interface/intents/intent_open_shared_deck.cpp
deleted file mode 100644
index 016de63e6..000000000
--- a/cockatrice/src/interface/intents/intent_open_shared_deck.cpp
+++ /dev/null
@@ -1,208 +0,0 @@
-#include "intent_open_shared_deck.h"
-
-#include "../deck_loader/deck_loader.h"
-#include "../widgets/dialogs/dlg_shared_decks_preview.h"
-#include "../widgets/tabs/tab_supervisor.h"
-#include "intent_connect_to_server.h"
-
-#include
-#include
-#include
-#include
-#include
-#include