Merge branch 'Cockatrice:master' into toggle-normal-untapping-once

This commit is contained in:
Skagra42 2026-08-13 13:09:08 -06:00 committed by GitHub
commit fdd7d4eac0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
543 changed files with 82935 additions and 48240 deletions

View file

@ -159,6 +159,7 @@ if [[ $PACKAGE_TYPE ]]; then
fi fi
if [[ $USE_VCPKG ]]; then if [[ $USE_VCPKG ]]; then
flags+=("-DUSE_VCPKG=1") flags+=("-DUSE_VCPKG=1")
flags+=("-DVCPKG_INSTALL_OPTIONS=--x-abi-tools-use-exact-versions")
fi fi
# Add cmake --build flags # Add cmake --build flags

View file

@ -1,5 +0,0 @@
---
exclude_paths:
- '**/translations/*.ts'
# codacy config documentation: https://support.codacy.com/hc/en-us/articles/115002130625-Codacy-Configuration-File

View file

@ -334,7 +334,7 @@ the tr() call, also you can add an extra string as a hint for translators:
QString message = tr("Everyone draws %n cards", "english hint for translators", amount); QString message = tr("Everyone draws %n cards", "english hint for translators", amount);
``` ```
See [Qt's wiki on translations]( See [Qt's wiki on translations](
https://doc.qt.io/qt-5/i18n-source-translation.html#handling-plurals) https://doc.qt.io/qt-6/i18n-source-translation.html#handling-plurals)
If you're about to propose a change that adds or modifies any translatable If you're about to propose a change that adds or modifies any translatable
string in the code, you don't need to take care of adding the new strings to string in the code, you don't need to take care of adding the new strings to

View file

@ -3,7 +3,7 @@ name: Build Desktop
permissions: permissions:
actions: write # needed to delete entries in GHA cache (update ccache) actions: write # needed to delete entries in GHA cache (update ccache)
attestations: write # needed to persist the attestation. attestations: write # needed to persist the attestation.
contents: write contents: write # needed for e.g. vcpkg dependency graph updates
id-token: write # needed for signing certificate in attestation id-token: write # needed for signing certificate in attestation
on: on:
@ -47,26 +47,21 @@ jobs:
tag: ${{ steps.configure.outputs.tag }} tag: ${{ steps.configure.outputs.tag }}
sha: ${{ steps.configure.outputs.sha }} sha: ${{ steps.configure.outputs.sha }}
steps: steps:
- name: "Configure" - name: "Configure"
env:
RESOLVED_SHA: ${{ case(github.event_name == 'pull_request', github.event.pull_request.head.sha, github.sha) }}
id: configure id: configure
shell: bash shell: bash
run: | run: |
tag_regex='^refs/tags/' if [[ "$GITHUB_REF_TYPE" == 'tag' ]]; then # release
if [[ $GITHUB_EVENT_NAME == pull-request ]]; then # pull request echo "tag=$GITHUB_REF_NAME" >> "$GITHUB_OUTPUT"
sha="${{github.event.pull_request.head.sha}}"
elif [[ $GITHUB_REF =~ $tag_regex ]]; then # release
sha="$GITHUB_SHA"
tag="${GITHUB_REF/refs\/tags\//}"
echo "tag=$tag" >>"$GITHUB_OUTPUT"
else # push to branch
sha="$GITHUB_SHA"
fi fi
echo "sha=$sha" >>"$GITHUB_OUTPUT" echo "sha=$RESOLVED_SHA" >> "$GITHUB_OUTPUT"
- name: "Checkout" - name: "Checkout"
if: steps.configure.outputs.tag != null if: steps.configure.outputs.tag != null
uses: actions/checkout@v6 uses: actions/checkout@v7
with: with:
fetch-depth: 0 # fetch all history for all branches and tags fetch-depth: 0 # fetch all history for all branches and tags
@ -92,7 +87,7 @@ jobs:
run: | run: |
args=() args=()
[[ $prerelease == yes ]] && args+=(--prerelease) [[ $prerelease == yes ]] && args+=(--prerelease)
gh release create "$tag_name" --verify-tag --draft "${args[@]}" \ gh release create "$tag_name" --verify-tag --draft "${args[@]}" \
--target "$target" \ --target "$target" \
--title "$release_name" \ --title "$release_name" \
@ -105,48 +100,48 @@ jobs:
# The files in ".ci/$distro$version" correspond to the values given here # The files in ".ci/$distro$version" correspond to the values given here
include: include:
- distro: Arch - distro: Arch
allow-failure: yes allow-failure: yes
package: skip # We are packaged in Arch already package: skip # We are packaged in Arch already
- distro: Servatrice_Debian - distro: Servatrice_Debian
version: 12 version: 12
package: DEB package: DEB
server_only: yes server_only: yes
test: skip test: skip
- distro: Debian - distro: Debian
version: 12 version: 12
package: DEB package: DEB
test: skip # Running tests on all distros is superfluous test: skip # Running tests on all distros is superfluous
- distro: Debian - distro: Debian
version: 13 version: 13
package: DEB package: DEB
- distro: Fedora - distro: Fedora
version: 43 version: 43
package: RPM package: RPM
test: skip # Running tests on all distros is superfluous test: skip # Running tests on all distros is superfluous
- distro: Fedora - distro: Fedora
version: 44 version: 44
package: RPM package: RPM
- distro: Ubuntu - distro: Ubuntu
version: 24.04 version: 24.04
package: DEB package: DEB
test: skip # Running tests on all distros is superfluous test: skip # Running tests on all distros is superfluous
- distro: Ubuntu - distro: Ubuntu
version: 26.04 version: 26.04
package: DEB package: DEB
name: ${{ matrix.distro }} ${{ matrix.version }} name: ${{ matrix.distro }} ${{ matrix.version }}
@ -163,11 +158,11 @@ jobs:
steps: steps:
- name: "Checkout" - name: "Checkout"
uses: actions/checkout@v6 uses: actions/checkout@v7
- name: "Restore compiler cache (ccache)" - name: "Restore compiler cache (ccache)"
id: ccache_restore id: ccache_restore
uses: actions/cache/restore@v5 uses: actions/cache/restore@v6
env: env:
BRANCH_NAME: ${{ github.head_ref || github.ref_name }} BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
with: with:
@ -203,7 +198,7 @@ jobs:
args+=(--ccache "$CCACHE_SIZE") args+=(--ccache "$CCACHE_SIZE")
args+=(--cmake-generator "$CMAKE_GENERATOR") args+=(--cmake-generator "$CMAKE_GENERATOR")
args+=(--suffix "$SUFFIX") args+=(--suffix "$SUFFIX")
RUN --server --release --package "$package" "${args[@]}" RUN --server --release --package "$package" "${args[@]}"
# Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342 # Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342
@ -211,15 +206,16 @@ jobs:
if: github.ref == 'refs/heads/master' && steps.ccache_restore.outputs.cache-hit if: github.ref == 'refs/heads/master' && steps.ccache_restore.outputs.cache-hit
continue-on-error: true continue-on-error: true
env: env:
CACHE_PRIMARY_KEY: ${{ steps.ccache_restore.outputs.cache-primary-key }}
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
run: | run: |
if gh cache delete --repo ${{ github.repository }} ${{ steps.ccache_restore.outputs.cache-primary-key }}; then if gh cache delete --repo "$GITHUB_REPOSITORY" "$CACHE_PRIMARY_KEY"; then
echo "Cache deleted successfully" echo "Cache deleted successfully"
fi fi
- name: "Save updated compiler cache (ccache)" - name: "Save updated compiler cache (ccache)"
if: github.ref == 'refs/heads/master' if: github.ref == 'refs/heads/master'
uses: actions/cache/save@v5 uses: actions/cache/save@v6
with: with:
key: ${{ steps.ccache_restore.outputs.cache-primary-key }} key: ${{ steps.ccache_restore.outputs.cache-primary-key }}
path: ${{ env.CACHE }} path: ${{ env.CACHE }}
@ -256,8 +252,9 @@ jobs:
if: steps.attestation.outcome == 'success' if: steps.attestation.outcome == 'success'
shell: bash shell: bash
env: env:
BUILD_PATH: ${{ steps.build.outputs.path }}
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
run: gh attestation verify "${{ steps.build.outputs.path }}" --repo Cockatrice/Cockatrice run: gh attestation verify "$BUILD_PATH" --repo Cockatrice/Cockatrice
build-vcpkg: build-vcpkg:
strategy: strategy:
@ -267,14 +264,13 @@ jobs:
- os: macOS - os: macOS
target: 13 target: 13
runner: macos-15-intel runner: macos-15-intel
ccache_eviction_age: 7d ccache_eviction_age: 7d
cmake_generator: Ninja cmake_generator: Ninja
make_package: 1 make_package: 1
override_target: 13 override_target: 13
package_suffix: "-macOS13_Intel" package_suffix: "-macOS13_Intel"
qt_version: 6.11.0 qt_version: 6.11.0
qt_arch: clang_64
qt_modules: qtimageformats qtmultimedia qtwebsockets qt_modules: qtimageformats qtmultimedia qtwebsockets
soc: Intel soc: Intel
type: Release type: Release
@ -284,13 +280,12 @@ jobs:
- os: macOS - os: macOS
target: 14 target: 14
runner: macos-14 runner: macos-14
ccache_eviction_age: 7d ccache_eviction_age: 7d
cmake_generator: Ninja cmake_generator: Ninja
make_package: 1 make_package: 1
package_suffix: "-macOS14" package_suffix: "-macOS14"
qt_version: 6.11.0 qt_version: 6.11.0
qt_arch: clang_64
qt_modules: qtimageformats qtmultimedia qtwebsockets qt_modules: qtimageformats qtmultimedia qtwebsockets
soc: Apple soc: Apple
type: Release type: Release
@ -300,13 +295,12 @@ jobs:
- os: macOS - os: macOS
target: 15 target: 15
runner: macos-15 runner: macos-15
ccache_eviction_age: 7d ccache_eviction_age: 7d
cmake_generator: Ninja cmake_generator: Ninja
make_package: 1 make_package: 1
package_suffix: "-macOS15" package_suffix: "-macOS15"
qt_version: 6.11.0 qt_version: 6.11.0
qt_arch: clang_64
qt_modules: qtimageformats qtmultimedia qtwebsockets qt_modules: qtimageformats qtmultimedia qtwebsockets
soc: Apple soc: Apple
type: Release type: Release
@ -316,11 +310,10 @@ jobs:
- os: macOS - os: macOS
target: 15 target: 15
runner: macos-15 runner: macos-15
ccache_eviction_age: 7d ccache_eviction_age: 7d
cmake_generator: Ninja cmake_generator: Ninja
qt_version: 6.11.0 qt_version: 6.11.0
qt_arch: clang_64
qt_modules: qtimageformats qtmultimedia qtwebsockets qt_modules: qtimageformats qtmultimedia qtwebsockets
soc: Apple soc: Apple
type: Debug type: Debug
@ -330,13 +323,12 @@ jobs:
- os: Windows - os: Windows
target: 10 target: 10
runner: windows-2025 runner: windows-2025
cmake_generator: "Visual Studio 17 2022" cmake_generator: "Visual Studio 18 2026"
cmake_generator_platform: x64 cmake_generator_platform: x64
make_package: 1 make_package: 1
package_suffix: "-Win10" package_suffix: "-Win10"
qt_version: 6.11.0 qt_version: 6.11.0
qt_arch: win64_msvc2022_64
qt_modules: qtimageformats qtmultimedia qtwebsockets qt_modules: qtimageformats qtmultimedia qtwebsockets
type: Release type: Release
@ -350,7 +342,7 @@ jobs:
steps: steps:
- name: "Checkout" - name: "Checkout"
uses: actions/checkout@v6 uses: actions/checkout@v7
with: with:
submodules: recursive submodules: recursive
@ -368,7 +360,7 @@ jobs:
- name: "[macOS] Restore compiler cache (ccache)" - name: "[macOS] Restore compiler cache (ccache)"
if: matrix.os == 'macOS' && matrix.use_ccache == 1 if: matrix.os == 'macOS' && matrix.use_ccache == 1
id: ccache_restore id: ccache_restore
uses: actions/cache/restore@v5 uses: actions/cache/restore@v6
env: env:
BRANCH_NAME: ${{ github.head_ref || github.ref_name }} BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
with: with:
@ -381,14 +373,16 @@ jobs:
# Resolve given wildcard versions (e.g. Qt 6.6.*) to latest version via aqtinstall to avoid stale caches on new releases # Resolve given wildcard versions (e.g. Qt 6.6.*) to latest version via aqtinstall to avoid stale caches on new releases
- name: "Resolve latest Qt patch version" - name: "Resolve latest Qt patch version"
env:
QT_VERSION: ${{ matrix.qt_version }}
id: resolve_qt_version id: resolve_qt_version
shell: bash shell: bash
run: .ci/resolve_latest_aqt_qt_version.sh "${{ matrix.qt_version }}" run: .ci/resolve_latest_aqt_qt_version.sh "$QT_VERSION"
- name: "[macOS] Restore thin Qt ${{ steps.resolve_qt_version.outputs.version }} libraries" - name: "[macOS] Restore thin Qt ${{ steps.resolve_qt_version.outputs.version }} libraries"
if: matrix.os == 'macOS' if: matrix.os == 'macOS'
id: restore_qt id: restore_qt
uses: actions/cache/restore@v5 uses: actions/cache/restore@v6
with: with:
key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }} key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }}
path: ${{ github.workspace }}/Qt path: ${{ github.workspace }}/Qt
@ -399,7 +393,6 @@ jobs:
if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true' if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true'
uses: jurplel/install-qt-action@v4 uses: jurplel/install-qt-action@v4
with: with:
arch: ${{ matrix.qt_arch }}
cache: false cache: false
dir: ${{ github.workspace }} dir: ${{ github.workspace }}
modules: ${{ matrix.qt_modules }} modules: ${{ matrix.qt_modules }}
@ -411,7 +404,7 @@ jobs:
- name: "[macOS] Cache thin Qt libraries" - name: "[macOS] Cache thin Qt libraries"
if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true' if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true'
uses: actions/cache/save@v5 uses: actions/cache/save@v6
with: with:
key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }} key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }}
path: ${{ github.workspace }}/Qt path: ${{ github.workspace }}/Qt
@ -422,7 +415,6 @@ jobs:
with: with:
# Qt 6.11.0 only works with aqtinstall directly from git until aqtinstall 3.4 is released # 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 aqtsource: git+https://github.com/miurahr/aqtinstall.git
arch: ${{ matrix.qt_arch }}
cache: true cache: true
modules: ${{ matrix.qt_modules }} modules: ${{ matrix.qt_modules }}
version: ${{ steps.resolve_qt_version.outputs.version }} version: ${{ steps.resolve_qt_version.outputs.version }}
@ -448,6 +440,7 @@ jobs:
CMAKE_GENERATOR: ${{ matrix.cmake_generator }} CMAKE_GENERATOR: ${{ matrix.cmake_generator }}
CMAKE_GENERATOR_PLATFORM: ${{ matrix.cmake_generator_platform }} CMAKE_GENERATOR_PLATFORM: ${{ matrix.cmake_generator_platform }}
DEVELOPER_DIR: '/Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer' DEVELOPER_DIR: '/Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer'
GITHUB_TOKEN: ${{ github.token }} # needed for vcpkg dependency graph updates, see VCPKG_FEATURE_FLAGS
MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }} MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }} MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }} MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }}
@ -458,6 +451,7 @@ jobs:
USE_CCACHE: ${{ matrix.use_ccache }} USE_CCACHE: ${{ matrix.use_ccache }}
VCPKG_BINARY_SOURCES: 'clear;files,${{ steps.vcpkg-cache.outputs.path }},readwrite' VCPKG_BINARY_SOURCES: 'clear;files,${{ steps.vcpkg-cache.outputs.path }},readwrite'
VCPKG_DISABLE_METRICS: 1 VCPKG_DISABLE_METRICS: 1
VCPKG_FEATURE_FLAGS: dependencygraph
run: .ci/compile.sh --server --test --vcpkg run: .ci/compile.sh --server --test --vcpkg
# Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342 # Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342
@ -465,15 +459,16 @@ jobs:
if: matrix.os == 'macOS' && matrix.use_ccache == 1 && github.ref == 'refs/heads/master' && steps.ccache_restore.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 continue-on-error: true
env: env:
CACHE_PRIMARY_KEY: ${{ steps.ccache_restore.outputs.cache-primary-key }}
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
run: | run: |
if gh cache delete --repo ${{ github.repository }} ${{ steps.ccache_restore.outputs.cache-primary-key }}; then if gh cache delete --repo "$GITHUB_REPOSITORY" "$CACHE_PRIMARY_KEY"; then
echo "Cache deleted successfully" echo "Cache deleted successfully"
fi fi
- name: "[macOS] Save updated compiler cache (ccache)" - name: "[macOS] Save updated compiler cache (ccache)"
if: matrix.os == 'macOS' && matrix.use_ccache == 1 && github.ref == 'refs/heads/master' if: matrix.os == 'macOS' && matrix.use_ccache == 1 && github.ref == 'refs/heads/master'
uses: actions/cache/save@v5 uses: actions/cache/save@v6
with: with:
key: ${{ steps.ccache_restore.outputs.cache-primary-key }} key: ${{ steps.ccache_restore.outputs.cache-primary-key }}
path: ${{ env.CCACHE_DIR }} path: ${{ env.CCACHE_DIR }}
@ -482,18 +477,20 @@ jobs:
if: matrix.os == 'macOS' && matrix.make_package && needs.configure.outputs.tag != null if: matrix.os == 'macOS' && matrix.make_package && needs.configure.outputs.tag != null
id: sign_macos id: sign_macos
env: env:
BUILD_PATH: ${{ steps.build.outputs.path }}
MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }} MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }} MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}
run: | run: |
if [[ -n "$MACOS_CERTIFICATE_NAME" ]] if [[ -n "$MACOS_CERTIFICATE_NAME" ]]
then then
security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
/usr/bin/codesign --sign="$MACOS_CERTIFICATE_NAME" --entitlements=".ci/macos.entitlements" --options=runtime --force --deep --timestamp --verbose "${{ steps.build.outputs.path }}" /usr/bin/codesign --sign="$MACOS_CERTIFICATE_NAME" --entitlements=".ci/macos.entitlements" --options=runtime --force --deep --timestamp --verbose "$BUILD_PATH"
fi fi
- name: "[macOS] Notarize app bundle" - name: "[macOS] Notarize app bundle"
if: matrix.os == 'macOS' && steps.sign_macos.outcome == 'success' if: matrix.os == 'macOS' && steps.sign_macos.outcome == 'success'
env: env:
BUILD_PATH: ${{ steps.build.outputs.path }}
MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }} MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }}
MACOS_NOTARIZATION_PWD: ${{ secrets.PROD_MACOS_NOTARIZATION_PWD }} MACOS_NOTARIZATION_PWD: ${{ secrets.PROD_MACOS_NOTARIZATION_PWD }}
MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }} MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }}
@ -508,7 +505,7 @@ jobs:
# Therefore, we create a zip file containing our app bundle, so that we can send it to the # Therefore, we create a zip file containing our app bundle, so that we can send it to the
# notarization service # notarization service
echo "Creating temp notarization archive" echo "Creating temp notarization archive"
ditto -c -k --keepParent "${{ steps.build.outputs.path }}" "notarization.zip" ditto -c -k --keepParent "$BUILD_PATH" "notarization.zip"
# Here we send the notarization request to the Apple's Notarization service, waiting for the result. # Here we send the notarization request to the Apple's Notarization service, waiting for the result.
# This typically takes a few seconds inside a CI environment, but it might take more depending on the App # This typically takes a few seconds inside a CI environment, but it might take more depending on the App
@ -520,7 +517,7 @@ jobs:
# Finally, we need to "attach the staple" to our executable, which will allow our app to be # Finally, we need to "attach the staple" to our executable, which will allow our app to be
# validated by macOS even when an internet connection is not available. # validated by macOS even when an internet connection is not available.
echo "Attach staple" echo "Attach staple"
xcrun stapler staple "${{ steps.build.outputs.path }}" xcrun stapler staple "$BUILD_PATH"
fi fi
- name: "Upload artifact" - name: "Upload artifact"
@ -566,5 +563,6 @@ jobs:
if: steps.attestation.outcome == 'success' if: steps.attestation.outcome == 'success'
shell: bash shell: bash
env: env:
BUILD_PATH: ${{ steps.build.outputs.path }}
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
run: gh attestation verify "${{ steps.build.outputs.path }}" --repo Cockatrice/Cockatrice run: gh attestation verify "$BUILD_PATH" --repo Cockatrice/Cockatrice

View file

@ -22,7 +22,7 @@ jobs:
steps: steps:
- name: "Checkout" - name: "Checkout"
uses: actions/checkout@v6 uses: actions/checkout@v7
with: with:
fetch-depth: 20 # should be enough to find merge base fetch-depth: 20 # should be enough to find merge base

View file

@ -31,7 +31,7 @@ jobs:
steps: steps:
- name: "Checkout" - name: "Checkout"
uses: actions/checkout@v6 uses: actions/checkout@v7
- name: "Docker metadata" - name: "Docker metadata"
id: metadata id: metadata
@ -56,8 +56,9 @@ jobs:
- name: "Set up Docker buildx" - name: "Set up Docker buildx"
uses: docker/setup-buildx-action@v4 uses: docker/setup-buildx-action@v4
- name: "Login to GitHub Container Registry" - name: "Login to GitHub Container Registry (GHCR)"
if: contains(github.event.release.tag_name, 'Release') && github.event.release.target_commitish == 'master' if: github.event_name == 'release' && github.event.release.prerelease == false
id: login
uses: docker/login-action@v4 uses: docker/login-action@v4
with: with:
password: ${{ github.token }} password: ${{ github.token }}
@ -73,5 +74,5 @@ jobs:
context: . context: .
labels: ${{ steps.metadata.outputs.labels }} labels: ${{ steps.metadata.outputs.labels }}
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
push: ${{ github.ref_type == 'tag' }} push: ${{ steps.login.outcome == 'success' }}
tags: ${{ steps.metadata.outputs.tags }} tags: ${{ steps.metadata.outputs.tags }}

View file

@ -21,7 +21,7 @@ jobs:
steps: steps:
- name: "Checkout code" - name: "Checkout code"
uses: actions/checkout@v6 uses: actions/checkout@v7
with: with:
submodules: recursive submodules: recursive

View file

@ -20,9 +20,11 @@ jobs:
steps: steps:
- name: "Checkout repo" - name: "Checkout repo"
uses: actions/checkout@v6 uses: actions/checkout@v7
- name: "Pull translated strings from Transifex" - name: "Pull translated strings from Transifex"
# Do not run this step for PR's from forks, they don't have access to the secret
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false
uses: transifex/cli-action@v2 uses: transifex/cli-action@v2
with: with:
# Used config file: https://github.com/Cockatrice/Cockatrice/blob/master/.tx/config # Used config file: https://github.com/Cockatrice/Cockatrice/blob/master/.tx/config
@ -41,11 +43,11 @@ jobs:
author: github-actions <github-actions@github.com> # owner of the commit author: github-actions <github-actions@github.com> # owner of the commit
body: | body: |
Pulled all translated strings from [Transifex][1]. Pulled all translated strings from [Transifex][1].
--- ---
*This PR is automatically generated and updated by the workflow at `.github/workflows/translations-pull.yml`. Review [action runs][2].*<br> *This PR is automatically generated and updated by the workflow at `.github/workflows/translations-pull.yml`. Review [action runs][2].*<br>
*After merging, all new languages and translations are available in the next build.* *After merging, all new languages and translations are available in the next build.*
[1]: https://explore.transifex.com/cockatrice/cockatrice/ [1]: https://explore.transifex.com/cockatrice/cockatrice/
[2]: https://github.com/Cockatrice/Cockatrice/actions/workflows/translations-pull.yml?query=branch%3Amaster [2]: https://github.com/Cockatrice/Cockatrice/actions/workflows/translations-pull.yml?query=branch%3Amaster
branch: ci-update_translations branch: ci-update_translations
@ -61,11 +63,9 @@ jobs:
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
shell: bash shell: bash
env: env:
STATUS: ${{ steps.create_pr.outputs.pull-request-operation }} PR_NUMBER: ${{ steps.create_pr.outputs.pull-request-number }}
PR_URL: ${{ steps.create_pr.outputs.pull-request-url }}
STATUS: ${{ case(steps.create_pr.outputs.pull-request-operation == 'none', 'unchanged', steps.create_pr.outputs.pull-request-operation) }}
run: | run: |
if [[ "$STATUS" == "none" ]]; then echo "PR #$PR_NUMBER $STATUS!" >> "$GITHUB_STEP_SUMMARY"
echo "PR #${{ steps.create_pr.outputs.pull-request-number }} unchanged!" >> $GITHUB_STEP_SUMMARY echo "URL: $PR_URL" >> "$GITHUB_STEP_SUMMARY"
else
echo "PR #${{ steps.create_pr.outputs.pull-request-number }} $STATUS!" >> $GITHUB_STEP_SUMMARY
fi
echo "URL: ${{ steps.create_pr.outputs.pull-request-url }}" >> $GITHUB_STEP_SUMMARY

View file

@ -20,7 +20,7 @@ jobs:
steps: steps:
- name: "Checkout repo" - name: "Checkout repo"
uses: actions/checkout@v6 uses: actions/checkout@v7
- name: "Install lupdate" - name: "Install lupdate"
shell: bash shell: bash
@ -29,12 +29,13 @@ jobs:
sudo apt-get install -y --no-install-recommends qttools5-dev-tools sudo apt-get install -y --no-install-recommends qttools5-dev-tools
- name: "Update Cockatrice translation source" - name: "Update Cockatrice translation source"
env:
FILE: cockatrice/cockatrice_en@source.ts
id: cockatrice id: cockatrice
shell: bash shell: bash
run: | run: >
FILE="cockatrice/cockatrice_en@source.ts" DIRS="cockatrice/src $(find . -maxdepth 1 -type d -name 'libcockatrice_*')"
export DIRS="cockatrice/src $(find . -maxdepth 1 -type d -name 'libcockatrice_*')" .ci/update_translation_source_strings.sh
FILE="$FILE" DIRS="$DIRS" .ci/update_translation_source_strings.sh
- name: "Update Oracle translation source" - name: "Update Oracle translation source"
id: oracle id: oracle
@ -77,11 +78,9 @@ jobs:
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
shell: bash shell: bash
env: env:
STATUS: ${{ steps.create_pr.outputs.pull-request-operation }} PR_NUMBER: ${{ steps.create_pr.outputs.pull-request-number }}
PR_URL: ${{ steps.create_pr.outputs.pull-request-url }}
STATUS: ${{ case(steps.create_pr.outputs.pull-request-operation == 'none', 'unchanged', steps.create_pr.outputs.pull-request-operation) }}
run: | run: |
if [[ "$STATUS" == "none" ]]; then echo "PR #$PR_NUMBER $STATUS!" >> "$GITHUB_STEP_SUMMARY"
echo "PR #${{ steps.create_pr.outputs.pull-request-number }} unchanged!" >> $GITHUB_STEP_SUMMARY echo "URL: $PR_URL" >> "$GITHUB_STEP_SUMMARY"
else
echo "PR #${{ steps.create_pr.outputs.pull-request-number }} $STATUS!" >> $GITHUB_STEP_SUMMARY
fi
echo "URL: ${{ steps.create_pr.outputs.pull-request-url }}" >> $GITHUB_STEP_SUMMARY

1
.gitignore vendored
View file

@ -6,6 +6,7 @@ mysql.cnf
.DS_Store .DS_Store
.idea/ .idea/
*.aps *.aps
*.cache
cmake-build* cmake-build*
preferences preferences
compile_commands.json compile_commands.json

View file

@ -64,10 +64,11 @@ if(WIN32 OR USE_VCPKG)
else() else()
set(QTDIR set(QTDIR
"" ""
CACHE PATH "Path to Qt (e.g. C:/Qt/5.7/msvc2015_64)" CACHE PATH "Path to Qt (e.g. C:/Qt/6.4.2/msvc2019_64)"
) )
message( message(
WARNING "QTDIR variable is missing. Please set this variable to specify path to Qt (e.g. C:/Qt/5.7/msvc2015_64)" WARNING
"QTDIR variable is missing. Please set this variable to specify path to Qt (e.g. C:/Qt/6.4.2/msvc2019_64)"
) )
endif() endif()
endif() endif()
@ -174,7 +175,7 @@ elseif(CMAKE_COMPILER_IS_GNUCXX)
-Wno-error=delete-non-virtual-dtor -Wno-error=delete-non-virtual-dtor
-Wno-error=sign-compare -Wno-error=sign-compare
-Wno-error=missing-declarations -Wno-error=missing-declarations
-Wno-error=sfinae-incomplete # GCC 16+: Qt MOC + protobuf forward decls trigger this -Wno-error=sfinae-incomplete # GCC 16+: Qt MOC + protobuf forward decls trigger this
) )
foreach(FLAG ${ADDITIONAL_DEBUG_FLAGS}) foreach(FLAG ${ADDITIONAL_DEBUG_FLAGS})
@ -280,11 +281,7 @@ if(UNIX)
if(CPACK_GENERATOR STREQUAL "RPM") if(CPACK_GENERATOR STREQUAL "RPM")
set(CPACK_RPM_PACKAGE_LICENSE "GPLv2") set(CPACK_RPM_PACKAGE_LICENSE "GPLv2")
set(CPACK_RPM_MAIN_COMPONENT "cockatrice") set(CPACK_RPM_MAIN_COMPONENT "cockatrice")
if(Qt6_FOUND) set(CPACK_RPM_PACKAGE_REQUIRES "protobuf, qt6-qttools, qt6-qtsvg, qt6-qtmultimedia, qt6-qtimageformats")
set(CPACK_RPM_PACKAGE_REQUIRES "protobuf, qt6-qttools, qt6-qtsvg, qt6-qtmultimedia, qt6-qtimageformats")
elseif(Qt5_FOUND)
set(CPACK_RPM_PACKAGE_REQUIRES "protobuf, qt5-qttools, qt5-qtsvg, qt5-qtmultimedia")
endif()
set(CPACK_RPM_PACKAGE_GROUP "Amusements/Games") set(CPACK_RPM_PACKAGE_GROUP "Amusements/Games")
set(CPACK_RPM_PACKAGE_URL "http://github.com/Cockatrice/Cockatrice") set(CPACK_RPM_PACKAGE_URL "http://github.com/Cockatrice/Cockatrice")
# stop directories from making package conflicts # stop directories from making package conflicts
@ -302,12 +299,8 @@ if(UNIX)
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
set(CPACK_DEBIAN_PACKAGE_SECTION "games") set(CPACK_DEBIAN_PACKAGE_SECTION "games")
set(CPACK_DEBIAN_PACKAGE_HOMEPAGE "http://github.com/Cockatrice/Cockatrice") set(CPACK_DEBIAN_PACKAGE_HOMEPAGE "http://github.com/Cockatrice/Cockatrice")
if(Qt6_FOUND) set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt6multimedia6, libqt6svg6, qt6-qpa-plugins, qt6-image-formats-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
set(CPACK_DEBIAN_PACKAGE_RECOMMENDS "libqt6sql6-mysql") # for connecting servatrice to a mysql db
elseif(Qt5_FOUND)
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt5multimedia5-plugins, libqt5svg5")
endif()
endif() endif()
endif() endif()
elseif(WIN32) elseif(WIN32)

View file

@ -349,7 +349,7 @@ OPTIMIZE_OUTPUT_SLICE = NO
# #
# Note see also the list of default file extension mappings. # Note see also the list of default file extension mappings.
EXTENSION_MAPPING = EXTENSION_MAPPING = proto=C++
# If the MARKDOWN_SUPPORT tag is enabled then Doxygen pre-processes all comments # If the MARKDOWN_SUPPORT tag is enabled then Doxygen pre-processes all comments
# according to the Markdown format, which allows for more readable # according to the Markdown format, which allows for more readable
@ -1086,7 +1086,8 @@ FILE_PATTERNS = *.cc \
*.h++ \ *.h++ \
*.markdown \ *.markdown \
*.md \ *.md \
*.dox *.dox \
*.proto
# The RECURSIVE tag can be used to specify whether or not subdirectories should # The RECURSIVE tag can be used to specify whether or not subdirectories should
# be searched for input files as well. # be searched for input files as well.
@ -1103,6 +1104,7 @@ RECURSIVE = YES
EXCLUDE = build/ \ EXCLUDE = build/ \
cmake/ \ cmake/ \
cmake-build-debug/ \
doc/doxygen/theme/docs/ \ doc/doxygen/theme/docs/ \
doc/doxygen/theme/include/ \ doc/doxygen/theme/include/ \
vcpkg/ vcpkg/
@ -1195,7 +1197,7 @@ INPUT_FILTER =
# need to set EXTENSION_MAPPING for the extension otherwise the files are not # need to set EXTENSION_MAPPING for the extension otherwise the files are not
# properly processed by Doxygen. # properly processed by Doxygen.
FILTER_PATTERNS = FILTER_PATTERNS = "*.proto=python doc/doxygen/filters/proto2cpp.py"
# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using # If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using
# INPUT_FILTER) will also be used to filter the input files that are used for # INPUT_FILTER) will also be used to filter the input files that are used for

View file

@ -158,7 +158,6 @@ The following flags (with their non-default values) can be passed to `cmake`:
| `-DWARNING_AS_ERROR=0` | Don't treat compilation warnings as errors in debug mode | | `-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<br> **Note:** `make clean` will remove the .ts files | | `-DUPDATE_TRANSLATIONS=1` | Configure `make` to update the translation .ts files for new strings in the source code<br> **Note:** `make clean` will remove the .ts files |
| `-DTEST=1` | Enable regression tests<br> **Note:** `make test` to run tests, *googletest* will be downloaded if not available | | `-DTEST=1` | Enable regression tests<br> **Note:** `make test` to run tests, *googletest* will be downloaded if not available |
| `-DFORCE_USE_QT5=1` | Skip looking for Qt6 before trying to find Qt5 |
# Run # Run

View file

@ -1,8 +1,7 @@
# Find a compatible Qt version # Find a compatible Qt version
# Inputs: WITH_SERVER, WITH_CLIENT, WITH_ORACLE, FORCE_USE_QT5 # Inputs: WITH_SERVER, WITH_CLIENT, WITH_ORACLE
# Optional Input: QT6_DIR -- Hint as to where Qt6 lives on the system # Optional Input: QT6_DIR -- Hint as to where Qt6 lives on the system
# Optional Input: QT5_DIR -- Hint as to where Qt5 lives on the system # Output: COCKATRICE_QT_VERSION_NAME -- Example values: Qt6
# Output: COCKATRICE_QT_VERSION_NAME -- Example values: Qt5, Qt6
# Output: SERVATRICE_QT_MODULES # Output: SERVATRICE_QT_MODULES
# Output: COCKATRICE_QT_MODULES # Output: COCKATRICE_QT_MODULES
# Output: ORACLE_QT_MODULES # Output: ORACLE_QT_MODULES
@ -39,69 +38,37 @@ set(REQUIRED_QT_COMPONENTS ${REQUIRED_QT_COMPONENTS} ${_SERVATRICE_NEEDED} ${_CO
) )
list(REMOVE_DUPLICATES REQUIRED_QT_COMPONENTS) list(REMOVE_DUPLICATES REQUIRED_QT_COMPONENTS)
if(NOT FORCE_USE_QT5) # Linguist is now a component in Qt6 instead of an external package
# Linguist is now a component in Qt6 instead of an external package find_package(
find_package( Qt6 6.4.2
Qt6 6.4.2 COMPONENTS ${REQUIRED_QT_COMPONENTS} Linguist
COMPONENTS ${REQUIRED_QT_COMPONENTS} Linguist QUIET HINTS ${Qt6_DIR}
QUIET HINTS ${Qt6_DIR} )
) if(NOT Qt6_FOUND)
message(FATAL_ERROR "No suitable version of Qt was found")
endif() endif()
if(Qt6_FOUND) set(COCKATRICE_QT_VERSION_NAME Qt6)
set(COCKATRICE_QT_VERSION_NAME Qt6)
list(FIND Qt6LinguistTools_TARGETS Qt6::lrelease QT6_LRELEASE_INDEX) list(FIND Qt6LinguistTools_TARGETS Qt6::lrelease QT6_LRELEASE_INDEX)
if(QT6_LRELEASE_INDEX EQUAL -1) if(QT6_LRELEASE_INDEX EQUAL -1)
message(WARNING "Qt6 lrelease not found.") message(WARNING "Qt6 lrelease not found.")
endif()
list(FIND Qt6LinguistTools_TARGETS Qt6::lupdate QT6_LUPDATE_INDEX)
if(QT6_LUPDATE_INDEX EQUAL -1)
message(WARNING "Qt6 lupdate not found.")
endif()
else()
find_package(
Qt5 5.15.2
COMPONENTS ${REQUIRED_QT_COMPONENTS}
QUIET HINTS ${Qt5_DIR}
)
if(Qt5_FOUND)
set(COCKATRICE_QT_VERSION_NAME Qt5)
else()
message(FATAL_ERROR "No suitable version of Qt was found")
endif()
# Qt5 Linguist is in a separate package
find_package(Qt5LinguistTools QUIET)
if(Qt5LinguistTools_FOUND)
if(NOT Qt5_LRELEASE_EXECUTABLE)
message(WARNING "Qt5 lrelease not found.")
endif()
if(NOT Qt5_LUPDATE_EXECUTABLE)
message(WARNING "Qt5 lupdate not found.")
endif()
else()
message(WARNING "Linguist Tools not found, cannot handle translations")
endif()
endif() endif()
if(Qt5_POSITION_INDEPENDENT_CODE OR Qt6_FOUND) list(FIND Qt6LinguistTools_TARGETS Qt6::lupdate QT6_LUPDATE_INDEX)
set(CMAKE_POSITION_INDEPENDENT_CODE ON) if(QT6_LUPDATE_INDEX EQUAL -1)
message(WARNING "Qt6 lupdate not found.")
endif() endif()
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
# Establish Qt Plugins directory & Library directories # Establish Qt Plugins directory & Library directories
get_target_property(QT_LIBRARY_DIR ${COCKATRICE_QT_VERSION_NAME}::Core LOCATION) get_target_property(QT_LIBRARY_DIR ${COCKATRICE_QT_VERSION_NAME}::Core LOCATION)
get_filename_component(QT_LIBRARY_DIR ${QT_LIBRARY_DIR} DIRECTORY) get_filename_component(QT_LIBRARY_DIR ${QT_LIBRARY_DIR} DIRECTORY)
if(Qt6_FOUND) get_filename_component(QT_PLUGINS_DIR "${Qt6Core_DIR}/../../../${QT6_INSTALL_PLUGINS}" ABSOLUTE)
get_filename_component(QT_PLUGINS_DIR "${Qt6Core_DIR}/../../../${QT6_INSTALL_PLUGINS}" ABSOLUTE) get_filename_component(QT_LIBRARY_DIR "${QT_LIBRARY_DIR}/../../.." ABSOLUTE)
get_filename_component(QT_LIBRARY_DIR "${QT_LIBRARY_DIR}/../../.." ABSOLUTE) if(UNIX AND APPLE)
if(UNIX AND APPLE) # Mac needs a bit more help finding all necessary components
# Mac needs a bit more help finding all necessary components list(APPEND QT_LIBRARY_DIR "/usr/local/lib")
list(APPEND QT_LIBRARY_DIR "/usr/local/lib")
endif()
elseif(Qt5_FOUND)
get_filename_component(QT_PLUGINS_DIR "${Qt5Core_DIR}/../../../plugins" ABSOLUTE)
get_filename_component(QT_LIBRARY_DIR "${QT_LIBRARY_DIR}/.." ABSOLUTE)
endif() endif()
message(DEBUG "QT_PLUGINS_DIR = ${QT_PLUGINS_DIR}") message(DEBUG "QT_PLUGINS_DIR = ${QT_PLUGINS_DIR}")
message(DEBUG "QT_LIBRARY_DIR = ${QT_LIBRARY_DIR}") message(DEBUG "QT_LIBRARY_DIR = ${QT_LIBRARY_DIR}")

View file

@ -1,38 +1,118 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<!-- ========================= -->
<!-- CORE BUNDLE METADATA -->
<!-- ========================= -->
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>English</string> <string>English</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string> <string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
<key>CFBundleGetInfoString</key> <key>CFBundleGetInfoString</key>
<string>${MACOSX_BUNDLE_INFO_STRING}</string> <string>${MACOSX_BUNDLE_INFO_STRING}</string>
<key>CFBundleIconFile</key> <key>CFBundleIconFile</key>
<string>${MACOSX_BUNDLE_ICON_FILE}</string> <string>${MACOSX_BUNDLE_ICON_FILE}</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string> <string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleLongVersionString</key> <key>CFBundleLongVersionString</key>
<string>${MACOSX_BUNDLE_LONG_VERSION_STRING}</string> <string>${MACOSX_BUNDLE_LONG_VERSION_STRING}</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>${MACOSX_BUNDLE_BUNDLE_NAME}</string> <string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string> <string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string> <string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
<key>CSResourcesFileMapped</key>
<true/>
<key>LSRequiresCarbon</key>
<true/>
<key>NSHumanReadableCopyright</key> <key>NSHumanReadableCopyright</key>
<string>${MACOSX_BUNDLE_COPYRIGHT}</string> <string>${MACOSX_BUNDLE_COPYRIGHT}</string>
<key>NSHighResolutionCapable</key> <key>NSHighResolutionCapable</key>
<true/> <true/>
<!-- ========================= -->
<!-- FILE TYPE (.cod) SUPPORT -->
<!-- ========================= -->
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeIdentifier</key>
<string>org.cockatrice.deck</string>
<key>UTTypeDescription</key>
<string>Cockatrice Deck</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
</array>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>cod</string>
</array>
</dict>
</dict>
</array>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>Cockatrice Deck</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSHandlerRank</key>
<string>Default</string>
<key>LSItemContentTypes</key>
<array>
<string>org.cockatrice.deck</string>
</array>
</dict>
</array>
<!-- ========================= -->
<!-- URL SCHEME (cockatrice://) -->
<!-- ========================= -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>Cockatrice URL Scheme</string>
<key>CFBundleURLSchemes</key>
<array>
<string>cockatrice</string>
</array>
</dict>
</array>
</dict> </dict>
</plist> </plist>

View file

@ -117,21 +117,22 @@ ${If} $InstDir == ""
; we need to set a default based on the install mode ; we need to set a default based on the install mode
StrCpy $InstDir $0 StrCpy $InstDir $0
${EndIf} ${EndIf}
Call SetModeDestinationFromInstdir
; --- Detect portable install when using /R --- ; --- Detect portable install when using /R (must come BEFORE SetModeDestinationFromInstdir) ---
${If} $ReinstallMode = 1 ${If} $ReinstallMode = 1
IfFileExists "$InstDir\portable.dat" 0 not_portable IfFileExists "$InstDir\portable.dat" 0 not_portable
StrCpy $PortableMode 1 StrCpy $PortableMode 1
Goto portable_done Goto portable_done
not_portable: not_portable:
StrCpy $PortableMode 0 StrCpy $PortableMode 0
portable_done: portable_done:
${EndIf} ${EndIf}
; Now that $PortableMode reflects reality, commit InstDir into the correct slot
Call SetModeDestinationFromInstdir
${If} $ReinstallMode = 1 ${If} $ReinstallMode = 1
${AndIf} $PortableMode = 0
Call AutoUninstallIfNeeded Call AutoUninstallIfNeeded
${EndIf} ${EndIf}

View file

@ -97,7 +97,11 @@ set(cockatrice_SOURCES
src/game_graphics/player/menu/rfg_menu.cpp src/game_graphics/player/menu/rfg_menu.cpp
src/game_graphics/player/menu/say_menu.cpp src/game_graphics/player/menu/say_menu.cpp
src/game_graphics/player/menu/sideboard_menu.cpp src/game_graphics/player/menu/sideboard_menu.cpp
src/game_graphics/player/menu/tally_menu.cpp
src/game_graphics/player/menu/utility_menu.cpp src/game_graphics/player/menu/utility_menu.cpp
src/game_graphics/tally/stats_tally.cpp
src/game_graphics/tally/subtype_tally.cpp
src/game_graphics/tally/tally.cpp
src/game/player/player_actions.cpp src/game/player/player_actions.cpp
src/game_graphics/player/player_area.cpp src/game_graphics/player/player_area.cpp
src/game_graphics/player/player_dialogs.cpp src/game_graphics/player/player_dialogs.cpp
@ -131,9 +135,19 @@ set(cockatrice_SOURCES
src/interface/card_picture_loader/card_picture_loader_worker.cpp src/interface/card_picture_loader/card_picture_loader_worker.cpp
src/interface/card_picture_loader/card_picture_loader_worker_work.cpp src/interface/card_picture_loader/card_picture_loader_worker_work.cpp
src/interface/card_picture_loader/card_picture_to_load.cpp src/interface/card_picture_loader/card_picture_to_load.cpp
src/interface/intents/intent.cpp
src/interface/intents/intent.h
src/interface/intents/intent_open_local_deck.cpp
src/interface/intents/intent_open_local_deck.h
src/interface/intents/intent_wait_for_database_load.cpp
src/interface/intents/intent_wait_for_database_load.h
src/interface/layouts/flow_layout.cpp src/interface/layouts/flow_layout.cpp
src/interface/layouts/overlap_layout.cpp src/interface/layouts/overlap_layout.cpp
src/interface/widgets/utility/card_completer_delegate.cpp
src/interface/widgets/utility/card_completer_styler.cpp
src/interface/widgets/utility/completer_utils.cpp
src/interface/widgets/utility/line_edit_completer.cpp src/interface/widgets/utility/line_edit_completer.cpp
src/interface/widgets/utility/reversed_completer_model.cpp
src/interface/pixel_map_generator.cpp src/interface/pixel_map_generator.cpp
src/interface/theme_config.cpp src/interface/theme_config.cpp
src/interface/theme_manager.cpp src/interface/theme_manager.cpp
@ -227,7 +241,9 @@ set(cockatrice_SOURCES
src/interface/widgets/quick_settings/settings_button_widget.cpp src/interface/widgets/quick_settings/settings_button_widget.cpp
src/interface/widgets/quick_settings/settings_popup_widget.cpp src/interface/widgets/quick_settings/settings_popup_widget.cpp
src/interface/widgets/replay/replay_manager.cpp src/interface/widgets/replay/replay_manager.cpp
src/interface/widgets/replay/replay_quick_settings_widget.cpp
src/interface/widgets/replay/replay_timeline_widget.cpp src/interface/widgets/replay/replay_timeline_widget.cpp
src/interface/widgets/replay/replay_widget.cpp
src/interface/widgets/server/chat_view/chat_view.cpp src/interface/widgets/server/chat_view/chat_view.cpp
src/interface/widgets/server/game_filter_configs.cpp src/interface/widgets/server/game_filter_configs.cpp
src/interface/widgets/server/game_selector.cpp src/interface/widgets/server/game_selector.cpp
@ -236,15 +252,22 @@ set(cockatrice_SOURCES
src/interface/widgets/server/handle_public_servers.cpp src/interface/widgets/server/handle_public_servers.cpp
src/interface/widgets/server/remote/remote_decklist_tree_widget.cpp src/interface/widgets/server/remote/remote_decklist_tree_widget.cpp
src/interface/widgets/server/remote/remote_replay_list_tree_widget.cpp src/interface/widgets/server/remote/remote_replay_list_tree_widget.cpp
src/interface/widgets/server/user/user_avatar_provider.cpp
src/interface/widgets/server/user/user_card_art_provider.cpp
src/interface/widgets/server/user/user_card_settings_dialog.cpp
src/interface/widgets/server/user/user_context_menu.cpp 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_box.cpp
src/interface/widgets/server/user/user_info_connection.cpp src/interface/widgets/server/user/user_info_connection.cpp
src/interface/widgets/server/user/user_list_manager.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_widget.cpp src/interface/widgets/server/user/user_list_widget.cpp
src/interface/widgets/settings_page/abstract_settings_page.cpp
src/interface/widgets/settings_page/appearance_settings_page.cpp src/interface/widgets/settings_page/appearance_settings_page.cpp
src/interface/widgets/settings_page/deck_editor_settings_page.cpp src/interface/widgets/settings_page/deck_editor_settings_page.cpp
src/interface/widgets/settings_page/general_settings_page.cpp src/interface/widgets/settings_page/general_settings_page.cpp
src/interface/widgets/settings_page/messages_settings_page.cpp src/interface/widgets/settings_page/messages_settings_page.cpp
src/interface/widgets/settings_page/settings_search_delegate.cpp
src/interface/widgets/settings_page/settings_search_model.cpp
src/interface/widgets/settings_page/shortcut_settings_page.cpp src/interface/widgets/settings_page/shortcut_settings_page.cpp
src/interface/widgets/settings_page/sound_settings_page.cpp src/interface/widgets/settings_page/sound_settings_page.cpp
src/interface/widgets/settings_page/storage_settings_page.cpp src/interface/widgets/settings_page/storage_settings_page.cpp
@ -283,6 +306,7 @@ set(cockatrice_SOURCES
src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp
src/interface/window_main.cpp src/interface/window_main.cpp
src/main.cpp src/main.cpp
src/single_instance_manager.cpp
src/interface/widgets/tabs/abstract_tab_deck_editor.cpp src/interface/widgets/tabs/abstract_tab_deck_editor.cpp
src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp
src/interface/widgets/tabs/api/archidekt/api_response/archidekt_deck_listing_api_response.cpp src/interface/widgets/tabs/api/archidekt/api_response/archidekt_deck_listing_api_response.cpp
@ -298,6 +322,13 @@ set(cockatrice_SOURCES
src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.cpp src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.cpp
src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_listings_display_widget.cpp src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_listings_display_widget.cpp
src/interface/widgets/tabs/api/archidekt/display/archidekt_deck_preview_image_display_widget.cpp src/interface/widgets/tabs/api/archidekt/display/archidekt_deck_preview_image_display_widget.cpp
src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.cpp
src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.cpp
src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.cpp
src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.cpp
src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.cpp
src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.cpp
src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.cpp
src/interface/widgets/tabs/api/edhrec/api_response/archidekt_links/edhrec_api_response_archidekt_links.cpp src/interface/widgets/tabs/api/edhrec/api_response/archidekt_links/edhrec_api_response_archidekt_links.cpp
src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_average_deck_api_response.cpp src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_average_deck_api_response.cpp
src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_deck_api_response.cpp src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_deck_api_response.cpp
@ -327,6 +358,7 @@ set(cockatrice_SOURCES
src/interface/widgets/tabs/tab.cpp src/interface/widgets/tabs/tab.cpp
src/interface/widgets/tabs/tab_account.cpp src/interface/widgets/tabs/tab_account.cpp
src/interface/widgets/tabs/tab_admin.cpp src/interface/widgets/tabs/tab_admin.cpp
src/interface/widgets/tabs/tab_card_art_rules.cpp
src/interface/widgets/tabs/tab_deck_editor.cpp src/interface/widgets/tabs/tab_deck_editor.cpp
src/interface/widgets/tabs/tab_deck_storage.cpp src/interface/widgets/tabs/tab_deck_storage.cpp
src/interface/widgets/tabs/tab_game.cpp src/interface/widgets/tabs/tab_game.cpp
@ -343,12 +375,31 @@ set(cockatrice_SOURCES
src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.cpp src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.cpp
src/interface/key_signals.cpp src/interface/key_signals.cpp
src/interface/logger.cpp src/interface/logger.cpp
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/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.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_bracket_navigation_widget.h
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.cpp src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.cpp
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.h src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.h
src/interface/widgets/utility/compact_push_button.cpp src/interface/widgets/utility/compact_push_button.cpp
src/interface/widgets/utility/compact_push_button.h src/interface/widgets/utility/compact_push_button.h
src/single_instance_manager.h
src/client/url_scheme_event_filter.h
src/interface/intents/intent_connect_to_server.cpp
src/interface/intents/intent_connect_to_server.h
src/interface/intents/intent_disconnect_from_server.cpp
src/interface/intents/intent_disconnect_from_server.h
src/interface/intents/intent_join_server_game.cpp
src/interface/intents/intent_join_server_game.h
src/interface/intents/intent_join_server_room.cpp
src/interface/intents/intent_join_server_room.h
src/interface/intents/intent_login.cpp
src/interface/intents/intent_login.h
src/interface/intents/url_parser.cpp
src/interface/intents/url_parser.h
src/interface/widgets/server/user/user_info_popup.cpp
src/interface/widgets/server/user/user_info_popup.h
) )
add_subdirectory(sounds) add_subdirectory(sounds)
@ -390,11 +441,7 @@ if(APPLE)
set(cockatrice_SOURCES ${cockatrice_SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/resources/appicon.icns) set(cockatrice_SOURCES ${cockatrice_SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/resources/appicon.icns)
endif(APPLE) endif(APPLE)
if(Qt6_FOUND) qt6_add_resources(cockatrice_RESOURCES_RCC ${cockatrice_RESOURCES})
qt6_add_resources(cockatrice_RESOURCES_RCC ${cockatrice_RESOURCES})
elseif(Qt5_FOUND)
qt5_add_resources(cockatrice_RESOURCES_RCC ${cockatrice_RESOURCES})
endif()
# Declare path variables # Declare path variables
set(ICONDIR set(ICONDIR
@ -406,71 +453,37 @@ set(DESKTOPDIR
CACHE STRING "desktop file destination" CACHE STRING "desktop file destination"
) )
set(MIMEDIR
share/mime/packages
CACHE STRING "mime file destination"
)
set(COCKATRICE_MAC_QM_INSTALL_DIR "cockatrice.app/Contents/Resources/translations") set(COCKATRICE_MAC_QM_INSTALL_DIR "cockatrice.app/Contents/Resources/translations")
set(COCKATRICE_UNIX_QM_INSTALL_DIR "share/cockatrice/translations") set(COCKATRICE_UNIX_QM_INSTALL_DIR "share/cockatrice/translations")
set(COCKATRICE_WIN32_QM_INSTALL_DIR "translations") set(COCKATRICE_WIN32_QM_INSTALL_DIR "translations")
if(Qt6_FOUND) qt6_add_executable(
qt6_add_executable( cockatrice
cockatrice WIN32
WIN32 MACOSX_BUNDLE
MACOSX_BUNDLE ${cockatrice_SOURCES}
${cockatrice_SOURCES} ${cockatrice_RESOURCES_RCC}
${cockatrice_RESOURCES_RCC} ${cockatrice_MOC_SRCS}
${cockatrice_MOC_SRCS} MANUAL_FINALIZATION
MANUAL_FINALIZATION )
)
elseif(Qt5_FOUND)
# Qt5 Translations need to be linked at executable creation time
if(Qt5LinguistTools_FOUND)
if(UPDATE_TRANSLATIONS)
qt5_create_translation(cockatrice_QM ${translate_SRCS} ${cockatrice_TS})
else()
qt5_add_translation(cockatrice_QM ${cockatrice_TS})
endif()
endif()
add_executable(
cockatrice WIN32 MACOSX_BUNDLE ${cockatrice_MOC_SRCS} ${cockatrice_QM} ${cockatrice_RESOURCES_RCC}
${cockatrice_SOURCES}
)
if(UNIX)
if(APPLE)
install(FILES ${cockatrice_QM} DESTINATION ${COCKATRICE_MAC_QM_INSTALL_DIR})
else()
install(FILES ${cockatrice_QM} DESTINATION ${COCKATRICE_UNIX_QM_INSTALL_DIR})
endif()
elseif(WIN32)
install(FILES ${cockatrice_QM} DESTINATION ${COCKATRICE_WIN32_QM_INSTALL_DIR})
endif()
endif()
if(Qt5_FOUND) target_link_libraries(
target_link_libraries( cockatrice
cockatrice PUBLIC libcockatrice_card
libcockatrice_card libcockatrice_deck_list
libcockatrice_deck_list libcockatrice_filters
libcockatrice_filters libcockatrice_utility
libcockatrice_utility libcockatrice_network
libcockatrice_network libcockatrice_models
libcockatrice_models libcockatrice_rng
libcockatrice_rng libcockatrice_settings
libcockatrice_settings ${COCKATRICE_QT_MODULES}
${COCKATRICE_QT_MODULES} )
)
else()
target_link_libraries(
cockatrice
PUBLIC libcockatrice_card
libcockatrice_deck_list
libcockatrice_filters
libcockatrice_utility
libcockatrice_network
libcockatrice_models
libcockatrice_rng
libcockatrice_settings
${COCKATRICE_QT_MODULES}
)
endif()
if(UNIX) if(UNIX)
if(APPLE) if(APPLE)
@ -490,6 +503,23 @@ if(UNIX)
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/resources/cockatrice.png DESTINATION ${ICONDIR}/hicolor/48x48/apps) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/resources/cockatrice.png DESTINATION ${ICONDIR}/hicolor/48x48/apps)
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/resources/cockatrice.svg DESTINATION ${ICONDIR}/hicolor/scalable/apps) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/resources/cockatrice.svg DESTINATION ${ICONDIR}/hicolor/scalable/apps)
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/cockatrice.desktop DESTINATION ${DESKTOPDIR}) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/cockatrice.desktop DESTINATION ${DESKTOPDIR})
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/cockatrice-cod.xml DESTINATION ${MIMEDIR})
# Refresh the freedesktop databases so the file associations and scheme
# handler register without requiring the user to run them manually. The
# tools may be missing on minimal systems; that is fine, packaging systems
# usually refresh these databases through their own triggers.
find_program(UPDATE_MIME_DATABASE update-mime-database)
if(UPDATE_MIME_DATABASE)
install(CODE "execute_process(COMMAND \"${UPDATE_MIME_DATABASE}\" \"${CMAKE_INSTALL_PREFIX}/share/mime\")")
endif()
find_program(UPDATE_DESKTOP_DATABASE update-desktop-database)
if(UPDATE_DESKTOP_DATABASE)
install(
CODE "execute_process(COMMAND \"${UPDATE_DESKTOP_DATABASE}\" \"${CMAKE_INSTALL_PREFIX}/share/applications\")"
)
endif()
endif() endif()
elseif(WIN32) elseif(WIN32)
install(TARGETS cockatrice RUNTIME DESTINATION ./) install(TARGETS cockatrice RUNTIME DESTINATION ./)
@ -500,7 +530,7 @@ if(APPLE)
set(plugin_dest_dir cockatrice.app/Contents/Plugins) set(plugin_dest_dir cockatrice.app/Contents/Plugins)
set(qtconf_dest_dir cockatrice.app/Contents/Resources) set(qtconf_dest_dir cockatrice.app/Contents/Resources)
# Qt plugins: audio (Qt5), iconengines, imageformats, multimedia (Qt6), platforms, printsupport (Qt5), styles, tls (Qt6) # Qt plugins: audio, iconengines, imageformats, multimedia, platforms, printsupport, styles, tls
install( install(
DIRECTORY "${QT_PLUGINS_DIR}/" DIRECTORY "${QT_PLUGINS_DIR}/"
DESTINATION ${plugin_dest_dir} DESTINATION ${plugin_dest_dir}
@ -567,7 +597,7 @@ if(WIN32)
PATTERN "*.ini" PATTERN "*.ini"
) )
# Qt plugins: audio (Qt5), iconengines, imageformats, multimedia (Qt6) platforms, printsupport (Qt5), styles, tls (Qt6) # Qt plugins: audio, iconengines, imageformats, multimedia, platforms, printsupport, styles, tls
install( install(
DIRECTORY "${QT_PLUGINS_DIR}/" DIRECTORY "${QT_PLUGINS_DIR}/"
DESTINATION ${plugin_dest_dir} DESTINATION ${plugin_dest_dir}
@ -623,7 +653,7 @@ Data = Resources\")
endif() endif()
endif() endif()
if(Qt6_FOUND AND Qt6LinguistTools_FOUND) if(Qt6LinguistTools_FOUND)
#Qt6 Translations happen after the executable is built up #Qt6 Translations happen after the executable is built up
if(UPDATE_TRANSLATIONS) if(UPDATE_TRANSLATIONS)
qt6_add_translations( qt6_add_translations(
@ -650,6 +680,4 @@ if(Qt6_FOUND AND Qt6LinguistTools_FOUND)
endif() endif()
endif() endif()
if(Qt6_FOUND) qt6_finalize_target(cockatrice)
qt6_finalize_target(cockatrice)
endif()

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
<mime-type type="application/x-cockatrice">
<comment>Cockatrice Deck File</comment>
<glob pattern="*.cod"/>
</mime-type>
</mime-info>

View file

@ -3,6 +3,8 @@
Version=1.0 Version=1.0
Type=Application Type=Application
Name=Cockatrice Name=Cockatrice
Exec=cockatrice Exec=cockatrice %U
Icon=cockatrice Icon=cockatrice
Categories=Game;CardGame; Categories=Game;CardGame;
MimeType=application/x-cockatrice;
X-Scheme-Handler/cockatrice=true

File diff suppressed because it is too large Load diff

View file

@ -14,6 +14,7 @@
#include <QThread> #include <QThread>
#include <libcockatrice/network/client/remote/remote_client.h> #include <libcockatrice/network/client/remote/remote_client.h>
#include <libcockatrice/protocol/pb/response.pb.h> #include <libcockatrice/protocol/pb/response.pb.h>
#include <libcockatrice/settings/servers_settings.h>
ConnectionController::ConnectionController(QWidget *dialogParent, QObject *parent) ConnectionController::ConnectionController(QWidget *dialogParent, QObject *parent)
: QObject(parent), dialogParent(dialogParent) : QObject(parent), dialogParent(dialogParent)

View file

@ -14,6 +14,8 @@
#include <QtConcurrent> #include <QtConcurrent>
#include <libcockatrice/card/database/card_database.h> #include <libcockatrice/card/database/card_database.h>
#include <libcockatrice/card/database/card_database_manager.h> #include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/settings/download_settings.h>
#include <libcockatrice/settings/paths_settings.h>
#include <version_string.h> #include <version_string.h>
#define SPOILERS_STATUS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/SpoilerSeasonEnabled" #define SPOILERS_STATUS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/SpoilerSeasonEnabled"
@ -21,7 +23,7 @@
SpoilerBackgroundUpdater::SpoilerBackgroundUpdater(QObject *apParent) : QObject(apParent), cardUpdateProcess(nullptr) SpoilerBackgroundUpdater::SpoilerBackgroundUpdater(QObject *apParent) : QObject(apParent), cardUpdateProcess(nullptr)
{ {
isSpoilerDownloadEnabled = SettingsCache::instance().getDownloadSpoilersStatus(); isSpoilerDownloadEnabled = SettingsCache::instance().downloads().getDownloadSpoilersStatus();
if (isSpoilerDownloadEnabled) { if (isSpoilerDownloadEnabled) {
// Start the process of checking if we're in spoiler season // Start the process of checking if we're in spoiler season
// File exists means we're in spoiler season // File exists means we're in spoiler season
@ -75,7 +77,7 @@ void SpoilerBackgroundUpdater::actDownloadFinishedSpoilersFile()
bool SpoilerBackgroundUpdater::deleteSpoilerFile() bool SpoilerBackgroundUpdater::deleteSpoilerFile()
{ {
QString fileName = SettingsCache::instance().getSpoilerCardDatabasePath(); QString fileName = SettingsCache::instance().paths().getSpoilerCardDatabasePath();
QFileInfo fi(fileName); QFileInfo fi(fileName);
QDir fileDir(fi.path()); QDir fileDir(fi.path());
QFile file(fileName); QFile file(fileName);
@ -126,7 +128,7 @@ void SpoilerBackgroundUpdater::actCheckIfSpoilerSeasonEnabled()
bool SpoilerBackgroundUpdater::saveDownloadedFile(QByteArray data) bool SpoilerBackgroundUpdater::saveDownloadedFile(QByteArray data)
{ {
QString fileName = SettingsCache::instance().getSpoilerCardDatabasePath(); QString fileName = SettingsCache::instance().paths().getSpoilerCardDatabasePath();
QFileInfo fi(fileName); QFileInfo fi(fileName);
QDir fileDir(fi.path()); QDir fileDir(fi.path());

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -5,7 +5,7 @@
#include <QtMath> #include <QtMath>
CardCounterSettings::CardCounterSettings(const QString &settingsPath, QObject *parent) CardCounterSettings::CardCounterSettings(const QString &settingsPath, QObject *parent)
: SettingsManager(settingsPath + "global.ini", "cards", "counters", parent) : SettingsManager(settingsPath + "card_counters.ini", "cards", "counters", parent)
{ {
} }

View file

@ -223,6 +223,10 @@ private:
{"TabDeckEditor/aLoadDeck", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Load Deck..."), {"TabDeckEditor/aLoadDeck", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Load Deck..."),
parseSequenceString("Ctrl+O"), parseSequenceString("Ctrl+O"),
ShortcutGroup::Deck_Editor)}, ShortcutGroup::Deck_Editor)},
{"TabDeckEditor/aLoadDeckFromWebsite",
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Load deck from online service..."),
parseSequenceString("Ctrl+Shift+O"),
ShortcutGroup::Deck_Editor)},
{"TabDeckEditor/aLoadDeckFromClipboard", {"TabDeckEditor/aLoadDeckFromClipboard",
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Load Deck from Clipboard..."), ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Load Deck from Clipboard..."),
parseSequenceString("Ctrl+Shift+V"), parseSequenceString("Ctrl+Shift+V"),
@ -283,6 +287,10 @@ private:
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Load Deck from Clipboard..."), ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Load Deck from Clipboard..."),
parseSequenceString("Ctrl+Shift+V"), parseSequenceString("Ctrl+Shift+V"),
ShortcutGroup::Game_Lobby)}, ShortcutGroup::Game_Lobby)},
{"DeckViewContainer/loadFromWebsiteButton",
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Load from website..."),
parseSequenceString("Ctrl+Shift+O"),
ShortcutGroup::Game_Lobby)},
{"DeckViewContainer/unloadDeckButton", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Unload Deck"), {"DeckViewContainer/unloadDeckButton", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Unload Deck"),
parseSequenceString("Ctrl+Alt+U"), parseSequenceString("Ctrl+Alt+U"),
ShortcutGroup::Game_Lobby)}, ShortcutGroup::Game_Lobby)},

View file

@ -2,12 +2,11 @@
#include "settings/cache_settings.h" #include "settings/cache_settings.h"
#include <QApplication>
#include <QAudioOutput>
#include <QDir> #include <QDir>
#include <QMediaPlayer> #include <QMediaPlayer>
#include <libcockatrice/settings/sound_settings.h>
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
#include <QAudioOutput>
#endif
#define DEFAULT_THEME_NAME "Default" #define DEFAULT_THEME_NAME "Default"
#define TEST_SOUND_FILENAME "player_join" #define TEST_SOUND_FILENAME "player_join"
@ -15,8 +14,10 @@
SoundEngine::SoundEngine(QObject *parent) : QObject(parent), audioOutput(nullptr), player(nullptr) SoundEngine::SoundEngine(QObject *parent) : QObject(parent), audioOutput(nullptr), player(nullptr)
{ {
ensureThemeDirectoryExists(); ensureThemeDirectoryExists();
connect(&SettingsCache::instance(), &SettingsCache::soundThemeChanged, this, &SoundEngine::themeChangedSlot); connect(&SettingsCache::instance().sound(), &SoundSettings::soundThemeChanged, this,
connect(&SettingsCache::instance(), &SettingsCache::soundEnabledChanged, this, &SoundEngine::soundEnabledChanged); &SoundEngine::themeChangedSlot);
connect(&SettingsCache::instance().sound(), &SoundSettings::soundEnabledChanged, this,
&SoundEngine::soundEnabledChanged);
soundEnabledChanged(); soundEnabledChanged();
themeChangedSlot(); themeChangedSlot();
@ -36,14 +37,12 @@ SoundEngine::~SoundEngine()
void SoundEngine::soundEnabledChanged() void SoundEngine::soundEnabledChanged()
{ {
if (SettingsCache::instance().getSoundEnabled()) { if (SettingsCache::instance().sound().getSoundEnabled()) {
qCInfo(SoundEngineLog) << "SoundEngine: enabling sound with" << audioData.size() << "sounds"; qCInfo(SoundEngineLog) << "SoundEngine: enabling sound with" << audioData.size() << "sounds";
if (!player) { if (!player) {
player = new QMediaPlayer; player = new QMediaPlayer;
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
audioOutput = new QAudioOutput(player); audioOutput = new QAudioOutput(player);
player->setAudioOutput(audioOutput); player->setAudioOutput(audioOutput);
#endif
} }
} else { } else {
qCInfo(SoundEngineLog) << "SoundEngine: disabling sound"; qCInfo(SoundEngineLog) << "SoundEngine: disabling sound";
@ -70,14 +69,9 @@ void SoundEngine::playSound(const QString &fileName)
} }
player->stop(); player->stop();
int volumeSliderValue = SettingsCache::instance().getMasterVolume(); int volumeSliderValue = SettingsCache::instance().sound().getMasterVolume();
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
player->audioOutput()->setVolume(qreal(volumeSliderValue) / 100); player->audioOutput()->setVolume(qreal(volumeSliderValue) / 100);
player->setSource(QUrl::fromLocalFile(audioData[fileName])); player->setSource(QUrl::fromLocalFile(audioData[fileName]));
#else
player->setVolume(volumeSliderValue);
player->setMedia(QUrl::fromLocalFile(audioData[fileName]));
#endif
player->play(); player->play();
} }
@ -88,10 +82,10 @@ void SoundEngine::testSound()
void SoundEngine::ensureThemeDirectoryExists() void SoundEngine::ensureThemeDirectoryExists()
{ {
if (SettingsCache::instance().getSoundThemeName().isEmpty() || if (SettingsCache::instance().sound().getSoundThemeName().isEmpty() ||
!getAvailableThemes().contains(SettingsCache::instance().getSoundThemeName())) { !getAvailableThemes().contains(SettingsCache::instance().sound().getSoundThemeName())) {
qCInfo(SoundEngineLog) << "Sounds theme name not set, setting default value"; qCInfo(SoundEngineLog) << "Sounds theme name not set, setting default value";
SettingsCache::instance().setSoundThemeName(DEFAULT_THEME_NAME); SettingsCache::instance().sound().setSoundThemeName(DEFAULT_THEME_NAME);
} }
} }
@ -132,7 +126,7 @@ QStringMap &SoundEngine::getAvailableThemes()
void SoundEngine::themeChangedSlot() void SoundEngine::themeChangedSlot()
{ {
QString themeName = SettingsCache::instance().getSoundThemeName(); QString themeName = SettingsCache::instance().sound().getSoundThemeName();
qCInfo(SoundEngineLog) << "Sound theme changed:" << themeName; qCInfo(SoundEngineLog) << "Sound theme changed:" << themeName;
QDir dir = getAvailableThemes().value(themeName); QDir dir = getAvailableThemes().value(themeName);

View file

@ -0,0 +1,69 @@
#ifndef COCKATRICE_URL_SCHEME_EVENT_FILTER_H
#define COCKATRICE_URL_SCHEME_EVENT_FILTER_H
#include <QEvent>
#include <QFileOpenEvent>
#include <QObject>
#include <QString>
#include <QUrl>
/**
* @brief Event filter that catches QFileOpenEvent URLs matching a scheme and
* re-emits them as urlReceived().
*
* On macOS, when the application is registered as a URL scheme handler, the
* OS delivers incoming URLs via QFileOpenEvent on the QApplication object.
* Install this filter on QApplication to intercept them:
*
* @code
* UrlSchemeEventFilter filter(QStringList{QStringLiteral("cockatrice")});
* QObject::connect(&filter, &UrlSchemeEventFilter::urlReceived,
* &mainWindow, &MainWindow::handleUrl);
* app.installEventFilter(&filter);
* @endcode
*
* Note: the strings are compared against QUrl::scheme(), so they must be
* written without the "://" suffix (e.g. "cockatrice", not "cockatrice://").
*/
class UrlSchemeEventFilter : public QObject
{
Q_OBJECT
public:
explicit UrlSchemeEventFilter(const QStringList &schemes, QObject *parent = nullptr)
: QObject(parent), prefixes(schemes)
{
}
signals:
void urlReceived(const QString &url);
public:
bool eventFilter(QObject *watched, QEvent *event) override
{
if (event->type() == QEvent::FileOpen) {
auto *fileEvent = static_cast<QFileOpenEvent *>(event);
const QUrl url = fileEvent->url();
for (const auto &prefix : prefixes) {
if (url.scheme() == prefix) {
emit urlReceived(url.toString());
return true;
}
}
if (url.isLocalFile()) {
emit urlReceived(url.toLocalFile());
return true;
}
}
return QObject::eventFilter(watched, event);
}
private:
QStringList prefixes;
};
#endif // COCKATRICE_URL_SCHEME_EVENT_FILTER_H

View file

@ -0,0 +1,41 @@
#ifndef SETTINGS_CARD_DATABASE_PATH_PROVIDER_H
#define SETTINGS_CARD_DATABASE_PATH_PROVIDER_H
#include "../../client/settings/cache_settings.h"
#include <libcockatrice/interfaces/interface_card_database_path_provider.h>
#include <libcockatrice/settings/paths_settings.h>
class SettingsCardDatabasePathProvider : public ICardDatabasePathProvider
{
Q_OBJECT
public:
explicit SettingsCardDatabasePathProvider(QObject *parent = nullptr) : ICardDatabasePathProvider(parent)
{
connect(&SettingsCache::instance().paths(), &PathsSettings::cardDatabasePathChanged, this,
&ICardDatabasePathProvider::cardDatabasePathChanged);
}
[[nodiscard]] QString getCardDatabasePath() const override
{
return SettingsCache::instance().paths().getCardDatabasePath();
}
[[nodiscard]] QString getCustomCardDatabasePath() const override
{
return SettingsCache::instance().paths().getCustomCardDatabasePath();
}
[[nodiscard]] QString getTokenDatabasePath() const override
{
return SettingsCache::instance().paths().getTokenDatabasePath();
}
[[nodiscard]] virtual QString getSpoilerCardDatabasePath() const override
{
return SettingsCache::instance().paths().getSpoilerCardDatabasePath();
}
};
#endif // SETTINGS_CARD_DATABASE_PATH_PROVIDER_H

View file

@ -3,6 +3,8 @@
#include "../../client/settings/cache_settings.h" #include "../../client/settings/cache_settings.h"
#include <libcockatrice/interfaces/interface_card_preference_provider.h> #include <libcockatrice/interfaces/interface_card_preference_provider.h>
#include <libcockatrice/settings/card_override_settings.h>
#include <libcockatrice/settings/cards_display_settings.h>
class SettingsCardPreferenceProvider : public ICardPreferenceProvider class SettingsCardPreferenceProvider : public ICardPreferenceProvider
{ {
@ -14,7 +16,7 @@ public:
[[nodiscard]] bool getIncludeRebalancedCards() const override [[nodiscard]] bool getIncludeRebalancedCards() const override
{ {
return SettingsCache::instance().getIncludeRebalancedCards(); return SettingsCache::instance().cardsDisplay().getIncludeRebalancedCards();
} }
}; };

View file

@ -52,18 +52,14 @@ static void setupParserRules()
search["Start"] = passthru; search["Start"] = passthru;
search["QueryPartList"] = [](const peg::SemanticValues &sv) -> DeckFilter { search["QueryPartList"] = [](const peg::SemanticValues &sv) -> DeckFilter {
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) { return [=](const DeckSearchData &data) {
auto matchesFilter = [&deck, &info](const std::any &query) { auto matchesFilter = [&data](const std::any &query) { return std::any_cast<DeckFilter>(query)(data); };
return std::any_cast<DeckFilter>(query)(deck, info);
};
return std::all_of(sv.begin(), sv.end(), matchesFilter); return std::all_of(sv.begin(), sv.end(), matchesFilter);
}; };
}; };
search["ComplexQueryPart"] = [](const peg::SemanticValues &sv) -> DeckFilter { search["ComplexQueryPart"] = [](const peg::SemanticValues &sv) -> DeckFilter {
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) { return [=](const DeckSearchData &data) {
auto matchesFilter = [&deck, &info](const std::any &query) { auto matchesFilter = [&data](const std::any &query) { return std::any_cast<DeckFilter>(query)(data); };
return std::any_cast<DeckFilter>(query)(deck, info);
};
return std::any_of(sv.begin(), sv.end(), matchesFilter); return std::any_of(sv.begin(), sv.end(), matchesFilter);
}; };
}; };
@ -71,9 +67,7 @@ static void setupParserRules()
search["QueryPart"] = passthru; search["QueryPart"] = passthru;
search["NotQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter { search["NotQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
const auto dependent = std::any_cast<DeckFilter>(sv[0]); const auto dependent = std::any_cast<DeckFilter>(sv[0]);
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) -> bool { return [=](const DeckSearchData &data) -> bool { return !dependent(data); };
return !dependent(deck, info);
};
}; };
search["String"] = [](const peg::SemanticValues &sv) -> QString { search["String"] = [](const peg::SemanticValues &sv) -> QString {
@ -125,9 +119,9 @@ static void setupParserRules()
auto cardFilter = FilterString(std::any_cast<QString>(sv[0])); auto cardFilter = FilterString(std::any_cast<QString>(sv[0]));
auto numberMatcher = sv.size() > 1 ? std::any_cast<NumberMatcher>(sv[1]) : [](int count) { return count > 0; }; auto numberMatcher = sv.size() > 1 ? std::any_cast<NumberMatcher>(sv[1]) : [](int count) { return count > 0; };
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) -> bool { return [=](const DeckSearchData &data) -> bool {
int count = 0; int count = 0;
auto cardNodes = deck->deckLoader->getDeck().deckList.getCardNodes(); auto cardNodes = data.deck->deckList.getCardNodes();
for (auto node : cardNodes) { for (auto node : cardNodes) {
auto cardInfoPtr = CardDatabaseManager::query()->getCardInfo(node->getName()); auto cardInfoPtr = CardDatabaseManager::query()->getCardInfo(node->getName());
if (!cardInfoPtr.isNull() && cardFilter.check(cardInfoPtr)) { if (!cardInfoPtr.isNull() && cardFilter.check(cardInfoPtr)) {
@ -146,53 +140,49 @@ static void setupParserRules()
search["DeckNameQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter { search["DeckNameQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
auto name = std::any_cast<QString>(sv[0]); auto name = std::any_cast<QString>(sv[0]);
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) { return [=](const DeckSearchData &data) {
return deck->deckLoader->getDeck().deckList.getName().contains(name, Qt::CaseInsensitive); return data.deck->deckList.getName().contains(name, Qt::CaseInsensitive);
}; };
}; };
search["FileNameQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter { search["FileNameQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
auto name = std::any_cast<QString>(sv[0]); auto name = std::any_cast<QString>(sv[0]);
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) { return [=](const DeckSearchData &data) {
auto filename = QFileInfo(deck->filePath).fileName(); auto filename = QFileInfo(data.filePath).fileName();
return filename.contains(name, Qt::CaseInsensitive); return filename.contains(name, Qt::CaseInsensitive);
}; };
}; };
search["PathQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter { search["PathQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
auto name = std::any_cast<QString>(sv[0]); auto name = std::any_cast<QString>(sv[0]);
return [=](const DeckPreviewWidget *, const ExtraDeckSearchInfo &info) { return [=](const DeckSearchData &data) { return data.relativeFilePath.contains(name, Qt::CaseInsensitive); };
return info.relativeFilePath.contains(name, Qt::CaseInsensitive);
};
}; };
search["FormatQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter { search["FormatQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
auto format = std::any_cast<QString>(sv[0]); auto format = std::any_cast<QString>(sv[0]);
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) { return [=](const DeckSearchData &data) {
auto gameFormat = deck->deckLoader->getDeck().deckList.getGameFormat(); auto gameFormat = data.deck->deckList.getGameFormat();
return QString::compare(format, gameFormat, Qt::CaseInsensitive) == 0; return QString::compare(format, gameFormat, Qt::CaseInsensitive) == 0;
}; };
}; };
search["CommentQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter { search["CommentQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
auto value = std::any_cast<QString>(sv[0]); auto value = std::any_cast<QString>(sv[0]);
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) { return [=](const DeckSearchData &data) {
auto comments = deck->deckLoader->getDeck().deckList.getComments(); auto comments = data.deck->deckList.getComments();
return comments.contains(value, Qt::CaseInsensitive); return comments.contains(value, Qt::CaseInsensitive);
}; };
}; };
search["GenericQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter { search["GenericQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
auto name = std::any_cast<QString>(sv[0]); auto name = std::any_cast<QString>(sv[0]);
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) { return [=](const DeckSearchData &data) { return data.displayName.contains(name, Qt::CaseInsensitive); };
return deck->getDisplayName().contains(name, Qt::CaseInsensitive);
};
}; };
} }
DeckFilterString::DeckFilterString() DeckFilterString::DeckFilterString()
{ {
filter = [](const DeckPreviewWidget *, const ExtraDeckSearchInfo &) { return false; }; filter = [](const DeckSearchData &) { return false; };
_error = "Not initialized"; _error = "Not initialized";
} }
@ -205,7 +195,7 @@ DeckFilterString::DeckFilterString(const QString &expr)
_error = QString(); _error = QString();
if (ba.isEmpty()) { if (ba.isEmpty()) {
filter = [](const DeckPreviewWidget *, const ExtraDeckSearchInfo &) { return true; }; filter = [](const DeckSearchData &) { return true; };
return; return;
} }
@ -215,6 +205,6 @@ DeckFilterString::DeckFilterString(const QString &expr)
if (!search.parse(ba.data(), filter)) { if (!search.parse(ba.data(), filter)) {
qCInfo(DeckFilterStringLog).nospace() << "DeckFilterString error for " << expr << "; " << qPrintable(_error); qCInfo(DeckFilterStringLog).nospace() << "DeckFilterString error for " << expr << "; " << qPrintable(_error);
filter = [](const DeckPreviewWidget *, const ExtraDeckSearchInfo &) { return false; }; filter = [](const DeckSearchData &) { return false; };
} }
} }

View file

@ -7,7 +7,7 @@
#ifndef DECK_FILTER_STRING_H #ifndef DECK_FILTER_STRING_H
#define DECK_FILTER_STRING_H #define DECK_FILTER_STRING_H
#include "../interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h" #include "../interface/deck_loader/loaded_deck.h"
#include <QLoggingCategory> #include <QLoggingCategory>
#include <QString> #include <QString>
@ -16,26 +16,29 @@
inline Q_LOGGING_CATEGORY(DeckFilterStringLog, "deck_filter_string"); inline Q_LOGGING_CATEGORY(DeckFilterStringLog, "deck_filter_string");
/** /**
* Extra info relevant to filtering that isn't present in the DeckPreviewWidget * The data a deck search expression is evaluated against.
*
* This is a data view rather than a widget pointer, so the same filter
* expression can be evaluated against a model or a live widget.
*/ */
struct ExtraDeckSearchInfo struct DeckSearchData
{ {
/** const LoadedDeck *deck = nullptr; ///< The loaded deck. Must not be null.
* The relative filepath starting from the deck folder QString filePath; ///< Absolute path of the deck file.
*/ QString displayName; ///< Deck name, or the file name if the deck has no name.
QString relativeFilePath; QString relativeFilePath; ///< File path relative to the deck folder.
}; };
typedef std::function<bool(const DeckPreviewWidget *, const ExtraDeckSearchInfo &)> DeckFilter; typedef std::function<bool(const DeckSearchData &data)> DeckFilter;
class DeckFilterString class DeckFilterString
{ {
public: public:
DeckFilterString(); DeckFilterString();
explicit DeckFilterString(const QString &expr); explicit DeckFilterString(const QString &expr);
bool check(const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) const bool check(const DeckSearchData &data) const
{ {
return filter(deck, info); return filter(data);
} }
[[nodiscard]] bool valid() const [[nodiscard]] bool valid() const

View file

@ -31,7 +31,7 @@ AbstractClient *AbstractGame::getClientForPlayer(int playerId) const
} }
} }
void AbstractGame::loadReplay(GameReplay *replay) void AbstractGame::loadReplay(const GameReplay *replay)
{ {
gameMetaInfo->setFromProto(replay->game_info()); gameMetaInfo->setFromProto(replay->game_info());
gameMetaInfo->setSpectatorsOmniscient(true); gameMetaInfo->setSpectatorsOmniscient(true);

View file

@ -53,7 +53,7 @@ public:
AbstractClient *getClientForPlayer(int playerId) const; AbstractClient *getClientForPlayer(int playerId) const;
void loadReplay(GameReplay *replay); void loadReplay(const GameReplay *replay);
CardItem *getCard(int playerId, const QString &zoneName, int cardId) const; CardItem *getCard(int playerId, const QString &zoneName, int cardId) const;

View file

@ -287,6 +287,9 @@ void GameEventHandler::eventGameStateChanged(const Event_GameStateChanged &event
if (!game->getGameMetaInfo()->proto().share_decklists_on_load()) { if (!game->getGameMetaInfo()->proto().share_decklists_on_load()) {
continue; continue;
} }
if (!playerInfo.has_deck_list()) {
continue;
}
opponentDecksToDisplay.append( opponentDecksToDisplay.append(
qMakePair(playerId, qMakePair(playerName, QString::fromStdString(playerInfo.deck_list())))); qMakePair(playerId, qMakePair(playerName, QString::fromStdString(playerInfo.deck_list()))));

View file

@ -1,8 +1,16 @@
/** /**
* @file game_event_handler.h * @file game_event_handler.h
* @ingroup GameLogic * @ingroup GameLogic
* @brief Game-level command sender and event dispatcher.
*
* GameEventHandler sends commands initiated by the local client to the server
* and processes incoming game-wide events. It bridges the networking layer
* (protobuf events received via AbstractClient) with the game model and UI
* (GameState, PlayerManager, logging, widgets).
*
* Player-scoped events are forwarded to PlayerEventHandler instances, while
* spectator and global game events are handled directly here.
*/ */
//! \todo Document this file.
#ifndef COCKATRICE_GAME_EVENT_HANDLER_H #ifndef COCKATRICE_GAME_EVENT_HANDLER_H
#define COCKATRICE_GAME_EVENT_HANDLER_H #define COCKATRICE_GAME_EVENT_HANDLER_H
@ -15,92 +23,310 @@
#include <libcockatrice/protocol/pb/serverinfo_player.pb.h> #include <libcockatrice/protocol/pb/serverinfo_player.pb.h>
class AbstractClient; class AbstractClient;
class Response; class AbstractGame;
class CommandContainer;
class GameCommand;
class GameEventContainer; class GameEventContainer;
class GameEventContext; class GameEventContext;
class GameCommand; class PendingCommand;
class GameState; class PlayerLogic;
class MessageLogWidget; class Response;
class CommandContainer;
class Event_GameJoined;
class Event_GameStateChanged; class Event_GameStateChanged;
class Event_PlayerPropertiesChanged; class Event_PlayerPropertiesChanged;
class Event_Join; class Event_Join;
class Event_Leave; class Event_Leave;
class Event_GameHostChanged; class Event_GameHostChanged;
class Event_GameClosed; class Event_GameClosed;
class Event_GameStart;
class Event_SetActivePlayer; class Event_SetActivePlayer;
class Event_SetActivePhase; class Event_SetActivePhase;
class Event_Ping;
class Event_GameSay; class Event_GameSay;
class Event_Kicked; class Event_Kicked;
class Event_ReverseTurn; class Event_ReverseTurn;
class AbstractGame; class Event_Ping;
class PendingCommand;
class PlayerLogic;
inline Q_LOGGING_CATEGORY(GameEventHandlerLog, "game_event_handler"); inline Q_LOGGING_CATEGORY(GameEventHandlerLog, "game_event_handler");
/**
* @class GameEventHandler
* @brief Central dispatcher for game-wide commands and events.
*
* This class owns no game state itself. Instead, it:
* - Sends commands to the server on behalf of local players
* - Receives and dispatches server-side game events
* - Updates the game model indirectly via Player, GameState, and PlayerManager
* - Emits high-level signals for UI updates and logging
*/
class GameEventHandler : public QObject class GameEventHandler : public QObject
{ {
Q_OBJECT Q_OBJECT
private: private:
/** Pointer to the owning game instance. */
AbstractGame *game; AbstractGame *game;
public: public:
/** @name Construction
* Lifecycle and ownership.
* @{
*/
/**
* @brief Construct a GameEventHandler.
*
* The handler is owned by the AbstractGame instance and uses it to
* access the game state, players, and network clients.
*
* @param _game Owning game instance (also used as QObject parent).
*/
explicit GameEventHandler(AbstractGame *_game); explicit GameEventHandler(AbstractGame *_game);
/** @} */
/** @name Outgoing game commands
* Commands initiated locally and sent to the server.
*
* These methods construct and send protobuf commands corresponding
* to user actions in the UI.
* @{
*/
/** @brief Request advancing the game to the next turn. */
void handleNextTurn(); void handleNextTurn();
/** @brief Request reversing the current turn order. */
void handleReverseTurn(); void handleReverseTurn();
/** @brief Concede the game for the currently active local player. */
void handleActiveLocalPlayerConceded(); void handleActiveLocalPlayerConceded();
/** @brief Undo a previous concede for the active local player. */
void handleActiveLocalPlayerUnconceded(); void handleActiveLocalPlayerUnconceded();
/**
* @brief Set the active phase of the game.
*
* Typically triggered by the active player selecting a new phase.
*
* @param phase Phase identifier.
*/
void handleActivePhaseChanged(int phase); void handleActivePhaseChanged(int phase);
/** @brief Leave the current game session. */
void handleGameLeft(); void handleGameLeft();
/**
* @brief Send a chat message to all players and spectators.
*
* @param chatMessage Message text.
*/
void handleChatMessageSent(const QString &chatMessage); void handleChatMessageSent(const QString &chatMessage);
/**
* @brief Delete an existing arrow.
*
* @param arrowId Unique identifier of the arrow to delete.
*/
void handleArrowDeletion(int creatorId, int arrowId); void handleArrowDeletion(int creatorId, int arrowId);
void handleArrowDeletionFinished(const Response &response, int creatorId, int arrowId); void handleArrowDeletionFinished(const Response &response, int creatorId, int arrowId);
/** @} */
/** @name Incoming event processing
* Entry points for server-sent events.
* @{
*/
/**
* @brief Process a container of game events received from the server.
*
* This is the main dispatch function for incoming game events.
* Events are routed to spectator handlers, game-level handlers,
* or forwarded to PlayerEventHandler instances as appropriate.
*
* @param cont Game event container from the server.
* @param client Client that received the container.
* @param options Processing flags (e.g. silent, replay).
*/
void
processGameEventContainer(const GameEventContainer &cont, AbstractClient *client, EventProcessingOptions options);
/** @} */
/** @name Command preparation helpers
* Internal helpers for building command containers.
* @{
*/
/**
* @brief Wrap a single protobuf command in a PendingCommand.
*
* @param cmd Protobuf command message.
* @return Newly allocated PendingCommand (caller takes ownership).
*/
PendingCommand *prepareGameCommand(const ::google::protobuf::Message &cmd);
/**
* @brief Wrap multiple protobuf commands in a single PendingCommand.
*
* Ownership of the messages in cmdList is transferred to the handler.
*
* @param cmdList List of protobuf command messages.
* @return Newly allocated PendingCommand.
*/
PendingCommand *prepareGameCommand(const QList<const ::google::protobuf::Message *> &cmdList);
/** @} */
/** @name Spectator event handlers
* Events originating from spectators.
* @{
*/
/**
* @brief Handle a spectator chat message.
*/
void eventSpectatorSay(const Event_GameSay &event, int eventPlayerId, const GameEventContext &context); void eventSpectatorSay(const Event_GameSay &event, int eventPlayerId, const GameEventContext &context);
/**
* @brief Handle a spectator leaving the game.
*/
void eventSpectatorLeave(const Event_Leave &event, int eventPlayerId, const GameEventContext &context); void eventSpectatorLeave(const Event_Leave &event, int eventPlayerId, const GameEventContext &context);
/** @} */
/** @name Game state event handlers
* Events that affect global game state.
* @{
*/
/**
* @brief Handle a full game state update from the server.
*
* Used during game startup, reconnection, and resynchronization.
*/
void eventGameStateChanged(const Event_GameStateChanged &event, int eventPlayerId, const GameEventContext &context); void eventGameStateChanged(const Event_GameStateChanged &event, int eventPlayerId, const GameEventContext &context);
/**
* @brief Update card attachment relationships for all players.
*
* Called after a game state update to ensure attachments are resolved
* consistently across all zones.
*/
void processCardAttachmentsForPlayers(const Event_GameStateChanged &event); void processCardAttachmentsForPlayers(const Event_GameStateChanged &event);
/** @brief Handle a change in game host. */
void eventGameHostChanged(const Event_GameHostChanged &event, int eventPlayerId, const GameEventContext &context);
/** @brief Handle the game being closed by the server. */
void eventGameClosed(const Event_GameClosed &event, int eventPlayerId, const GameEventContext &context);
/** @brief Handle a change of the active player. */
void eventSetActivePlayer(const Event_SetActivePlayer &event, int eventPlayerId, const GameEventContext &context);
/** @brief Handle a change of the active phase. */
void eventSetActivePhase(const Event_SetActivePhase &event, int eventPlayerId, const GameEventContext &context);
/** @brief Handle a turn reversal event. */
void eventReverseTurn(const Event_ReverseTurn &event, int eventPlayerId, const GameEventContext &context);
/** @brief Handle ping / latency updates. */
void eventPing(const Event_Ping &event, int eventPlayerId, const GameEventContext &context);
/** @} */
/** @name Player lifecycle and property handlers
* Events related to players joining, leaving, or changing state.
* @{
*/
/**
* @brief Handle updates to a player's properties.
*
* Includes readiness, concede state, deck selection, sideboard lock,
* and connection state changes.
*/
void eventPlayerPropertiesChanged(const Event_PlayerPropertiesChanged &event, void eventPlayerPropertiesChanged(const Event_PlayerPropertiesChanged &event,
int eventPlayerId, int eventPlayerId,
const GameEventContext &context); const GameEventContext &context);
/** @brief Handle a player or spectator joining the game. */
void eventJoin(const Event_Join &event, int eventPlayerId, const GameEventContext &context); void eventJoin(const Event_Join &event, int eventPlayerId, const GameEventContext &context);
/** @brief Handle a player leaving the game. */
void eventLeave(const Event_Leave &event, int eventPlayerId, const GameEventContext &context); void eventLeave(const Event_Leave &event, int eventPlayerId, const GameEventContext &context);
QString getLeaveReason(Event_Leave::LeaveReason reason);
/** @brief Handle the local player being kicked from the game. */
void eventKicked(const Event_Kicked &event, int eventPlayerId, const GameEventContext &context); void eventKicked(const Event_Kicked &event, int eventPlayerId, const GameEventContext &context);
void eventGameHostChanged(const Event_GameHostChanged &event, int eventPlayerId, const GameEventContext &context);
void eventGameClosed(const Event_GameClosed &event, int eventPlayerId, const GameEventContext &context);
void eventSetActivePlayer(const Event_SetActivePlayer &event, int eventPlayerId, const GameEventContext &context); /**
void eventSetActivePhase(const Event_SetActivePhase &event, int eventPlayerId, const GameEventContext &context); * @brief Convert a leave reason enum to a human-readable string.
void eventPing(const Event_Ping &event, int eventPlayerId, const GameEventContext &context); */
void eventReverseTurn(const Event_ReverseTurn &event, int eventPlayerId, const GameEventContext & /*context*/); QString getLeaveReason(Event_Leave::LeaveReason reason);
void commandFinished(const Response &response); /** @} */
void
processGameEventContainer(const GameEventContainer &cont, AbstractClient *client, EventProcessingOptions options);
PendingCommand *prepareGameCommand(const ::google::protobuf::Message &cmd);
PendingCommand *prepareGameCommand(const QList<const ::google::protobuf::Message *> &cmdList);
public slots: public slots:
/** @name Command dispatch slots
* Low-level command transmission.
* @{
*/
/**
* @brief Send a prepared PendingCommand.
*
* @param pend Pending command to send.
* @param playerId Player whose client should send the command.
*/
void sendGameCommand(PendingCommand *pend, int playerId = -1); void sendGameCommand(PendingCommand *pend, int playerId = -1);
/**
* @brief Send a single protobuf command.
*
* @param command Protobuf command message.
* @param playerId Player whose client should send the command.
*/
void sendGameCommand(const ::google::protobuf::Message &command, int playerId = -1); void sendGameCommand(const ::google::protobuf::Message &command, int playerId = -1);
/**
* @brief Called when a PendingCommand finishes execution.
*
* Used to detect server-side errors such as chat flood protection.
*/
void commandFinished(const Response &response);
/** @} */
signals: signals:
/** @name Core state signals
* @{
*/
void emitUserEvent(); void emitUserEvent();
void containerProcessingStarted(GameEventContext context);
void containerProcessingDone();
void gameFlooded();
void setContextJudgeName(QString judgeName);
/** @} */
/** @name Player and spectator signals
* @{
*/
void addPlayerToAutoCompleteList(QString playerName); void addPlayerToAutoCompleteList(QString playerName);
void localPlayerDeckSelected(PlayerLogic *localPlayer, int playerId, ServerInfo_Player playerInfo); void localPlayerDeckSelected(PlayerLogic *localPlayer, int playerId, ServerInfo_Player playerInfo);
void remotePlayerDeckSelected(QString deckList, int playerId, QString playerName); void remotePlayerDeckSelected(QString deckList, int playerId, QString playerName);
void remotePlayersDecksSelected(QVector<QPair<int, QPair<QString, QString>>> opponentDecks); void remotePlayersDecksSelected(QVector<QPair<int, QPair<QString, QString>>> opponentDecks);
void localPlayerSideboardLocked(int playerId, bool sideboardLocked); void localPlayerSideboardLocked(int playerId, bool sideboardLocked);
void localPlayerReadyStateChanged(int playerId, bool ready); void localPlayerReadyStateChanged(int playerId, bool ready);
/** @} */
/** @name Game flow signals
* @{
*/
void gameStopped(); void gameStopped();
void gameClosed(); void gameClosed();
void playerPropertiesChanged(const ServerInfo_PlayerProperties &prop, int playerId); void playerPropertiesChanged(const ServerInfo_PlayerProperties &prop, int playerId);
@ -109,11 +335,15 @@ signals:
void playerKicked(); void playerKicked();
void spectatorJoined(const ServerInfo_PlayerProperties &spectatorInfo); void spectatorJoined(const ServerInfo_PlayerProperties &spectatorInfo);
void spectatorLeft(int leavingSpectatorId); void spectatorLeft(int leavingSpectatorId);
void gameFlooded();
void containerProcessingStarted(GameEventContext context);
void setContextJudgeName(QString judgeName);
void containerProcessingDone();
void arrowDeleted(int creatorId, int arrowId); void arrowDeleted(int creatorId, int arrowId);
/** @} */
/** @name Logging signals
* Signals consumed by MessageLogWidget.
* @{
*/
void logSpectatorSay(ServerInfo_User userInfo, QString message); void logSpectatorSay(ServerInfo_User userInfo, QString message);
void logSpectatorLeave(QString name, QString reason); void logSpectatorLeave(QString name, QString reason);
void logGameStart(); void logGameStart();
@ -132,6 +362,8 @@ signals:
void logActivePhaseChanged(int activePhase); void logActivePhaseChanged(int activePhase);
void logConcede(int playerId); void logConcede(int playerId);
void logUnconcede(int playerId); void logUnconcede(int playerId);
/** @} */
}; };
#endif // COCKATRICE_GAME_EVENT_HANDLER_H #endif // COCKATRICE_GAME_EVENT_HANDLER_H

View file

@ -27,8 +27,11 @@
#include <libcockatrice/protocol/pb/command_shuffle.pb.h> #include <libcockatrice/protocol/pb/command_shuffle.pb.h>
#include <libcockatrice/protocol/pb/command_undo_draw.pb.h> #include <libcockatrice/protocol/pb/command_undo_draw.pb.h>
#include <libcockatrice/protocol/pb/context_move_card.pb.h> #include <libcockatrice/protocol/pb/context_move_card.pb.h>
#include <libcockatrice/settings/card_override_settings.h>
#include <libcockatrice/settings/interface_settings.h>
#include <libcockatrice/utility/clamped_arithmetic.h>
#include <libcockatrice/utility/counter_limits.h>
#include <libcockatrice/utility/expression.h> #include <libcockatrice/utility/expression.h>
#include <libcockatrice/utility/trice_limits.h>
#include <libcockatrice/utility/zone_names.h> #include <libcockatrice/utility/zone_names.h>
// milliseconds in between triggers of the move top cards until action // milliseconds in between triggers of the move top cards until action
@ -66,7 +69,7 @@ void PlayerActions::playCard(CardItem *card, bool faceDown)
const CardInfo &info = exactCard.getInfo(); const CardInfo &info = exactCard.getInfo();
int tableRow = info.getUiAttributes().tableRow; int tableRow = info.getUiAttributes().tableRow;
bool playToStack = SettingsCache::instance().getPlayToStack(); bool playToStack = SettingsCache::instance().userInterface().getPlayToStack();
QString currentZone = card->getZone()->getName(); QString currentZone = card->getZone()->getName();
if (!faceDown && currentZone == ZoneNames::STACK && tableRow == 3) { if (!faceDown && currentZone == ZoneNames::STACK && tableRow == 3) {
cmd.set_target_zone(ZoneNames::GRAVE); cmd.set_target_zone(ZoneNames::GRAVE);
@ -309,7 +312,7 @@ void PlayerActions::actDrawCard()
void PlayerActions::actRequestMulliganDialog() void PlayerActions::actRequestMulliganDialog()
{ {
int startSize = SettingsCache::instance().getStartingHandSize(); int startSize = SettingsCache::instance().userInterface().getStartingHandSize();
int handSize = player->getHandZone()->getCards().size(); int handSize = player->getHandZone()->getCards().size();
int deckSize = player->getDeckZone()->getCards().size() + handSize; int deckSize = player->getDeckZone()->getCards().size() + handSize;
@ -325,7 +328,7 @@ void PlayerActions::actMulligan(int number)
} }
doMulligan(number); doMulligan(number);
SettingsCache::instance().setStartingHandSize(number); SettingsCache::instance().userInterface().setStartingHandSize(number);
} }
void PlayerActions::actMulliganSameSize() void PlayerActions::actMulliganSameSize()
@ -882,7 +885,8 @@ void PlayerActions::actCreateToken(TokenInfo tokenToCreate)
ExactCard correctedCard = CardDatabaseManager::query()->guessCard({lastTokenInfo.name, lastTokenInfo.providerId}); ExactCard correctedCard = CardDatabaseManager::query()->guessCard({lastTokenInfo.name, lastTokenInfo.providerId});
if (correctedCard) { if (correctedCard) {
lastTokenInfo.name = correctedCard.getName(); lastTokenInfo.name = correctedCard.getName();
lastTokenTableRow = TableZone::tableRowToGridY(correctedCard.getInfo().getUiAttributes().tableRow); int tableRow = lastTokenInfo.faceDown ? 2 : correctedCard.getInfo().getUiAttributes().tableRow;
lastTokenTableRow = TableZone::tableRowToGridY(tableRow);
if (lastTokenInfo.pt.isEmpty()) { if (lastTokenInfo.pt.isEmpty()) {
lastTokenInfo.pt = correctedCard.getInfo().getPowTough(); lastTokenInfo.pt = correctedCard.getInfo().getPowTough();
} }
@ -928,13 +932,13 @@ void PlayerActions::setLastTokenInfo(CardInfoPtr cardInfo)
return; return;
} }
lastTokenInfo = {.name = cardInfo->getName(), lastTokenInfo = {
.color = cardInfo->getColors().isEmpty() ? QString() : cardInfo->getColors().left(1).toLower(), .name = cardInfo->getName(),
.pt = cardInfo->getPowTough(), .color = cardInfo->getColors().isEmpty() ? QString() : cardInfo->getColors().left(1).toLower(),
.annotation = SettingsCache::instance().getAnnotateTokens() ? cardInfo->getText() : "", .pt = cardInfo->getPowTough(),
.destroy = true, .annotation = SettingsCache::instance().userInterface().getAnnotateTokens() ? cardInfo->getText() : "",
.providerId = .destroy = true,
SettingsCache::instance().cardOverrides().getCardPreferenceOverride(cardInfo->getName())}; .providerId = SettingsCache::instance().cardOverrides().getCardPreferenceOverride(cardInfo->getName())};
lastTokenTableRow = TableZone::tableRowToGridY(cardInfo->getUiAttributes().tableRow); lastTokenTableRow = TableZone::tableRowToGridY(cardInfo->getUiAttributes().tableRow);
@ -1017,8 +1021,9 @@ void PlayerActions::actCreateAllRelatedCards()
if (!cardRelationAll->getDoesAttach() && !cardRelationAll->getIsVariable()) { if (!cardRelationAll->getDoesAttach() && !cardRelationAll->getIsVariable()) {
dbName = cardRelationAll->getName(); dbName = cardRelationAll->getName();
bool persistent = cardRelationAll->getIsPersistent(); bool persistent = cardRelationAll->getIsPersistent();
bool faceDown = cardRelationAll->getIsFaceDown();
for (int i = 0; i < cardRelationAll->getDefaultCount(); ++i) { for (int i = 0; i < cardRelationAll->getDefaultCount(); ++i) {
createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent); createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent, faceDown);
} }
++tokensTypesCreated; ++tokensTypesCreated;
if (tokensTypesCreated == 1) { if (tokensTypesCreated == 1) {
@ -1033,8 +1038,9 @@ void PlayerActions::actCreateAllRelatedCards()
if (!cardRelationNotExcluded->getDoesAttach() && !cardRelationNotExcluded->getIsVariable()) { if (!cardRelationNotExcluded->getDoesAttach() && !cardRelationNotExcluded->getIsVariable()) {
dbName = cardRelationNotExcluded->getName(); dbName = cardRelationNotExcluded->getName();
bool persistent = cardRelationNotExcluded->getIsPersistent(); bool persistent = cardRelationNotExcluded->getIsPersistent();
bool faceDown = cardRelationNotExcluded->getIsFaceDown();
for (int i = 0; i < cardRelationNotExcluded->getDefaultCount(); ++i) { for (int i = 0; i < cardRelationNotExcluded->getDefaultCount(); ++i) {
createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent); createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent, faceDown);
} }
++tokensTypesCreated; ++tokensTypesCreated;
if (tokensTypesCreated == 1) { if (tokensTypesCreated == 1) {
@ -1072,6 +1078,7 @@ bool PlayerActions::createRelatedFromRelation(const CardItem *sourceCard,
const QString dbName = cardRelation->getName(); const QString dbName = cardRelation->getName();
const bool persistent = cardRelation->getIsPersistent(); const bool persistent = cardRelation->getIsPersistent();
const bool faceDown = cardRelation->getIsFaceDown();
// Variable relations always use DoesNotAttach, regardless of the count the user // Variable relations always use DoesNotAttach, regardless of the count the user
// entered. // entered.
@ -1080,7 +1087,7 @@ bool PlayerActions::createRelatedFromRelation(const CardItem *sourceCard,
return false; return false;
} }
for (int i = 0; i < variableCount; ++i) { for (int i = 0; i < variableCount; ++i) {
createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent); createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent, faceDown);
} }
return true; return true;
} }
@ -1089,7 +1096,7 @@ bool PlayerActions::createRelatedFromRelation(const CardItem *sourceCard,
if (count > 1) { if (count > 1) {
for (int i = 0; i < count; ++i) { for (int i = 0; i < count; ++i) {
createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent); createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent, faceDown);
} }
return true; return true;
} }
@ -1109,7 +1116,7 @@ bool PlayerActions::createRelatedFromRelation(const CardItem *sourceCard,
playCardToTable(sourceCard, false); playCardToTable(sourceCard, false);
} }
createCard(sourceCard, dbName, attachType, persistent); createCard(sourceCard, dbName, attachType, persistent, faceDown);
return true; return true;
} }
@ -1136,7 +1143,8 @@ void PlayerActions::onRelatedCardCreated(const CardItem *sourceCard, const CardR
void PlayerActions::createCard(const CardItem *sourceCard, void PlayerActions::createCard(const CardItem *sourceCard,
const QString &dbCardName, const QString &dbCardName,
CardRelationType attachType, CardRelationType attachType,
bool persistent) bool persistent,
bool faceDown)
{ {
CardInfoPtr cardInfo = CardDatabaseManager::query()->getCardInfo(dbCardName); CardInfoPtr cardInfo = CardDatabaseManager::query()->getCardInfo(dbCardName);
@ -1163,7 +1171,7 @@ void PlayerActions::createCard(const CardItem *sourceCard,
} }
cmd.set_pt(cardInfo->getPowTough().toStdString()); cmd.set_pt(cardInfo->getPowTough().toStdString());
if (SettingsCache::instance().getAnnotateTokens()) { if (SettingsCache::instance().userInterface().getAnnotateTokens()) {
cmd.set_annotation(cardInfo->getText().toStdString()); cmd.set_annotation(cardInfo->getText().toStdString());
} else { } else {
cmd.set_annotation(""); cmd.set_annotation("");
@ -1171,6 +1179,7 @@ void PlayerActions::createCard(const CardItem *sourceCard,
cmd.set_destroy_on_zone_change(!persistent); cmd.set_destroy_on_zone_change(!persistent);
cmd.set_x(gridPoint.x()); cmd.set_x(gridPoint.x());
cmd.set_y(gridPoint.y()); cmd.set_y(gridPoint.y());
cmd.set_face_down(faceDown);
ExactCard relatedCard = ExactCard relatedCard =
CardDatabaseManager::query()->getCardFromSameSet(cardInfo->getName(), sourceCard->getCard().getPrinting()); CardDatabaseManager::query()->getCardFromSameSet(cardInfo->getName(), sourceCard->getCard().getPrinting());
@ -1345,11 +1354,7 @@ void PlayerActions::actSetPT(QList<CardItem *> selectedCards, const QString &pt)
const auto oldpt = CardItem::parsePT(card->getPT()); const auto oldpt = CardItem::parsePT(card->getPT());
int ptIter = 0; int ptIter = 0;
for (const auto &_item : ptList) { for (const auto &_item : ptList) {
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
if (_item.typeId() == QMetaType::Type::Int) { if (_item.typeId() == QMetaType::Type::Int) {
#else
if (_item.type() == QVariant::Int) {
#endif
int oldItem = ptIter < oldpt.size() ? oldpt.at(ptIter).toInt() : 0; int oldItem = ptIter < oldpt.size() ? oldpt.at(ptIter).toInt() : 0;
newpt += '/' + QString::number(oldItem + _item.toInt()); newpt += '/' + QString::number(oldItem + _item.toInt());
} else { } else {
@ -1524,12 +1529,15 @@ void PlayerActions::offsetCardCounter(QList<CardItem *> selectedCards, int count
QList<const ::google::protobuf::Message *> commandList; QList<const ::google::protobuf::Message *> commandList;
for (auto card : selectedCards) { for (auto card : selectedCards) {
int oldValue = card->getCounters().value(counterId, 0); int oldValue = card->getCounters().value(counterId, 0);
int newValue = oldValue + offset;
// Early exit optimization: server enforces [0, MAX_COUNTERS_ON_CARD]. // Overflow-safe clamp to the server-enforced range [0, MAX_COUNTER_VALUE];
// Compare clamped value to allow recovery from invalid states. // a result differing from oldValue also corrects an out-of-range cached value.
int clampedValue = qBound(0, newValue, MAX_COUNTERS_ON_CARD); // Callers only ever pass offset == ±1 (actAddCardCounter / actRemoveCardCounter).
if (clampedValue != oldValue) { // This client-side clamp is a defense-in-depth UX check, consistent with
// actSetCardCounter and actIncrementAllCardCounters; the server remains the
// authoritative enforcer of the bounds.
int newValue = addClamped(oldValue, offset, 0, MAX_COUNTER_VALUE);
if (newValue != oldValue) {
auto *cmd = new Command_SetCardCounter; auto *cmd = new Command_SetCardCounter;
cmd->set_zone(card->getZone()->getName().toStdString()); cmd->set_zone(card->getZone()->getName().toStdString());
cmd->set_card_id(card->getId()); cmd->set_card_id(card->getId());
@ -1562,7 +1570,7 @@ void PlayerActions::actSetCardCounter(QList<CardItem *> selectedCards, int count
Expression exp(oldValue); Expression exp(oldValue);
double parsed = exp.parse(counterValue); double parsed = exp.parse(counterValue);
// Clamp in double precision first to avoid UB, then cast // Clamp in double precision first to avoid UB, then cast
int number = static_cast<int>(qBound(0.0, parsed, static_cast<double>(MAX_COUNTERS_ON_CARD))); int number = static_cast<int>(qBound(0.0, parsed, static_cast<double>(MAX_COUNTER_VALUE)));
auto *cmd = new Command_SetCardCounter; auto *cmd = new Command_SetCardCounter;
cmd->set_zone(card->getZone()->getName().toStdString()); cmd->set_zone(card->getZone()->getName().toStdString());
@ -1592,7 +1600,7 @@ void PlayerActions::actIncrementAllCardCounters(QList<CardItem *> cardsToUpdate)
counterIterator.next(); counterIterator.next();
int counterId = counterIterator.key(); int counterId = counterIterator.key();
int currentValue = counterIterator.value(); int currentValue = counterIterator.value();
if (currentValue >= MAX_COUNTERS_ON_CARD) { if (currentValue >= MAX_COUNTER_VALUE) {
continue; continue;
} }

View file

@ -240,7 +240,8 @@ private:
void createCard(const CardItem *sourceCard, void createCard(const CardItem *sourceCard,
const QString &dbCardName, const QString &dbCardName,
CardRelationType attach = CardRelationType::DoesNotAttach, CardRelationType attach = CardRelationType::DoesNotAttach,
bool persistent = false); bool persistent = false,
bool faceDown = false);
void playSelectedCards(QList<CardItem *> selectedCards, bool faceDown = false); void playSelectedCards(QList<CardItem *> selectedCards, bool faceDown = false);

View file

@ -1,11 +1,24 @@
/** /**
* @file player_event_handler.h * @file player_event_handler.h
* @ingroup GameLogicPlayers * @ingroup GameLogicPlayers
* @brief Player-scoped game event handler.
*
* PlayerEventHandler applies game events that affect a single Players
* board state, zones, cards, counters, arrows, and related UI/log output.
*
* It is invoked by GameEventHandler after basic routing and validation.
* Each instance is bound 1:1 to a Player and must never mutate state
* belonging to other players except where explicitly required by events
* (e.g. moving cards between players, attaching cards, arrows).
*
* This class is intentionally stateful and tightly coupled to Player,
* PlayerActions, and the board/zones implementation. It performs both
* model mutation and UI-side bookkeeping (zone views, arrows, menus).
*/ */
//! \todo Document this file.
#ifndef COCKATRICE_PLAYER_EVENT_HANDLER_H #ifndef COCKATRICE_PLAYER_EVENT_HANDLER_H
#define COCKATRICE_PLAYER_EVENT_HANDLER_H #define COCKATRICE_PLAYER_EVENT_HANDLER_H
#include "event_processing_options.h" #include "event_processing_options.h"
#include <QObject> #include <QObject>
@ -16,6 +29,7 @@
class CardItem; class CardItem;
class CardZoneLogic; class CardZoneLogic;
class PlayerLogic; class PlayerLogic;
class Event_AttachCard; class Event_AttachCard;
class Event_ChangeZoneProperties; class Event_ChangeZoneProperties;
class Event_CreateArrow; class Event_CreateArrow;
@ -37,11 +51,176 @@ class Event_SetCounter;
class Event_Shuffle; class Event_Shuffle;
class Event_GameLogNotice; class Event_GameLogNotice;
/**
* @class PlayerEventHandler
* @brief Applies player-specific game events and emits corresponding log signals.
*
* Design notes:
* - All event handlers assume events are authoritative and already validated
* by the server.
* - Most handlers mutate both logical state (CardItem, CardZoneLogic, counters)
* and visual/UI state (views, arrows, menus).
* - Logging signals are emitted *after* or *during* state mutation, depending
* on whether later mutations would invalidate log data.
*/
class PlayerEventHandler : public QObject class PlayerEventHandler : public QObject
{ {
Q_OBJECT Q_OBJECT
public:
/**
* @brief Construct a PlayerEventHandler bound to a Player.
* @param player Owning player instance.
*/
explicit PlayerEventHandler(PlayerLogic *player);
/** @name Event dispatch
* @{
*/
/**
* @brief Dispatch a generic GameEvent to the appropriate handler.
*
* This is the single entry point used by GameEventHandler. It extracts
* the correct protobuf extension and forwards the event to a typed
* handler method.
*
* @param type Game event type enum.
* @param event Generic protobuf container.
* @param context Additional context (undo, judge, etc.).
* @param options Processing options (UI suppression, reveal behavior).
*/
void processGameEvent(GameEvent::GameEventType type,
const GameEvent &event,
const GameEventContext &context,
EventProcessingOptions options);
/** @} */
/** @name Chat and randomization events
* @{
*/
/// Handle in-game chat messages from this player.
void eventGameSay(const Event_GameSay &event);
/// Handle zone shuffle events (typically libraries).
void eventShuffle(const Event_Shuffle &event);
/// Handle die roll events.
void eventRollDie(const Event_RollDie &event);
/** @} */
/** @name Arrow and targeting events
* @{
*/
/// Create a visual arrow between cards or players.
void eventCreateArrow(const Event_CreateArrow &event);
/// Delete an existing arrow.
void eventDeleteArrow(const Event_DeleteArrow &event);
/** @} */
/** @name Token and card creation
* @{
*/
/// Create a token card in a target zone.
void eventCreateToken(const Event_CreateToken &event);
/** @} */
/** @name Card attribute and counter updates
* @{
*/
/**
* @brief Set a card attribute (tapped, PT, annotation, etc.).
*
* May apply to a single card or all cards in a zone if no card ID
* is provided by the event.
*/
void
eventSetCardAttr(const Event_SetCardAttr &event, const GameEventContext &context, EventProcessingOptions options);
/// Update a counter attached to a card.
void eventSetCardCounter(const Event_SetCardCounter &event);
/// Create a player-level counter.
void eventCreateCounter(const Event_CreateCounter &event);
/// Set a player-level counter value.
void eventSetCounter(const Event_SetCounter &event);
/// Delete a player-level counter.
void eventDelCounter(const Event_DelCounter &event);
/** @} */
/** @name Zone-level operations
* @{
*/
/// Log a zone dump (e.g. reveal graveyard/library contents).
void eventDumpZone(const Event_DumpZone &event);
/**
* @brief Move a card between zones and/or players.
*
* This is one of the most complex handlers:
* - Removes the card from the start zone
* - Updates card identity and ownership if needed
* - Handles attachments and arrows
* - Emits appropriate move or undo-draw logs
* - Inserts the card into the target zone
*/
void eventMoveCard(const Event_MoveCard &event, const GameEventContext &context);
/// Flip a card face up or face down.
void eventFlipCard(const Event_FlipCard &event);
/// Destroy a card and clean up attachments.
void eventDestroyCard(const Event_DestroyCard &event);
/// Attach or detach a card to/from another card.
void eventAttachCard(const Event_AttachCard &event);
/** @} */
/** @name Draw and reveal operations
* @{
*/
/// Draw one or more cards from the deck.
void eventDrawCards(const Event_DrawCards &event);
/**
* @brief Reveal cards from a zone.
*
* Handles peeking, in-place top-card reveals, full reveal windows,
* and write-access granting.
*/
void eventRevealCards(const Event_RevealCards &event, EventProcessingOptions options);
/** @} */
/** @name Zone configuration
* @{
*/
/// Update zone visibility and reveal behavior.
void eventChangeZoneProperties(const Event_ChangeZoneProperties &event);
/** @} */
void eventGameLogNotice(const Event_GameLogNotice &event);
signals: signals:
/** @name Logging signals
* @{
*/
void logSay(PlayerLogic *player, QString message); void logSay(PlayerLogic *player, QString message);
void logShuffle(PlayerLogic *player, CardZoneLogic *zone, int start, int end); void logShuffle(PlayerLogic *player, CardZoneLogic *zone, int start, int end);
void logRollDie(PlayerLogic *player, int sides, const QList<uint> &rolls); void logRollDie(PlayerLogic *player, int sides, const QList<uint> &rolls);
@ -83,40 +262,13 @@ signals:
bool isLentToAnotherPlayer = false); bool isLentToAnotherPlayer = false);
void logAlwaysRevealTopCard(PlayerLogic *player, CardZoneLogic *zone, bool reveal); void logAlwaysRevealTopCard(PlayerLogic *player, CardZoneLogic *zone, bool reveal);
void logAlwaysLookAtTopCard(PlayerLogic *player, CardZoneLogic *zone, bool reveal); void logAlwaysLookAtTopCard(PlayerLogic *player, CardZoneLogic *zone, bool reveal);
/** @} */
void cardZoneChanged(CardItem *card, bool sameZone); void cardZoneChanged(CardItem *card, bool sameZone);
void requestCardMenuUpdate(const CardItem *card); void requestCardMenuUpdate(const CardItem *card);
public:
PlayerEventHandler(PlayerLogic *player);
void processGameEvent(GameEvent::GameEventType type,
const GameEvent &event,
const GameEventContext &context,
EventProcessingOptions options);
void eventGameSay(const Event_GameSay &event);
void eventShuffle(const Event_Shuffle &event);
void eventRollDie(const Event_RollDie &event);
void eventCreateArrow(const Event_CreateArrow &event);
void eventDeleteArrow(const Event_DeleteArrow &event);
void eventCreateToken(const Event_CreateToken &event);
void
eventSetCardAttr(const Event_SetCardAttr &event, const GameEventContext &context, EventProcessingOptions options);
void eventSetCardCounter(const Event_SetCardCounter &event);
void eventCreateCounter(const Event_CreateCounter &event);
void eventSetCounter(const Event_SetCounter &event);
void eventDelCounter(const Event_DelCounter &event);
void eventDumpZone(const Event_DumpZone &event);
void eventMoveCard(const Event_MoveCard &event, const GameEventContext &context);
void eventFlipCard(const Event_FlipCard &event);
void eventDestroyCard(const Event_DestroyCard &event);
void eventAttachCard(const Event_AttachCard &event);
void eventDrawCards(const Event_DrawCards &event);
void eventRevealCards(const Event_RevealCards &event, EventProcessingOptions options);
void eventChangeZoneProperties(const Event_ChangeZoneProperties &event);
void eventGameLogNotice(const Event_GameLogNotice &event);
private: private:
/** Owning player instance. */
PlayerLogic *player; PlayerLogic *player;
void setCardAttrHelper(const GameEventContext &context, void setCardAttrHelper(const GameEventContext &context,

View file

@ -2,7 +2,7 @@
#include "../interface/widgets/tabs/tab_game.h" #include "../interface/widgets/tabs/tab_game.h"
Replay::Replay(QObject *_parent, GameReplay *_replay, bool isLocalGame) : AbstractGame(_parent) Replay::Replay(QObject *_parent, const GameReplay *_replay, bool isLocalGame) : AbstractGame(_parent)
{ {
gameState = new GameState(this, 0, -1, isLocalGame, {}, false, false, -1, false); gameState = new GameState(this, 0, -1, isLocalGame, {}, false, false, -1, false);
connect(gameMetaInfo, &GameMetaInfo::startedChanged, gameState, &GameState::onStartedChanged); connect(gameMetaInfo, &GameMetaInfo::startedChanged, gameState, &GameState::onStartedChanged);

View file

@ -15,7 +15,7 @@ class Replay : public AbstractGame
Q_OBJECT Q_OBJECT
public: public:
explicit Replay(QObject *_parent, GameReplay *_replay, bool isLocalGame); explicit Replay(QObject *_parent, const GameReplay *_replay, bool isLocalGame);
}; };
#endif // COCKATRICE_REPLAY_H #endif // COCKATRICE_REPLAY_H

View file

@ -3,6 +3,7 @@
#include "../../client/settings/cache_settings.h" #include "../../client/settings/cache_settings.h"
#include "../../game_graphics/board/card_item.h" #include "../../game_graphics/board/card_item.h"
#include <libcockatrice/settings/interface_settings.h>
/** /**
* @param _player the player that the cards are revealed to. * @param _player the player that the cards are revealed to.
* @param _origZone the zone the cards were revealed from. * @param _origZone the zone the cards were revealed from.
@ -57,7 +58,7 @@ bool ZoneViewZoneLogic::prepareAddCard(int x)
// autoclose check is done both here and in removeCard // autoclose check is done both here and in removeCard
if (cards.isEmpty() && !doInsert && SettingsCache::instance().getCloseEmptyCardView()) { if (cards.isEmpty() && !doInsert && SettingsCache::instance().userInterface().getCloseEmptyCardView()) {
emit closeView(); emit closeView();
} }
@ -144,7 +145,7 @@ void ZoneViewZoneLogic::removeCard(int position, bool toNewZone)
// card gets dragged within the view. // card gets dragged within the view.
// Another autoclose check is done in prepareAddCard so that the view autocloses if the last card was moved to an // Another autoclose check is done in prepareAddCard so that the view autocloses if the last card was moved to an
// unrevealed portion of the same zone. // unrevealed portion of the same zone.
if (cards.isEmpty() && SettingsCache::instance().getCloseEmptyCardView() && toNewZone) { if (cards.isEmpty() && SettingsCache::instance().userInterface().getCloseEmptyCardView() && toNewZone) {
emit closeView(); emit closeView();
return; return;
} }

View file

@ -0,0 +1,26 @@
#ifndef ANIMATED_ITEM_H
#define ANIMATED_ITEM_H
/**
* @file animated_item.h
* @ingroup GameGraphics
* @brief Interface for scene items driven by GameScene's shared animation timer.
*
* Items that want per-tick animation while a single QBasicTimer runs (instead of
* owning their own QTimer) implement this interface and register with the scene
* via GameScene::registerAnimationItem.
*/
class IAnimatedItem
{
public:
virtual ~IAnimatedItem() = default;
/**
* @brief Advances the item's animation by one timer tick.
* @return true while the animation is still running, false once it has finished.
*/
virtual bool animationEvent() = 0;
};
#endif

View file

@ -7,6 +7,7 @@
#include <QDebug> #include <QDebug>
#include <QGraphicsSceneMouseEvent> #include <QGraphicsSceneMouseEvent>
#include <QPainter> #include <QPainter>
#include <libcockatrice/settings/cards_display_settings.h>
const QColor GHOST_MASK = QColor(255, 255, 255, 50); const QColor GHOST_MASK = QColor(255, 255, 255, 50);
@ -34,12 +35,13 @@ AbstractCardDragItem::AbstractCardDragItem(AbstractCardItem *_item,
setCacheMode(DeviceCoordinateCache); setCacheMode(DeviceCoordinateCache);
connect(&SettingsCache::instance(), &SettingsCache::roundCardCornersChanged, this, [this](bool _roundCardCorners) { connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::roundCardCornersChanged, this,
Q_UNUSED(_roundCardCorners); [this](bool _roundCardCorners) {
Q_UNUSED(_roundCardCorners);
prepareGeometryChange(); prepareGeometryChange();
update(); update();
}); });
connect(item, &QObject::destroyed, this, &AbstractCardDragItem::deleteLater); connect(item, &QObject::destroyed, this, &AbstractCardDragItem::deleteLater);
} }
@ -47,7 +49,8 @@ AbstractCardDragItem::AbstractCardDragItem(AbstractCardItem *_item,
QPainterPath AbstractCardDragItem::shape() const QPainterPath AbstractCardDragItem::shape() const
{ {
QPainterPath shape; QPainterPath shape;
qreal cardCornerRadius = SettingsCache::instance().getRoundCardCorners() ? 0.05 * CardDimensions::WIDTH_F : 0.0; qreal cardCornerRadius =
SettingsCache::instance().cardsDisplay().getRoundCardCorners() ? 0.05 * CardDimensions::WIDTH_F : 0.0;
shape.addRoundedRect(boundingRect(), cardCornerRadius, cardCornerRadius); shape.addRoundedRect(boundingRect(), cardCornerRadius, cardCornerRadius);
return shape; return shape;
} }

View file

@ -12,6 +12,9 @@
#include <algorithm> #include <algorithm>
#include <libcockatrice/card/database/card_database.h> #include <libcockatrice/card/database/card_database.h>
#include <libcockatrice/card/database/card_database_manager.h> #include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/settings/appearance_settings.h>
#include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/settings/debug_settings.h>
AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef, PlayerLogic *_owner, int _id) AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef, PlayerLogic *_owner, int _id)
: ArrowTarget(_owner, parent), id(_id), cardRef(cardRef), tapped(false), facedown(false), tapAngle(0), : ArrowTarget(_owner, parent), id(_id), cardRef(cardRef), tapped(false), facedown(false), tapAngle(0),
@ -21,15 +24,17 @@ AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef
setFlag(ItemIsSelectable); setFlag(ItemIsSelectable);
setCacheMode(DeviceCoordinateCache); setCacheMode(DeviceCoordinateCache);
connect(&SettingsCache::instance(), &SettingsCache::displayCardNamesChanged, this, [this] { update(); }); connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::displayCardNamesChanged, this,
[this] { update(); });
refreshCardInfo(); refreshCardInfo();
connect(&SettingsCache::instance(), &SettingsCache::roundCardCornersChanged, this, [this](bool _roundCardCorners) { connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::roundCardCornersChanged, this,
Q_UNUSED(_roundCardCorners); [this](bool _roundCardCorners) {
Q_UNUSED(_roundCardCorners);
prepareGeometryChange(); prepareGeometryChange();
update(); update();
}); });
} }
AbstractCardItem::~AbstractCardItem() AbstractCardItem::~AbstractCardItem()
@ -45,7 +50,8 @@ QRectF AbstractCardItem::boundingRect() const
QPainterPath AbstractCardItem::shape() const QPainterPath AbstractCardItem::shape() const
{ {
QPainterPath shape; QPainterPath shape;
qreal cardCornerRadius = SettingsCache::instance().getRoundCardCorners() ? 0.05 * CardDimensions::WIDTH_F : 0.0; qreal cardCornerRadius =
SettingsCache::instance().cardsDisplay().getRoundCardCorners() ? 0.05 * CardDimensions::WIDTH_F : 0.0;
shape.addRoundedRect(boundingRect(), cardCornerRadius, cardCornerRadius); shape.addRoundedRect(boundingRect(), cardCornerRadius, cardCornerRadius);
return shape; return shape;
} }
@ -101,7 +107,7 @@ QSizeF AbstractCardItem::getTranslatedSize(QPainter *painter) const
void AbstractCardItem::transformPainter(QPainter *painter, const QSizeF &translatedSize, int angle) void AbstractCardItem::transformPainter(QPainter *painter, const QSizeF &translatedSize, int angle)
{ {
const int MAX_FONT_SIZE = SettingsCache::instance().getMaxFontSize(); const int MAX_FONT_SIZE = SettingsCache::instance().appearance().getMaxFontSize();
const int fontSize = std::max(9, MAX_FONT_SIZE); const int fontSize = std::max(9, MAX_FONT_SIZE);
QRectF totalBoundingRect = painter->combinedTransform().mapRect(boundingRect()); QRectF totalBoundingRect = painter->combinedTransform().mapRect(boundingRect());
@ -151,7 +157,7 @@ void AbstractCardItem::paintPicture(QPainter *painter, const QSizeF &translatedS
painter->drawPath(shape()); painter->drawPath(shape());
} }
if (translatedPixmap.isNull() || SettingsCache::instance().getDisplayCardNames() || facedown) { if (translatedPixmap.isNull() || SettingsCache::instance().cardsDisplay().getDisplayCardNames() || facedown) {
painter->save(); painter->save();
transformPainter(painter, translatedSize, angle); transformPainter(painter, translatedSize, angle);
painter->setPen(Qt::white); painter->setPen(Qt::white);
@ -234,7 +240,7 @@ void AbstractCardItem::setHovered(bool _hovered)
isHovered = _hovered; isHovered = _hovered;
setZValue(_hovered ? ZValues::HOVERED_CARD : realZValue); setZValue(_hovered ? ZValues::HOVERED_CARD : realZValue);
setScale(_hovered && SettingsCache::instance().getScaleCards() ? 1.1 : 1); setScale(_hovered && SettingsCache::instance().cardsDisplay().getScaleCards() ? 1.1 : 1);
setTransformOriginPoint(_hovered ? CardDimensions::WIDTH_HALF_F : 0, _hovered ? CardDimensions::HEIGHT_HALF_F : 0); setTransformOriginPoint(_hovered ? CardDimensions::WIDTH_HALF_F : 0, _hovered ? CardDimensions::HEIGHT_HALF_F : 0);
update(); update();
} }
@ -287,7 +293,7 @@ void AbstractCardItem::setTapped(bool _tapped, bool canAnimate)
} }
tapped = _tapped; tapped = _tapped;
if (SettingsCache::instance().getTapAnimation() && canAnimate) { if (SettingsCache::instance().cardsDisplay().getTapAnimation() && canAnimate) {
static_cast<GameScene *>(scene())->registerAnimationItem(this); static_cast<GameScene *>(scene())->registerAnimationItem(this);
} else { } else {
tapAngle = tapped ? 90 : 0; tapAngle = tapped ? 90 : 0;
@ -299,6 +305,11 @@ void AbstractCardItem::setTapped(bool _tapped, bool canAnimate)
} }
} }
bool AbstractCardItem::animationEvent()
{
return false;
}
void AbstractCardItem::setFaceDown(bool _facedown) void AbstractCardItem::setFaceDown(bool _facedown)
{ {
facedown = _facedown; facedown = _facedown;

View file

@ -7,6 +7,7 @@
#ifndef ABSTRACTCARDITEM_H #ifndef ABSTRACTCARDITEM_H
#define ABSTRACTCARDITEM_H #define ABSTRACTCARDITEM_H
#include "../animated_item.h"
#include "../card_dimensions.h" #include "../card_dimensions.h"
#include "arrow_target.h" #include "arrow_target.h"
#include "graphics_item_type.h" #include "graphics_item_type.h"
@ -16,7 +17,7 @@
class PlayerLogic; class PlayerLogic;
class AbstractCardItem : public ArrowTarget class AbstractCardItem : public ArrowTarget, public IAnimatedItem
{ {
Q_OBJECT Q_OBJECT
protected: protected:
@ -126,6 +127,9 @@ public:
emit deleteCardInfoPopup(cardRef.name); emit deleteCardInfoPopup(cardRef.name);
} }
/** @brief Default: no per-tick animation. Subclasses override to animate. */
bool animationEvent() override;
protected: protected:
void transformPainter(QPainter *painter, const QSizeF &translatedSize, int angle); void transformPainter(QPainter *painter, const QSizeF &translatedSize, int angle);
void mousePressEvent(QGraphicsSceneMouseEvent *event) override; void mousePressEvent(QGraphicsSceneMouseEvent *event) override;

View file

@ -1,6 +1,7 @@
#include "abstract_counter.h" #include "abstract_counter.h"
#include "../../client/settings/cache_settings.h" #include "../../client/settings/cache_settings.h"
#include "../../client/settings/shortcuts_settings.h"
#include "../../game/player/player_actions.h" #include "../../game/player/player_actions.h"
#include "../../game/player/player_logic.h" #include "../../game/player/player_logic.h"
#include "../../game_graphics/board/translate_counter_name.h" #include "../../game_graphics/board/translate_counter_name.h"
@ -28,8 +29,9 @@ AbstractCounter::AbstractCounter(CounterState *state,
{ {
setAcceptHoverEvents(true); setAcceptHoverEvents(true);
connect(state, &CounterState::valueChanged, this, [this](int, int newValue) { connect(state, &CounterState::valueChanged, this, [this](int oldValue, int newValue) {
value = newValue; value = newValue;
onValueChanged(oldValue, newValue);
update(); update();
}); });
@ -227,3 +229,9 @@ void AbstractCounterDialog::changeValue(int diff)
curValue += diff; curValue += diff;
setTextValue(QString::number(curValue)); setTextValue(QString::number(curValue));
} }
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).
}

View file

@ -35,6 +35,13 @@ protected:
bool hovered = false; bool hovered = false;
bool useNameForShortcut; bool useNameForShortcut;
/**
* @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.
*/
virtual void onValueChanged(int oldValue, int newValue);
void mousePressEvent(QGraphicsSceneMouseEvent *event) override; void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override; void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override;
void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override; void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override;

View file

@ -18,6 +18,7 @@
#include <libcockatrice/protocol/pb/command_attach_card.pb.h> #include <libcockatrice/protocol/pb/command_attach_card.pb.h>
#include <libcockatrice/protocol/pb/command_create_arrow.pb.h> #include <libcockatrice/protocol/pb/command_create_arrow.pb.h>
#include <libcockatrice/protocol/pb/command_delete_arrow.pb.h> #include <libcockatrice/protocol/pb/command_delete_arrow.pb.h>
#include <libcockatrice/settings/interface_settings.h>
#include <libcockatrice/utility/color.h> #include <libcockatrice/utility/color.h>
#include <libcockatrice/utility/zone_names.h> #include <libcockatrice/utility/zone_names.h>
@ -261,7 +262,7 @@ void ArrowDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
if (startZone->getName() == ZoneNames::HAND) { if (startZone->getName() == ZoneNames::HAND) {
startCard->playCard(false); startCard->playCard(false);
CardInfoPtr ci = startCard->getCard().getCardPtr(); CardInfoPtr ci = startCard->getCard().getCardPtr();
bool playToStack = SettingsCache::instance().getPlayToStack(); bool playToStack = SettingsCache::instance().userInterface().getPlayToStack();
if (ci && ((!playToStack && ci->getUiAttributes().tableRow == 3) || if (ci && ((!playToStack && ci->getUiAttributes().tableRow == 3) ||
(playToStack && ci->getUiAttributes().tableRow != 0 && (playToStack && ci->getUiAttributes().tableRow != 0 &&
startCard->getZone()->getName() != ZoneNames::STACK))) { startCard->getZone()->getName() != ZoneNames::STACK))) {

View file

@ -1,6 +1,7 @@
#include "card_item.h" #include "card_item.h"
#include "../../client/settings/cache_settings.h" #include "../../client/settings/cache_settings.h"
#include "../../client/settings/card_counter_settings.h"
#include "../../game/phase.h" #include "../../game/phase.h"
#include "../../game/player/player_actions.h" #include "../../game/player/player_actions.h"
#include "../../game/player/player_logic.h" #include "../../game/player/player_logic.h"
@ -19,6 +20,7 @@
#include <QPainter> #include <QPainter>
#include <libcockatrice/card/card_info.h> #include <libcockatrice/card/card_info.h>
#include <libcockatrice/protocol/pb/serverinfo_card.pb.h> #include <libcockatrice/protocol/pb/serverinfo_card.pb.h>
#include <libcockatrice/settings/interface_settings.h>
CardItem::CardItem(PlayerLogic *_owner, CardItem::CardItem(PlayerLogic *_owner,
QGraphicsItem *parent, QGraphicsItem *parent,
@ -301,7 +303,7 @@ void CardItem::drawArrow(const QColor &arrowColor)
auto *game = owner->getGame(); auto *game = owner->getGame();
PlayerLogic *arrowOwner = game->getPlayerManager()->getActiveLocalPlayer(game->getGameState()->getActivePlayer()); PlayerLogic *arrowOwner = game->getPlayerManager()->getActiveLocalPlayer(game->getGameState()->getActivePlayer());
int phase = 0; // 0 means to not set the phase int phase = 0; // 0 means to not set the phase
if (SettingsCache::instance().getDoNotDeleteArrowsInSubPhases()) { if (SettingsCache::instance().userInterface().getDoNotDeleteArrowsInSubPhases()) {
int currentPhase = game->getGameState()->getCurrentPhase(); int currentPhase = game->getGameState()->getCurrentPhase();
phase = Phases::getLastSubphase(currentPhase) + 1; phase = Phases::getLastSubphase(currentPhase) + 1;
} }
@ -420,7 +422,7 @@ void CardItem::playCard(bool faceDown)
if (tz) { if (tz) {
emit tz->toggleTapped(); emit tz->toggleTapped();
} else { } else {
if (SettingsCache::instance().getClickPlaysAllSelected()) { if (SettingsCache::instance().userInterface().getClickPlaysAllSelected()) {
if (faceDown) { if (faceDown) {
emit playSelectedFaceDown(this); emit playSelectedFaceDown(this);
} else { } else {
@ -484,7 +486,7 @@ static bool isUnwritableRevealZone(CardZoneLogic *zone)
void CardItem::handleClickedToPlay(bool shiftHeld) void CardItem::handleClickedToPlay(bool shiftHeld)
{ {
if (isUnwritableRevealZone(state->getZone())) { if (isUnwritableRevealZone(state->getZone())) {
if (SettingsCache::instance().getClickPlaysAllSelected()) { if (SettingsCache::instance().userInterface().getClickPlaysAllSelected()) {
emit hideSelected(this); emit hideSelected(this);
} else { } else {
state->getZone()->removeCard(this); state->getZone()->removeCard(this);
@ -501,7 +503,7 @@ void CardItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
return; return;
} }
if ((event->modifiers() != Qt::AltModifier) && (event->button() == Qt::LeftButton) && if ((event->modifiers() != Qt::AltModifier) && (event->button() == Qt::LeftButton) &&
(!SettingsCache::instance().getDoubleClickToPlay())) { (!SettingsCache::instance().userInterface().getDoubleClickToPlay())) {
handleClickedToPlay(event->modifiers().testFlag(Qt::ShiftModifier)); handleClickedToPlay(event->modifiers().testFlag(Qt::ShiftModifier));
} }
if (owner != nullptr) { if (owner != nullptr) {
@ -513,7 +515,7 @@ void CardItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
void CardItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) void CardItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
{ {
if ((event->modifiers() != Qt::AltModifier) && (event->buttons() == Qt::LeftButton) && if ((event->modifiers() != Qt::AltModifier) && (event->buttons() == Qt::LeftButton) &&
(SettingsCache::instance().getDoubleClickToPlay())) { (SettingsCache::instance().userInterface().getDoubleClickToPlay())) {
handleClickedToPlay(event->modifiers().testFlag(Qt::ShiftModifier)); handleClickedToPlay(event->modifiers().testFlag(Qt::ShiftModifier));
} }
event->accept(); event->accept();

View file

@ -12,7 +12,6 @@
#include "abstract_card_item.h" #include "abstract_card_item.h"
#include <libcockatrice/network/server/remote/game/server_card.h> #include <libcockatrice/network/server/remote/game/server_card.h>
#include <libcockatrice/utility/trice_limits.h>
class CardDatabase; class CardDatabase;
class CardDragItem; class CardDragItem;
@ -143,7 +142,7 @@ public:
void resetState(bool keepAnnotations = false); void resetState(bool keepAnnotations = false);
void processCardInfo(const ServerInfo_Card &_info); void processCardInfo(const ServerInfo_Card &_info);
bool animationEvent(); bool animationEvent() override;
CardDragItem *createDragItem(int _id, const QPointF &_pos, const QPointF &_scenePos, bool forceFaceDown); CardDragItem *createDragItem(int _id, const QPointF &_pos, const QPointF &_scenePos, bool forceFaceDown);
void deleteDragItem(); void deleteDragItem();
void drawArrow(const QColor &arrowColor); void drawArrow(const QColor &arrowColor);

View file

@ -11,6 +11,7 @@
#include <libcockatrice/card/card_info.h> #include <libcockatrice/card/card_info.h>
#include <libcockatrice/deck_list/deck_list.h> #include <libcockatrice/deck_list/deck_list.h>
#include <libcockatrice/deck_list/tree/deck_list_card_node.h> #include <libcockatrice/deck_list/tree/deck_list_card_node.h>
#include <libcockatrice/settings/cards_display_settings.h>
DeckViewCardDragItem::DeckViewCardDragItem(DeckViewCard *_item, DeckViewCardDragItem::DeckViewCardDragItem(DeckViewCard *_item,
const QPointF &_hotSpot, const QPointF &_hotSpot,
@ -77,11 +78,12 @@ DeckViewCard::DeckViewCard(QGraphicsItem *parent, const CardRef &cardRef, const
{ {
setAcceptHoverEvents(true); setAcceptHoverEvents(true);
connect(&SettingsCache::instance(), &SettingsCache::roundCardCornersChanged, this, [this](bool _roundCardCorners) { connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::roundCardCornersChanged, this,
Q_UNUSED(_roundCardCorners); [this](bool _roundCardCorners) {
Q_UNUSED(_roundCardCorners);
update(); update();
}); });
} }
DeckViewCard::~DeckViewCard() DeckViewCard::~DeckViewCard()
@ -99,7 +101,8 @@ void DeckViewCard::paint(QPainter *painter, const QStyleOptionGraphicsItem *opti
pen.setJoinStyle(Qt::MiterJoin); pen.setJoinStyle(Qt::MiterJoin);
pen.setColor(originZone == DECK_ZONE_MAIN ? Qt::green : Qt::red); pen.setColor(originZone == DECK_ZONE_MAIN ? Qt::green : Qt::red);
painter->setPen(pen); painter->setPen(pen);
qreal cardRadius = SettingsCache::instance().getRoundCardCorners() ? 0.05 * (CardDimensions::WIDTH_F - 3) : 0.0; qreal cardRadius =
SettingsCache::instance().cardsDisplay().getRoundCardCorners() ? 0.05 * (CardDimensions::WIDTH_F - 3) : 0.0;
painter->drawRoundedRect(QRectF(1.5, 1.5, CardDimensions::WIDTH_F - 3, CardDimensions::HEIGHT_F - 3), cardRadius, painter->drawRoundedRect(QRectF(1.5, 1.5, CardDimensions::WIDTH_F - 3, CardDimensions::HEIGHT_F - 3), cardRadius,
cardRadius); cardRadius);
painter->restore(); painter->restore();
@ -360,6 +363,16 @@ void DeckViewScene::rebuildTree()
return; return;
} }
QStringList requiredZones = {DECK_ZONE_MAIN, DECK_ZONE_SIDE};
for (const QString &zoneName : requiredZones) {
if (!cardContainers.contains(zoneName)) {
auto *container = new DeckViewCardContainer(zoneName);
cardContainers.insert(zoneName, container);
addItem(container);
}
}
for (auto *currentZone : deck->getZoneNodes()) { for (auto *currentZone : deck->getZoneNodes()) {
DeckViewCardContainer *container = cardContainers.value(currentZone->getName(), 0); DeckViewCardContainer *container = cardContainers.value(currentZone->getName(), 0);
if (!container) { if (!container) {

View file

@ -1,6 +1,7 @@
#include "deck_view_container.h" #include "deck_view_container.h"
#include "../../client/settings/cache_settings.h" #include "../../client/settings/cache_settings.h"
#include "../../client/settings/shortcuts_settings.h"
#include "../../interface/card_picture_loader/card_picture_loader.h" #include "../../interface/card_picture_loader/card_picture_loader.h"
#include "../../interface/deck_loader/deck_loader.h" #include "../../interface/deck_loader/deck_loader.h"
#include "../../interface/widgets/dialogs/dlg_load_deck.h" #include "../../interface/widgets/dialogs/dlg_load_deck.h"
@ -19,7 +20,8 @@
#include <libcockatrice/protocol/pb/command_set_sideboard_plan.pb.h> #include <libcockatrice/protocol/pb/command_set_sideboard_plan.pb.h>
#include <libcockatrice/protocol/pb/response_deck_download.pb.h> #include <libcockatrice/protocol/pb/response_deck_download.pb.h>
#include <libcockatrice/protocol/pending_command.h> #include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/utility/trice_limits.h> #include <libcockatrice/settings/visual_deck_storage_settings.h>
#include <libcockatrice/utility/string_limits.h>
ToggleButton::ToggleButton(QWidget *parent) : QPushButton(parent), state(false) ToggleButton::ToggleButton(QWidget *parent) : QPushButton(parent), state(false)
{ {
@ -95,8 +97,8 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent)
&DeckViewContainer::refreshShortcuts); &DeckViewContainer::refreshShortcuts);
refreshShortcuts(); refreshShortcuts();
connect(&SettingsCache::instance(), &SettingsCache::visualDeckStorageInGameChanged, this, connect(&SettingsCache::instance().visualDeckStorage(), &VisualDeckStorageSettings::visualDeckStorageInGameChanged,
&DeckViewContainer::setVisualDeckStorageExists); this, &DeckViewContainer::setVisualDeckStorageExists);
switchToDeckSelectView(); switchToDeckSelectView();
} }
@ -138,7 +140,7 @@ static void setVisibility(QPushButton *button, bool visible)
void DeckViewContainer::switchToDeckSelectView() void DeckViewContainer::switchToDeckSelectView()
{ {
if (SettingsCache::instance().getVisualDeckStorageInGame()) { if (SettingsCache::instance().visualDeckStorage().getVisualDeckStorageInGame()) {
deckView->setHidden(true); deckView->setHidden(true);
tryCreateVisualDeckStorageWidget(); tryCreateVisualDeckStorageWidget();
@ -209,6 +211,7 @@ void DeckViewContainer::refreshShortcuts()
loadLocalButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadLocalButton")); loadLocalButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadLocalButton"));
loadRemoteButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadRemoteButton")); loadRemoteButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadRemoteButton"));
loadFromClipboardButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadFromClipboardButton")); loadFromClipboardButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadFromClipboardButton"));
loadFromWebsiteButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadFromWebsiteButton"));
unloadDeckButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/unloadDeckButton")); unloadDeckButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/unloadDeckButton"));
readyStartButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/readyStartButton")); readyStartButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/readyStartButton"));
sideboardLockButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/sideboardLockButton")); sideboardLockButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/sideboardLockButton"));

View file

@ -20,7 +20,10 @@
#include <libcockatrice/deck_list/deck_list.h> #include <libcockatrice/deck_list/deck_list.h>
#include <libcockatrice/models/database/card_database_model.h> #include <libcockatrice/models/database/card_database_model.h>
#include <libcockatrice/models/database/token/token_display_model.h> #include <libcockatrice/models/database/token/token_display_model.h>
#include <libcockatrice/utility/trice_limits.h> #include <libcockatrice/settings/card_override_settings.h>
#include <libcockatrice/settings/interface_settings.h>
#include <libcockatrice/settings/layouts_settings.h>
#include <libcockatrice/utility/string_limits.h>
DlgCreateToken::DlgCreateToken(const QStringList &_predefinedTokens, QWidget *parent) DlgCreateToken::DlgCreateToken(const QStringList &_predefinedTokens, QWidget *parent)
: QDialog(parent), predefinedTokens(_predefinedTokens) : QDialog(parent), predefinedTokens(_predefinedTokens)
@ -186,7 +189,7 @@ void DlgCreateToken::tokenSelectionChanged(const QModelIndex &current, const QMo
const QChar cardColor = cardInfo->getColorChar(); const QChar cardColor = cardInfo->getColorChar();
colorEdit->setCurrentIndex(colorEdit->findData(cardColor, Qt::UserRole, Qt::MatchFixedString)); colorEdit->setCurrentIndex(colorEdit->findData(cardColor, Qt::UserRole, Qt::MatchFixedString));
ptEdit->setText(cardInfo->getPowTough()); ptEdit->setText(cardInfo->getPowTough());
if (SettingsCache::instance().getAnnotateTokens()) { if (SettingsCache::instance().userInterface().getAnnotateTokens()) {
annotationEdit->setText(cardInfo->getText()); annotationEdit->setText(cardInfo->getText());
} }
} else { } else {

View file

@ -5,7 +5,7 @@
#include <QSpinBox> #include <QSpinBox>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QWidget> #include <QWidget>
#include <libcockatrice/utility/trice_limits.h> #include <libcockatrice/utility/dice_limits.h>
DlgRollDice::DlgRollDice(QWidget *parent) : QDialog(parent) DlgRollDice::DlgRollDice(QWidget *parent) : QDialog(parent)
{ {

View file

@ -17,8 +17,8 @@
#include <QDebug> #include <QDebug>
#include <QGraphicsSceneMouseEvent> #include <QGraphicsSceneMouseEvent>
#include <QGraphicsView> #include <QGraphicsView>
#include <QSet>
#include <QtMath> #include <QtMath>
#include <libcockatrice/settings/interface_settings.h>
#include <libcockatrice/utility/zone_names.h> #include <libcockatrice/utility/zone_names.h>
#include <numeric> #include <numeric>
@ -36,7 +36,7 @@ GameScene::GameScene(PhasesToolbar *_phasesToolbar, QObject *parent)
{ {
animationTimer = new QBasicTimer; animationTimer = new QBasicTimer;
addItem(phasesToolbar); addItem(phasesToolbar);
connect(&SettingsCache::instance(), &SettingsCache::minPlayersForMultiColumnLayoutChanged, this, connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::minPlayersForMultiColumnLayoutChanged, this,
&GameScene::rearrange); &GameScene::rearrange);
rearrange(); rearrange();
@ -44,7 +44,25 @@ GameScene::GameScene(PhasesToolbar *_phasesToolbar, QObject *parent)
GameScene::~GameScene() GameScene::~GameScene()
{ {
// 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; delete animationTimer;
animationTimer = nullptr;
// Delete all ArrowItems before QGraphicsScene's base destructor runs.
// QGraphicsScene::~QGraphicsScene() destroys items in arbitrary order.
// If a PlayerTarget is destroyed before an ArrowItem pointing to it,
// ArrowItem::onTargetDestroyed fires and emits on the partially-destroyed
// GameScene, causing a segfault.
for (auto *item : items()) {
if (auto *arrow = qgraphicsitem_cast<ArrowItem *>(item)) {
delete arrow;
}
}
// DO NOT call clearViews() here // DO NOT call clearViews() here
// clearViews calls close() on the zoneViews, which sends signals; sending signals in destructors leads to segfaults // clearViews calls close() on the zoneViews, which sends signals; sending signals in destructors leads to segfaults
@ -324,7 +342,7 @@ QList<PlayerLogic *> GameScene::rotatePlayers(const QList<PlayerLogic *> &active
int GameScene::determineColumnCount(int playerCount) int GameScene::determineColumnCount(int playerCount)
{ {
return playerCount < SettingsCache::instance().getMinPlayersForMultiColumnLayout() ? 1 : 2; return playerCount < SettingsCache::instance().userInterface().getMinPlayersForMultiColumnLayout() ? 1 : 2;
} }
/** /**
@ -529,7 +547,9 @@ void GameScene::clearArrowsForPlayer(int playerId)
void GameScene::clearArrowsForPlayerLocally(int playerId) void GameScene::clearArrowsForPlayerLocally(int playerId)
{ {
for (int arrowId : arrowRegistry.idsForPlayer(playerId)) { for (int arrowId : arrowRegistry.idsForPlayer(playerId)) {
arrowRegistry.take(playerId, arrowId)->delArrow(); if (auto *arrow = arrowRegistry.take(playerId, arrowId)) {
arrow->delArrow();
}
} }
} }
@ -722,30 +742,45 @@ bool GameScene::event(QEvent *event)
void GameScene::timerEvent(QTimerEvent * /*event*/) void GameScene::timerEvent(QTimerEvent * /*event*/)
{ {
QMutableSetIterator<CardItem *> i(cardsToAnimate); QMutableHashIterator<QObject *, IAnimatedItem *> i(animatedItems);
while (i.hasNext()) { while (i.hasNext()) {
i.next(); i.next();
if (!i.value()->animationEvent()) { if (!i.value()->animationEvent()) {
i.remove(); i.remove();
} }
} }
if (cardsToAnimate.isEmpty()) { if (animatedItems.isEmpty()) {
animationTimer->stop(); animationTimer->stop();
} }
} }
void GameScene::registerAnimationItem(AbstractCardItem *card) void GameScene::registerAnimationItem(IAnimatedItem *item)
{ {
cardsToAnimate.insert(static_cast<CardItem *>(card)); auto *object = dynamic_cast<QObject *>(item);
if (!animationTimer->isActive()) { if (!object) {
return;
}
if (!animatedItems.contains(object)) {
connect(object, &QObject::destroyed, this, &GameScene::removeAnimatedItem);
}
animatedItems.insert(object, item);
if (animationTimer && !animationTimer->isActive()) {
animationTimer->start(10, this); animationTimer->start(10, this);
} }
} }
void GameScene::unregisterAnimationItem(AbstractCardItem *card) void GameScene::unregisterAnimationItem(IAnimatedItem *item)
{ {
cardsToAnimate.remove(static_cast<CardItem *>(card)); animatedItems.remove(dynamic_cast<QObject *>(item));
if (cardsToAnimate.isEmpty()) { if (animationTimer && animatedItems.isEmpty()) {
animationTimer->stop();
}
}
void GameScene::removeAnimatedItem(QObject *item)
{
animatedItems.remove(item);
if (animationTimer && animatedItems.isEmpty()) {
animationTimer->stop(); animationTimer->stop();
} }
} }

View file

@ -4,13 +4,14 @@
#include "../game/arrow_registry.h" #include "../game/arrow_registry.h"
#include "../game/board/arrow_data.h" #include "../game/board/arrow_data.h"
#include "../game/zones/card_zone_logic.h" #include "../game/zones/card_zone_logic.h"
#include "animated_item.h"
#include "board/arrow_item.h" #include "board/arrow_item.h"
#include <QGraphicsScene> #include <QGraphicsScene>
#include <QHash>
#include <QList> #include <QList>
#include <QLoggingCategory> #include <QLoggingCategory>
#include <QPointer> #include <QPointer>
#include <QSet>
inline Q_LOGGING_CATEGORY(GameSceneLog, "game_scene"); inline Q_LOGGING_CATEGORY(GameSceneLog, "game_scene");
inline Q_LOGGING_CATEGORY(GameScenePlayerAdditionRemovalLog, "game_scene.player_addition_removal"); inline Q_LOGGING_CATEGORY(GameScenePlayerAdditionRemovalLog, "game_scene.player_addition_removal");
@ -24,6 +25,7 @@ class CardItem;
class ServerInfo_Card; class ServerInfo_Card;
class PhasesToolbar; class PhasesToolbar;
class QBasicTimer; class QBasicTimer;
class QObject;
/** /**
* @class GameScene * @class GameScene
@ -50,8 +52,8 @@ private:
QList<ZoneViewWidget *> zoneViews; ///< Active zone view widgets QList<ZoneViewWidget *> zoneViews; ///< Active zone view widgets
QSize viewSize; ///< Current view size QSize viewSize; ///< Current view size
QPointer<CardItem> hoveredCard; ///< Currently hovered card QPointer<CardItem> hoveredCard; ///< Currently hovered card
QBasicTimer *animationTimer; ///< Timer for card animations QBasicTimer *animationTimer; ///< Timer for scene animations
QSet<CardItem *> cardsToAnimate; ///< Cards currently animating QHash<QObject *, IAnimatedItem *> animatedItems; ///< Items currently animating
int playerRotation; ///< Rotation offset for player layout int playerRotation; ///< Rotation offset for player layout
/** /**
@ -182,15 +184,24 @@ public:
/** @brief Updates hovered card highlighting. */ /** @brief Updates hovered card highlighting. */
void updateHoveredCard(CardItem *newCard); void updateHoveredCard(CardItem *newCard);
/** @brief Registers a card for animation updates. */ /**
void registerAnimationItem(AbstractCardItem *card); * @brief Registers an item for animation updates with the shared scene timer.
*
* The item must inherit QObject; it is unregistered automatically when it is
* destroyed, so it may be deleted mid-animation without a dangling pointer.
*/
void registerAnimationItem(IAnimatedItem *item);
/** @brief Unregisters a card from animation updates. */ /** @brief Unregisters an item from animation updates. */
void unregisterAnimationItem(AbstractCardItem *card); void unregisterAnimationItem(IAnimatedItem *item);
void startRubberBand(const QPointF &selectionOrigin); void startRubberBand(const QPointF &selectionOrigin);
void resizeRubberBand(const QPointF &cursorPoint, int selectedCount); void resizeRubberBand(const QPointF &cursorPoint, int selectedCount);
void stopRubberBand(); void stopRubberBand();
private slots:
/** @brief Removes a destroyed item from the animation set. */
void removeAnimatedItem(QObject *item);
public slots: public slots:
void onCardSelectionChanged(AbstractCardItem *card, bool selected); void onCardSelectionChanged(AbstractCardItem *card, bool selected);
void onCardRightClicked(AbstractCardItem *card, QPoint screenPos); void onCardRightClicked(AbstractCardItem *card, QPoint screenPos);

View file

@ -1,12 +1,17 @@
#include "game_view.h" #include "game_view.h"
#include "../client/settings/cache_settings.h" #include "../client/settings/cache_settings.h"
#include "../client/settings/shortcuts_settings.h"
#include "game_scene.h" #include "game_scene.h"
#include <QAction> #include <QAction>
#include <QGridLayout>
#include <QLabel> #include <QLabel>
#include <QLayout>
#include <QResizeEvent> #include <QResizeEvent>
#include <QRubberBand> #include <QRubberBand>
#include <libcockatrice/settings/interface_settings.h>
#include <libcockatrice/utility/qt_utils.h>
// QRubberBand calls raise() in showEvent() and changeEvent() to stay on top of siblings. // QRubberBand calls raise() in showEvent() and changeEvent() to stay on top of siblings.
// This subclass disables that behavior so dragCountLabel can appear above it. // This subclass disables that behavior so dragCountLabel can appear above it.
@ -34,7 +39,6 @@ GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, par
{ {
setBackgroundBrush(QBrush(QColor(0, 0, 0))); setBackgroundBrush(QBrush(QColor(0, 0, 0)));
setRenderHints(QPainter::TextAntialiasing | QPainter::Antialiasing); setRenderHints(QPainter::TextAntialiasing | QPainter::Antialiasing);
setFocusPolicy(Qt::ClickFocus);
setViewportUpdateMode(BoundingRectViewportUpdate); setViewportUpdateMode(BoundingRectViewportUpdate);
connect(scene, &GameScene::sceneRectChanged, this, &GameView::updateSceneRect); connect(scene, &GameScene::sceneRectChanged, this, &GameView::updateSceneRect);
@ -43,6 +47,12 @@ GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, par
connect(scene, &GameScene::sigResizeRubberBand, this, &GameView::resizeRubberBand); connect(scene, &GameScene::sigResizeRubberBand, this, &GameView::resizeRubberBand);
connect(scene, &GameScene::sigStopRubberBand, this, &GameView::stopRubberBand); connect(scene, &GameScene::sigStopRubberBand, this, &GameView::stopRubberBand);
connect(scene, &QGraphicsScene::selectionChanged, this, [this]() { updateTotalSelectionCount(); }); connect(scene, &QGraphicsScene::selectionChanged, this, [this]() { updateTotalSelectionCount(); });
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::tallyTypeChanged, this,
[this] { updateTotalSelectionCount(); });
setFocusDisabled(SettingsCache::instance().userInterface().getKeepGameChatFocus());
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::keepGameChatFocusChanged, this,
&GameView::setFocusDisabled);
aCloseMostRecentZoneView = new QAction(this); aCloseMostRecentZoneView = new QAction(this);
@ -53,31 +63,40 @@ GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, par
refreshShortcuts(); refreshShortcuts();
rubberBand = new SelectionRubberBand(QRubberBand::Rectangle, this); rubberBand = new SelectionRubberBand(QRubberBand::Rectangle, this);
const QString countLabelStyle = "color: white; " const QString baseProperties = "color: white; "
"font-size: 14px; " "font-family: monospace; "
"font-weight: bold; " "background-color: rgba(0, 0, 0, 160); "
"background-color: rgba(0, 0, 0, 160); " "border-radius: 3px; "
"border-radius: 3px; " "padding: 1px 2px; "
"padding: 1px 2px;"; "white-space: pre;";
const QString dragCountLabelStyle = baseProperties + "font-size: 14px; font-weight: bold;";
const QString totalCountLabelStyle = baseProperties + "font-size: 16px; font-weight: bold;";
const QString subtypeTallyLabelStyle = baseProperties + "font-size: 12px;";
dragCountLabel = new QLabel(this); dragCountLabel = new QLabel(this);
dragCountLabel->setStyleSheet(countLabelStyle); dragCountLabel->setStyleSheet(dragCountLabelStyle);
dragCountLabel->hide(); dragCountLabel->hide();
dragCountLabel->raise(); dragCountLabel->raise();
totalCountLabel = new QLabel(this); totalCountLabel = new QLabel(this);
totalCountLabel->setStyleSheet(countLabelStyle); totalCountLabel->setStyleSheet(totalCountLabelStyle);
totalCountLabel->hide(); totalCountLabel->hide();
tallyContainer = new QWidget(this);
tallyContainer->setStyleSheet(subtypeTallyLabelStyle);
tallyLayout = new QGridLayout(tallyContainer);
tallyLayout->setContentsMargins(2, 2, 2, 2);
tallyLayout->setSpacing(2);
tallyContainer->hide();
} }
void GameView::resizeEvent(QResizeEvent *event) void GameView::resizeEvent(QResizeEvent *event)
{ {
QGraphicsView::resizeEvent(event); QGraphicsView::resizeEvent(event);
GameScene *s = dynamic_cast<GameScene *>(scene()); GameScene *s = static_cast<GameScene *>(scene());
if (s) { s->processViewSizeChange(event->size());
s->processViewSizeChange(event->size());
}
updateSceneRect(scene()->sceneRect()); updateSceneRect(scene()->sceneRect());
updateTotalSelectionCount(event->size()); updateTotalSelectionCount(event->size());
@ -111,7 +130,7 @@ void GameView::resizeRubberBand(const QPointF &cursorPoint, int selectedCount)
QRect rect = QRect(mapFromScene(selectionOrigin), cursor).normalized(); QRect rect = QRect(mapFromScene(selectionOrigin), cursor).normalized();
rubberBand->setGeometry(rect); rubberBand->setGeometry(rect);
if (!SettingsCache::instance().getShowDragSelectionCount()) { if (!SettingsCache::instance().userInterface().getShowDragSelectionCount()) {
dragCountLabel->hide(); dragCountLabel->hide();
return; return;
} }
@ -162,27 +181,117 @@ void GameView::refreshShortcuts()
SettingsCache::instance().shortcuts().getShortcut("Player/aCloseMostRecentZoneView")); SettingsCache::instance().shortcuts().getShortcut("Player/aCloseMostRecentZoneView"));
} }
void GameView::clearTallyLabels()
{
QtUtils::clearLayoutRec(tallyLayout);
}
QSize GameView::rebuildTallyLabels(const QList<TallyRow> &entries)
{
clearTallyLabels();
const QString nameStyle = QStringLiteral("color: white; font-size: 12px; background: transparent;");
const QString countStyle =
QStringLiteral("color: white; font-size: 14px; font-weight: bold; background: transparent;");
int totalHeight = 0;
int maxNameWidth = 0;
int maxCountWidth = 0;
int row = 0;
for (const TallyRow &entry : entries) {
auto *nameLabel = new QLabel(entry.name, tallyContainer);
nameLabel->setStyleSheet(nameStyle);
nameLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
tallyLayout->addWidget(nameLabel, row, 0);
auto *countLabel = new QLabel(entry.value, tallyContainer);
countLabel->setStyleSheet(countStyle);
countLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
tallyLayout->addWidget(countLabel, row, 1);
QSize nameSize = nameLabel->sizeHint();
QSize countSize = countLabel->sizeHint();
maxNameWidth = qMax(maxNameWidth, nameSize.width());
maxCountWidth = qMax(maxCountWidth, countSize.width());
totalHeight += qMax(nameSize.height(), countSize.height());
++row;
}
int spacing = tallyLayout->spacing();
int margins = tallyLayout->contentsMargins().left() + tallyLayout->contentsMargins().right();
int verticalMargins = tallyLayout->contentsMargins().top() + tallyLayout->contentsMargins().bottom();
int width = maxNameWidth + spacing + maxCountWidth + margins;
int height = totalHeight + (row - 1) * spacing + verticalMargins;
return QSize(width, height);
}
void GameView::updateTotalSelectionCount(const QSize &viewSize) void GameView::updateTotalSelectionCount(const QSize &viewSize)
{ {
if (!SettingsCache::instance().getShowTotalSelectionCount()) { constexpr int kMarginInPixels = 10;
totalCountLabel->hide(); constexpr int kSpacingBetweenLabels = 4;
return;
} int availableWidth = viewSize.isValid() ? viewSize.width() : viewport()->width();
int availableHeight = viewSize.isValid() ? viewSize.height() : viewport()->height();
int count = scene()->selectedItems().count(); int count = scene()->selectedItems().count();
if (count > 1) { if (!SettingsCache::instance().userInterface().getShowTotalSelectionCount() || count <= 1) {
totalCountLabel->hide();
} else {
totalCountLabel->setText(QString::number(count)); totalCountLabel->setText(QString::number(count));
totalCountLabel->adjustSize(); totalCountLabel->adjustSize();
constexpr int kMarginInPixels = 10;
int availableWidth = viewSize.isValid() ? viewSize.width() : viewport()->width();
int availableHeight = viewSize.isValid() ? viewSize.height() : viewport()->height();
int x = availableWidth - totalCountLabel->width() - kMarginInPixels; int x = availableWidth - totalCountLabel->width() - kMarginInPixels;
int y = availableHeight - totalCountLabel->height() - kMarginInPixels; int y = availableHeight - totalCountLabel->height() - kMarginInPixels;
totalCountLabel->move(x, y); totalCountLabel->move(x, y);
totalCountLabel->show(); totalCountLabel->show();
} else {
totalCountLabel->hide();
} }
TallyType tallyType = Tally::intToType(SettingsCache::instance().userInterface().getTallyType());
GameScene *gameScene = static_cast<GameScene *>(scene());
QList<TallyRow> entries = Tally::compute(gameScene->selectedCards(), tallyType);
if (entries.isEmpty()) {
tallyContainer->hide();
cachedTallyRows.clear();
return;
}
// Only rebuild labels if entries changed
QSize containerSize;
if (entries != cachedTallyRows) {
cachedTallyRows = entries;
containerSize = rebuildTallyLabels(entries);
tallyContainer->resize(containerSize);
} else {
containerSize = tallyContainer->size();
}
int x = availableWidth - containerSize.width() - kMarginInPixels;
int y;
if (totalCountLabel->isVisible()) {
y = totalCountLabel->y() - containerSize.height() - kSpacingBetweenLabels;
} else {
y = availableHeight - containerSize.height() - kMarginInPixels;
}
y = qMax(kMarginInPixels, y);
tallyContainer->move(x, y);
tallyContainer->show();
}
/**
* Disabling focus on the game view will allow chat to maintain the autofocusing behavior of pre 2.10.3,
* at the cost of disabling the zone view search bar.
*/
void GameView::setFocusDisabled(bool disabled)
{
setFocusPolicy(disabled ? Qt::NoFocus : Qt::ClickFocus);
} }

View file

@ -7,9 +7,12 @@
#ifndef GAMEVIEW_H #ifndef GAMEVIEW_H
#define GAMEVIEW_H #define GAMEVIEW_H
#include "tally/tally.h"
#include <QGraphicsView> #include <QGraphicsView>
class GameScene; class GameScene;
class QGridLayout;
class QLabel; class QLabel;
class QRubberBand; class QRubberBand;
@ -21,7 +24,13 @@ private:
QRubberBand *rubberBand; QRubberBand *rubberBand;
QLabel *dragCountLabel; QLabel *dragCountLabel;
QLabel *totalCountLabel; QLabel *totalCountLabel;
QWidget *tallyContainer;
QGridLayout *tallyLayout;
QPointF selectionOrigin; QPointF selectionOrigin;
QList<TallyRow> cachedTallyRows; ///< Cached entries to avoid redundant rebuilds
QSize rebuildTallyLabels(const QList<TallyRow> &entries);
void clearTallyLabels();
protected: protected:
void resizeEvent(QResizeEvent *event) override; void resizeEvent(QResizeEvent *event) override;
@ -31,6 +40,7 @@ private slots:
void stopRubberBand(); void stopRubberBand();
void refreshShortcuts(); void refreshShortcuts();
void updateTotalSelectionCount(const QSize &viewSize = QSize()); void updateTotalSelectionCount(const QSize &viewSize = QSize());
void setFocusDisabled(bool disabled);
public slots: public slots:
void updateSceneRect(const QRectF &rect); void updateSceneRect(const QRectF &rect);

View file

@ -1,6 +1,7 @@
#include "card_menu.h" #include "card_menu.h"
#include "../../../client/settings/card_counter_settings.h" #include "../../../client/settings/card_counter_settings.h"
#include "../../../client/settings/shortcuts_settings.h"
#include "../../../interface/widgets/tabs/tab_game.h" #include "../../../interface/widgets/tabs/tab_game.h"
#include "../../board/card_item.h" #include "../../board/card_item.h"
#include "../../game/player/player_actions.h" #include "../../game/player/player_actions.h"

View file

@ -1,5 +1,6 @@
#include "grave_menu.h" #include "grave_menu.h"
#include "../../../client/settings/shortcuts_settings.h"
#include "../../game/abstract_game.h" #include "../../game/abstract_game.h"
#include "../../game/player/player_actions.h" #include "../../game/player/player_actions.h"
#include "../../game/player/player_logic.h" #include "../../game/player/player_logic.h"

View file

@ -1,5 +1,6 @@
#include "move_menu.h" #include "move_menu.h"
#include "../../../client/settings/shortcuts_settings.h"
#include "../../game/player/player_actions.h" #include "../../game/player/player_actions.h"
#include "../../game/player/player_logic.h" #include "../../game/player/player_logic.h"
#include "../card_menu_action_type.h" #include "../card_menu_action_type.h"

View file

@ -1,5 +1,6 @@
#include "player_menu.h" #include "player_menu.h"
#include "../../../client/settings/shortcuts_settings.h"
#include "../../../game_graphics/zones/hand_zone.h" #include "../../../game_graphics/zones/hand_zone.h"
#include "../../../game_graphics/zones/pile_zone.h" #include "../../../game_graphics/zones/pile_zone.h"
#include "../../../game_graphics/zones/table_zone.h" #include "../../../game_graphics/zones/table_zone.h"
@ -44,6 +45,8 @@ PlayerMenu::PlayerMenu(PlayerGraphicsItem *_player) : QObject(_player), player(_
utilityMenu = nullptr; utilityMenu = nullptr;
} }
tallyMenu = addManagedMenu<TallyMenu>();
if (player->getLogic()->getPlayerInfo()->getLocal()) { if (player->getLogic()->getPlayerInfo()->getLocal()) {
sayMenu = addManagedMenu<SayMenu>(player); sayMenu = addManagedMenu<SayMenu>(player);
} else { } else {

View file

@ -15,11 +15,13 @@
#include "rfg_menu.h" #include "rfg_menu.h"
#include "say_menu.h" #include "say_menu.h"
#include "sideboard_menu.h" #include "sideboard_menu.h"
#include "tally_menu.h"
#include "utility_menu.h" #include "utility_menu.h"
#include <QList> #include <QList>
#include <QMenu> #include <QMenu>
#include <QObject> #include <QObject>
#include <libcockatrice/utility/card_ref.h>
class CardItem; class CardItem;
class CardMenu; class CardMenu;
@ -87,6 +89,7 @@ private:
GraveyardMenu *graveMenu; GraveyardMenu *graveMenu;
RfgMenu *rfgMenu; RfgMenu *rfgMenu;
UtilityMenu *utilityMenu; UtilityMenu *utilityMenu;
TallyMenu *tallyMenu;
SayMenu *sayMenu; SayMenu *sayMenu;
CustomZoneMenu *customZonesMenu; CustomZoneMenu *customZonesMenu;

View file

@ -1,5 +1,6 @@
#include "pt_menu.h" #include "pt_menu.h"
#include "../../../client/settings/shortcuts_settings.h"
#include "../../game/player/player_actions.h" #include "../../game/player/player_actions.h"
#include "../../game/player/player_logic.h" #include "../../game/player/player_logic.h"
#include "../player_graphics_item.h" #include "../player_graphics_item.h"

View file

@ -5,6 +5,7 @@
#include "../../game/player/player_logic.h" #include "../../game/player/player_logic.h"
#include "../player_graphics_item.h" #include "../player_graphics_item.h"
#include <libcockatrice/settings/message_settings.h>
SayMenu::SayMenu(PlayerGraphicsItem *_player) : player(_player) SayMenu::SayMenu(PlayerGraphicsItem *_player) : player(_player)
{ {
connect(&SettingsCache::instance().messages(), &MessageSettings::messageMacrosChanged, this, &SayMenu::initSayMenu); connect(&SettingsCache::instance().messages(), &MessageSettings::messageMacrosChanged, this, &SayMenu::initSayMenu);

View file

@ -1,5 +1,6 @@
#include "sideboard_menu.h" #include "sideboard_menu.h"
#include "../../../client/settings/shortcuts_settings.h"
#include "../../game/player/player_actions.h" #include "../../game/player/player_actions.h"
#include "../../game/player/player_logic.h" #include "../../game/player/player_logic.h"
#include "../player_graphics_item.h" #include "../player_graphics_item.h"

View file

@ -0,0 +1,57 @@
#include "tally_menu.h"
#include "../../../client/settings/cache_settings.h"
#include <QActionGroup>
TallyMenu::TallyMenu()
{
actionGroup = new QActionGroup(this);
actionGroup->setExclusive(true);
aTallyNone = createTallyAction(TallyType::None);
aTallySubtypes = createTallyAction(TallyType::Subtypes);
aTallyTotalPower = createTallyAction(TallyType::TotalPower);
addAction(aTallyNone);
addSeparator();
addAction(aTallySubtypes);
addAction(aTallyTotalPower);
retranslateUi();
}
QAction *TallyMenu::createTallyAction(TallyType tallyType)
{
TallyType currentType = Tally::intToType(SettingsCache::instance().userInterface().getTallyType());
QAction *action = new QAction(this);
action->setCheckable(true);
action->setChecked(tallyType == currentType);
connect(action, &QAction::triggered, &SettingsCache::instance().userInterface(),
[tallyType] { SettingsCache::instance().userInterface().setTallyType(static_cast<int>(tallyType)); });
actionGroup->addAction(action);
return action;
}
void TallyMenu::setShortcutsActive()
{
// no-op because we haven't decided if we're adding shortcuts for tally types
}
void TallyMenu::setShortcutsInactive()
{
// no-op because we haven't decided if we're adding shortcuts for tally types
}
void TallyMenu::retranslateUi()
{
setTitle(tr("Tally"));
aTallyNone->setText(tr("None"));
aTallySubtypes->setText(tr("Subtypes"));
aTallyTotalPower->setText(tr("Total Power"));
}

View file

@ -0,0 +1,31 @@
#ifndef COCKATRICE_TALLY_MENU_H
#define COCKATRICE_TALLY_MENU_H
#include "../../../interface/widgets/menus/tearoff_menu.h"
#include "../../tally/tally.h"
#include "abstract_player_component.h"
#include <QMenu>
class TallyMenu : public TearOffMenu, public AbstractPlayerComponent
{
Q_OBJECT
public:
TallyMenu();
void setShortcutsActive() override;
void setShortcutsInactive() override;
void retranslateUi() override;
private:
QActionGroup *actionGroup = nullptr;
QAction *aTallyNone = nullptr;
QAction *aTallySubtypes = nullptr;
QAction *aTallyTotalPower = nullptr;
QAction *createTallyAction(TallyType tallyType);
};
#endif // COCKATRICE_TALLY_MENU_H

View file

@ -1,5 +1,6 @@
#include "utility_menu.h" #include "utility_menu.h"
#include "../../../client/settings/shortcuts_settings.h"
#include "../../../interface/deck_loader/deck_loader.h" #include "../../../interface/deck_loader/deck_loader.h"
#include "../../game/player/player_actions.h" #include "../../game/player/player_actions.h"
#include "../../game/player/player_logic.h" #include "../../game/player/player_logic.h"

View file

@ -8,6 +8,7 @@
#include <QInputDialog> #include <QInputDialog>
#include <libcockatrice/card/relation/card_relation.h> #include <libcockatrice/card/relation/card_relation.h>
#include <libcockatrice/utility/string_limits.h>
PlayerDialogs::PlayerDialogs(PlayerGraphicsItem *_player, PlayerActions *_playerActions) PlayerDialogs::PlayerDialogs(PlayerGraphicsItem *_player, PlayerActions *_playerActions)
: QObject(_player), player(_player), playerActions(_playerActions) : QObject(_player), player(_player), playerActions(_playerActions)

View file

@ -13,12 +13,13 @@
#include "player_dialogs.h" #include "player_dialogs.h"
#include <QGraphicsView> #include <QGraphicsView>
#include <libcockatrice/settings/interface_settings.h>
PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player) PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player)
{ {
connect(&SettingsCache::instance(), &SettingsCache::horizontalHandChanged, this, connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::horizontalHandChanged, this,
&PlayerGraphicsItem::rearrangeZones); &PlayerGraphicsItem::rearrangeZones);
connect(&SettingsCache::instance(), &SettingsCache::handJustificationChanged, this, connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::handJustificationChanged, this,
&PlayerGraphicsItem::rearrangeZones); &PlayerGraphicsItem::rearrangeZones);
connect(player, &PlayerLogic::rearrangeCounters, this, &PlayerGraphicsItem::rearrangeCounters); connect(player, &PlayerLogic::rearrangeCounters, this, &PlayerGraphicsItem::rearrangeCounters);
connect(player, &PlayerLogic::activeChanged, this, &PlayerGraphicsItem::onPlayerActiveChanged); connect(player, &PlayerLogic::activeChanged, this, &PlayerGraphicsItem::onPlayerActiveChanged);
@ -59,6 +60,9 @@ PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player)
initializeZones(); initializeZones();
connect(player, &PlayerLogic::addViewCustomZoneActionToCustomZoneMenu, this,
&PlayerGraphicsItem::onCustomZoneAdded);
playerMenu->setMenusForGraphicItems(); playerMenu->setMenusForGraphicItems();
connect(tableZoneGraphicsItem, &TableZone::sizeChanged, this, &PlayerGraphicsItem::updateBoundingRect); connect(tableZoneGraphicsItem, &TableZone::sizeChanged, this, &PlayerGraphicsItem::updateBoundingRect);
@ -121,6 +125,19 @@ void PlayerGraphicsItem::initializeZones()
connect(handZoneGraphicsItem->getLogic(), &HandZoneLogic::cardCountChanged, handCounter, connect(handZoneGraphicsItem->getLogic(), &HandZoneLogic::cardCountChanged, handCounter,
&HandCounter::updateNumber); &HandCounter::updateNumber);
connect(handCounter, &HandCounter::showContextMenu, handZoneGraphicsItem, &HandZone::showContextMenu); connect(handCounter, &HandCounter::showContextMenu, handZoneGraphicsItem, &HandZone::showContextMenu);
zoneGraphicsItems.insert(player->getDeckZone()->getName(), deckZoneGraphicsItem);
zoneGraphicsItems.insert(player->getGraveZone()->getName(), graveyardZoneGraphicsItem);
zoneGraphicsItems.insert(player->getRfgZone()->getName(), rfgZoneGraphicsItem);
zoneGraphicsItems.insert(player->getSideboardZone()->getName(), sideboardGraphicsItem);
zoneGraphicsItems.insert(player->getTableZone()->getName(), tableZoneGraphicsItem);
zoneGraphicsItems.insert(player->getStackZone()->getName(), stackZoneGraphicsItem);
zoneGraphicsItems.insert(player->getHandZone()->getName(), handZoneGraphicsItem);
}
void PlayerGraphicsItem::onCustomZoneAdded(QString customZoneName)
{
zoneGraphicsItems.insert(customZoneName, nullptr); // Custom zone view goes here, if we ever implement it.
} }
QRectF PlayerGraphicsItem::boundingRect() const QRectF PlayerGraphicsItem::boundingRect() const
@ -132,7 +149,7 @@ qreal PlayerGraphicsItem::getMinimumWidth() const
{ {
qreal result = tableZoneGraphicsItem->getMinimumWidth() + CardDimensions::HEIGHT_F + 15 + counterAreaWidth + qreal result = tableZoneGraphicsItem->getMinimumWidth() + CardDimensions::HEIGHT_F + 15 + counterAreaWidth +
stackZoneGraphicsItem->boundingRect().width(); stackZoneGraphicsItem->boundingRect().width();
if (!SettingsCache::instance().getHorizontalHand()) { if (!SettingsCache::instance().userInterface().getHorizontalHand()) {
result += handZoneGraphicsItem->boundingRect().width(); result += handZoneGraphicsItem->boundingRect().width();
} }
return result; return result;
@ -149,7 +166,7 @@ void PlayerGraphicsItem::processSceneSizeChange(int newPlayerWidth)
// Extend table (and hand, if horizontal) to accommodate the new player width. // Extend table (and hand, if horizontal) to accommodate the new player width.
qreal tableWidth = newPlayerWidth - CardDimensions::HEIGHT_F - 15 - counterAreaWidth - qreal tableWidth = newPlayerWidth - CardDimensions::HEIGHT_F - 15 - counterAreaWidth -
stackZoneGraphicsItem->boundingRect().width(); stackZoneGraphicsItem->boundingRect().width();
if (!SettingsCache::instance().getHorizontalHand()) { if (!SettingsCache::instance().userInterface().getHorizontalHand()) {
tableWidth -= handZoneGraphicsItem->boundingRect().width(); tableWidth -= handZoneGraphicsItem->boundingRect().width();
} }
@ -171,6 +188,11 @@ void PlayerGraphicsItem::onCounterAdded(CounterState *state)
AbstractCounter *widget; AbstractCounter *widget;
if (state->getName() == "life") { if (state->getName() == "life") {
widget = playerTarget->addCounter(state); widget = playerTarget->addCounter(state);
connect(state, &CounterState::valueChanged, this, [this](int oldValue, int newValue) {
if (newValue < oldValue) {
tableZoneGraphicsItem->triggerDamageShimmer();
}
});
} else { } else {
widget = new GeneralCounter(state, player, true, this); widget = new GeneralCounter(state, player, true, this);
} }
@ -217,7 +239,7 @@ void PlayerGraphicsItem::rearrangeCounters()
void PlayerGraphicsItem::rearrangeZones() void PlayerGraphicsItem::rearrangeZones()
{ {
auto base = QPointF(CardDimensions::HEIGHT_F + counterAreaWidth + 15, 0); auto base = QPointF(CardDimensions::HEIGHT_F + counterAreaWidth + 15, 0);
if (SettingsCache::instance().getHorizontalHand()) { if (SettingsCache::instance().userInterface().getHorizontalHand()) {
if (mirrored) { if (mirrored) {
if (player->getHandZone()->contentsKnown()) { if (player->getHandZone()->contentsKnown()) {
handVisible = true; handVisible = true;
@ -268,7 +290,7 @@ void PlayerGraphicsItem::updateBoundingRect()
{ {
prepareGeometryChange(); prepareGeometryChange();
qreal width = CardDimensions::HEIGHT_F + 15 + counterAreaWidth + stackZoneGraphicsItem->boundingRect().width(); qreal width = CardDimensions::HEIGHT_F + 15 + counterAreaWidth + stackZoneGraphicsItem->boundingRect().width();
if (SettingsCache::instance().getHorizontalHand()) { if (SettingsCache::instance().userInterface().getHorizontalHand()) {
qreal handHeight = handVisible ? handZoneGraphicsItem->boundingRect().height() : 0; qreal handHeight = handVisible ? handZoneGraphicsItem->boundingRect().height() : 0;
bRect = QRectF(0, 0, width + tableZoneGraphicsItem->boundingRect().width(), bRect = QRectF(0, 0, width + tableZoneGraphicsItem->boundingRect().width(),
tableZoneGraphicsItem->boundingRect().height() + handHeight); tableZoneGraphicsItem->boundingRect().height() + handHeight);

View file

@ -77,6 +77,11 @@ public:
return playerTarget; return playerTarget;
} }
CardZone *getZoneGraphicsItem(const QString &name) const
{
return zoneGraphicsItems.value(name, nullptr);
}
[[nodiscard]] PileZone *getDeckZoneGraphicsItem() const [[nodiscard]] PileZone *getDeckZoneGraphicsItem() const
{ {
return deckZoneGraphicsItem; return deckZoneGraphicsItem;
@ -110,6 +115,7 @@ public:
public slots: public slots:
void onPlayerActiveChanged(bool _active); void onPlayerActiveChanged(bool _active);
void onCustomZoneAdded(QString customZoneName);
void onCounterAdded(CounterState *state); void onCounterAdded(CounterState *state);
void onCounterRemoved(int counterId); void onCounterRemoved(int counterId);
void rearrangeCounters(); void rearrangeCounters();
@ -128,6 +134,7 @@ private:
PlayerArea *playerArea; PlayerArea *playerArea;
PlayerTarget *playerTarget; PlayerTarget *playerTarget;
QMap<int, AbstractCounter *> counterWidgets; QMap<int, AbstractCounter *> counterWidgets;
QMap<QString, CardZone *> zoneGraphicsItems;
PileZone *deckZoneGraphicsItem; PileZone *deckZoneGraphicsItem;
PileZone *sideboardGraphicsItem; PileZone *sideboardGraphicsItem;
PileZone *graveyardZoneGraphicsItem; PileZone *graveyardZoneGraphicsItem;

View file

@ -25,11 +25,7 @@ bool PlayerListItemDelegate::editorEvent(QEvent *event,
if ((event->type() == QEvent::MouseButtonPress) && index.isValid()) { if ((event->type() == QEvent::MouseButtonPress) && index.isValid()) {
auto *const mouseEvent = static_cast<QMouseEvent *>(event); auto *const mouseEvent = static_cast<QMouseEvent *>(event);
if (mouseEvent->button() == Qt::RightButton) { if (mouseEvent->button() == Qt::RightButton) {
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
static_cast<PlayerListWidget *>(parent())->showContextMenu(mouseEvent->globalPosition().toPoint(), index); static_cast<PlayerListWidget *>(parent())->showContextMenu(mouseEvent->globalPosition().toPoint(), index);
#else
static_cast<PlayerListWidget *>(parent())->showContextMenu(mouseEvent->globalPos(), index);
#endif
return true; return true;
} }
} }

View file

@ -1,8 +1,11 @@
#include "player_target.h" #include "player_target.h"
#include "../../client/settings/cache_settings.h"
#include "../../game/player/player_logic.h" #include "../../game/player/player_logic.h"
#include "../../interface/pixel_map_generator.h" #include "../../interface/pixel_map_generator.h"
#include "../game_scene.h"
#include <QApplication>
#include <QDebug> #include <QDebug>
#include <QPainter> #include <QPainter>
#include <QPixmapCache> #include <QPixmapCache>
@ -21,17 +24,24 @@ QRectF PlayerCounter::boundingRect() const
void PlayerCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) void PlayerCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
{ {
const int radius = 8; const int radius = 15;
const qreal border = 1; const qreal border = 1.5;
QPainterPath path(QPointF(50 - border / 2, border / 2)); // The box is drawn with a border-wide stroke straddling the path, so the
path.lineTo(radius, border / 2); // visible outline spans [inset, inset + border]. Fills that must not cover
path.arcTo(border / 2, border / 2, 2 * radius, 2 * radius, 90, 90); // the outline (e.g. the life-change flash) use a path inset by `border`.
path.lineTo(border / 2, 30 - border / 2); const auto makePath = [](qreal inset) {
path.lineTo(50 - border / 2, 30 - border / 2); QPainterPath path(QPointF(50 - inset, inset));
path.closeSubpath(); path.lineTo(radius, inset);
path.arcTo(inset, inset, 2 * radius, 2 * radius, 90, 90);
path.lineTo(inset, 30 - inset);
path.lineTo(50 - inset, 30 - inset);
path.closeSubpath();
return path;
};
QPainterPath path = makePath(border / 2);
QPen pen(QColor(100, 100, 100)); QPen pen(QColor(100, 100, 100));
pen.setWidth(border); pen.setWidthF(border);
painter->setPen(pen); painter->setPen(pen);
painter->setBrush(hovered ? QColor(50, 50, 50, 160) : QColor(0, 0, 0, 160)); painter->setBrush(hovered ? QColor(50, 50, 50, 160) : QColor(0, 0, 0, 160));
@ -45,6 +55,48 @@ void PlayerCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*
painter->setFont(font); painter->setFont(font);
painter->setPen(Qt::white); painter->setPen(Qt::white);
painter->drawText(translatedRect, Qt::AlignCenter, QString::number(value)); painter->drawText(translatedRect, Qt::AlignCenter, QString::number(value));
// Life-change flash: emerald on gain, red on loss, decaying over a few ticks.
if (flashAlpha > 0) {
painter->save();
QColor flashColor = flashDelta > 0 ? QColor(52, 224, 122) : QColor(239, 68, 68);
flashColor.setAlphaF(0.45 * flashAlpha);
painter->setPen(Qt::NoPen);
painter->setBrush(flashColor);
painter->setOpacity(0.85);
painter->drawPath(makePath(border));
painter->restore();
}
}
void PlayerCounter::onValueChanged(int oldValue, int newValue)
{
flashDelta = newValue - oldValue;
if (flashDelta == 0) {
return;
}
if (!SettingsCache::instance().userInterface().getLifeCounterAnimationsEnabled()) {
flashAlpha = 0.0;
return;
}
flashAlpha = 1.0;
flashClock.start();
if (scene()) {
static_cast<GameScene *>(scene())->registerAnimationItem(this);
}
}
bool PlayerCounter::animationEvent()
{
flashAlpha = 1.0 - flashClock.elapsed() / flashDurationMs;
if (flashAlpha <= 0.0) {
flashAlpha = 0.0;
return false;
}
update();
return true;
} }
PlayerTarget::PlayerTarget(PlayerLogic *_owner, QGraphicsItem *parentItem) PlayerTarget::PlayerTarget(PlayerLogic *_owner, QGraphicsItem *parentItem)

View file

@ -7,21 +7,34 @@
#ifndef PLAYERTARGET_H #ifndef PLAYERTARGET_H
#define PLAYERTARGET_H #define PLAYERTARGET_H
#include "../animated_item.h"
#include "../board/abstract_counter.h" #include "../board/abstract_counter.h"
#include "../board/arrow_target.h" #include "../board/arrow_target.h"
#include "../board/graphics_item_type.h" #include "../board/graphics_item_type.h"
#include <QElapsedTimer>
#include <QPixmap> #include <QPixmap>
class PlayerLogic; class PlayerLogic;
class PlayerCounter : public AbstractCounter class PlayerCounter : public AbstractCounter, public IAnimatedItem
{ {
Q_OBJECT Q_OBJECT
protected:
void onValueChanged(int oldValue, int newValue) override;
private:
static constexpr qreal flashDurationMs = 450.0;
QElapsedTimer flashClock;
qreal flashAlpha = 0.0;
int flashDelta = 0;
public: public:
PlayerCounter(CounterState *state, PlayerLogic *player, QGraphicsItem *parent); PlayerCounter(CounterState *state, PlayerLogic *player, QGraphicsItem *parent);
QRectF boundingRect() const override; QRectF boundingRect() const override;
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
bool animationEvent() override;
}; };
class PlayerTarget : public ArrowTarget class PlayerTarget : public ArrowTarget

View file

@ -0,0 +1,36 @@
#include "stats_tally.h"
#include "../board/card_item.h"
#include <QCoreApplication>
#include <QList>
#include <algorithm>
static int sumPowers(const QList<CardItem *> &cards)
{
// calculate total power;
int total = 0;
for (auto card : cards) {
QVariantList parsed = CardItem::parsePT(card->getPT());
if (!parsed.isEmpty()) {
int power = parsed.first().toInt(); // toInt will default to 0 if it's not an int
total += qMax(power, 0);
}
}
return total;
}
QList<TallyRow> StatsTally::computeTotalPower(const QList<CardItem *> &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 = sumPowers(cards);
QString name = QCoreApplication::translate("StatsTally", "Total Power");
return {TallyRow{name, QString::number(total)}};
}

View file

@ -0,0 +1,21 @@
#ifndef COCKATRICE_STATS_TALLY_H
#define COCKATRICE_STATS_TALLY_H
#include "tally.h"
/**
* @brief Extracts and tallies stats from selected cards.
*/
namespace StatsTally
{
/**
* @brief Sums the power 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<TallyRow> computeTotalPower(const QList<CardItem *> &cards);
} // namespace StatsTally
#endif // COCKATRICE_STATS_TALLY_H

View file

@ -0,0 +1,77 @@
#include "subtype_tally.h"
#include "../board/card_item.h"
#include <QMap>
#include <algorithm>
namespace
{
/** @brief Extracts subtypes from a single card face's type line. */
QStringList extractSubtypesFromFace(const QString &faceType)
{
// Card type format: "Creature — Goblin Warrior" or "Legendary Enchantment — Saga"
QStringList parts = faceType.split(QStringLiteral(""));
if (parts.size() > 1) {
return parts[1].split(QStringLiteral(" "), Qt::SkipEmptyParts);
}
return {};
}
/** @brief A single subtype (e.g., "Goblin", "Warrior") with its occurrence count. */
struct SubtypeEntry
{
QString name;
int count;
};
} // anonymous namespace
namespace SubtypeTally
{
QList<TallyRow> countSubtypes(const QList<CardItem *> &cards)
{
QMap<QString, int> subtypeCounts;
for (CardItem *card : cards) {
if (card->getFaceDown() || card->getCard().isEmpty()) {
continue;
}
QString cardType = card->getCardInfo().getCardType();
// Handle double-faced cards: "Creature — Human // Creature — Werewolf"
QStringList cardFaces = cardType.split(QStringLiteral(" // "));
for (const QString &face : cardFaces) {
QStringList subtypes = extractSubtypesFromFace(face);
for (const QString &subtype : subtypes) {
subtypeCounts[subtype]++;
}
}
}
QList<SubtypeEntry> entries;
for (auto it = subtypeCounts.constBegin(); it != subtypeCounts.constEnd(); ++it) {
entries.append({it.key(), it.value()});
}
// Sort by count ascending, then alphabetically (lowest counts at bottom of display)
std::sort(entries.begin(), entries.end(), [](const SubtypeEntry &a, const SubtypeEntry &b) {
if (a.count != b.count) {
return a.count < b.count;
}
return a.name < b.name;
});
// convert entries into TallyRows
QList<TallyRow> rows;
rows.reserve(entries.size());
std::transform(entries.begin(), entries.end(), std::back_inserter(rows),
[](const SubtypeEntry &e) { return TallyRow{e.name, QString::number(e.count)}; });
return rows;
}
} // namespace SubtypeTally

View file

@ -0,0 +1,26 @@
#ifndef COCKATRICE_SUBTYPE_TALLY_H
#define COCKATRICE_SUBTYPE_TALLY_H
#include "tally.h"
#include <QList>
#include <QString>
class CardItem;
/**
* @brief Extracts and tallies subtypes from selected cards.
*/
namespace SubtypeTally
{
/**
* @brief Parses card type lines and counts each subtype occurrence.
*
* Skips face-down cards and cards without type info.
* @param cards The list of selected card items to analyze.
* @return Entries sorted by count ascending, then alphabetically.
*/
QList<TallyRow> countSubtypes(const QList<CardItem *> &cards);
} // namespace SubtypeTally
#endif

View file

@ -0,0 +1,26 @@
#include "tally.h"
#include "stats_tally.h"
#include "subtype_tally.h"
TallyType Tally::intToType(int value)
{
if (value < static_cast<int>(TallyType::None) || value > static_cast<int>(TallyType::MaxValue)) {
return TallyType::None;
}
return static_cast<TallyType>(value);
}
QList<TallyRow> Tally::compute(const QList<CardItem *> &cards, const TallyType type)
{
switch (type) {
case TallyType::None:
return {};
case TallyType::Subtypes:
return SubtypeTally::countSubtypes(cards);
case TallyType::TotalPower:
return StatsTally::computeTotalPower(cards);
}
return {};
}

View file

@ -0,0 +1,50 @@
#ifndef COCKATRICE_TALLY_H
#define COCKATRICE_TALLY_H
#include <QString>
class CardItem;
/** @brief A single row of the tally output. */
struct TallyRow
{
QString name; ///< The row name (displayed on the left)
QString value; ///< Value for the row (displayed on the right)
bool operator==(const TallyRow &) const = default;
};
/**
* The tally type
*/
enum class TallyType
{
None,
Subtypes,
TotalPower,
MaxValue = TotalPower // sentinel value
};
namespace Tally
{
/**
* Safely converts an int into the corresponding TallyType.
*
* @param value The int value
* @return The TallyType. Returns TallyType::None if the value is not within range
*/
TallyType intToType(int value);
/**
* @brief Analyzes the selected cards according to the tally type and builds the resulting tally rows.
* This forwards the cards to the code for that tally type.
*
* @param cards The list of selected card items to analyze.
* @param type The type of tally to do
* @return Rows sorted in top-to-bottom display order
*/
QList<TallyRow> compute(const QList<CardItem *> &cards, TallyType type);
} // namespace Tally
#endif // COCKATRICE_TALLY_H

View file

@ -9,6 +9,7 @@
#include <QPainter> #include <QPainter>
#include <libcockatrice/protocol/pb/command_move_card.pb.h> #include <libcockatrice/protocol/pb/command_move_card.pb.h>
#include <libcockatrice/settings/interface_settings.h>
HandZone::HandZone(HandZoneLogic *_logic, int _zoneHeight, QGraphicsItem *parent) HandZone::HandZone(HandZoneLogic *_logic, int _zoneHeight, QGraphicsItem *parent)
: SelectZone(_logic, parent), zoneHeight(_zoneHeight) : SelectZone(_logic, parent), zoneHeight(_zoneHeight)
@ -33,7 +34,7 @@ void HandZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
QPoint point = dropPoint + scenePos().toPoint(); QPoint point = dropPoint + scenePos().toPoint();
int x = -1; int x = -1;
if (SettingsCache::instance().getHorizontalHand()) { if (SettingsCache::instance().userInterface().getHorizontalHand()) {
for (x = 0; x < getLogic()->getCards().size(); x++) { for (x = 0; x < getLogic()->getCards().size(); x++) {
if (point.x() < static_cast<CardItem *>(getLogic()->getCards().at(x))->scenePos().x()) { if (point.x() < static_cast<CardItem *>(getLogic()->getCards().at(x))->scenePos().x()) {
break; break;
@ -60,7 +61,7 @@ void HandZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
QRectF HandZone::boundingRect() const QRectF HandZone::boundingRect() const
{ {
if (SettingsCache::instance().getHorizontalHand()) { if (SettingsCache::instance().userInterface().getHorizontalHand()) {
return QRectF(0, 0, width, CardDimensions::HEIGHT_F + 10); return QRectF(0, 0, width, CardDimensions::HEIGHT_F + 10);
} else { } else {
return QRectF(0, 0, CardDimensions::WIDTH_F * 1.5, zoneHeight); return QRectF(0, 0, CardDimensions::WIDTH_F * 1.5, zoneHeight);
@ -77,8 +78,8 @@ void HandZone::reorganizeCards()
{ {
if (!getLogic()->getCards().isEmpty()) { if (!getLogic()->getCards().isEmpty()) {
const int cardCount = getLogic()->getCards().size(); const int cardCount = getLogic()->getCards().size();
if (SettingsCache::instance().getHorizontalHand()) { if (SettingsCache::instance().userInterface().getHorizontalHand()) {
bool leftJustified = SettingsCache::instance().getLeftJustified(); bool leftJustified = SettingsCache::instance().userInterface().getLeftJustified();
qreal cardWidth = getLogic()->getCards().at(0)->boundingRect().width(); qreal cardWidth = getLogic()->getCards().at(0)->boundingRect().width();
const int xPadding = leftJustified ? cardWidth * 1.4 : 5; const int xPadding = leftJustified ? cardWidth * 1.4 : 5;
qreal totalWidth = qreal totalWidth =
@ -126,7 +127,7 @@ void HandZone::sortHand(const QList<CardList::SortOption> &options)
void HandZone::setWidth(qreal _width) void HandZone::setWidth(qreal _width)
{ {
if (SettingsCache::instance().getHorizontalHand()) { if (SettingsCache::instance().userInterface().getHorizontalHand()) {
prepareGeometryChange(); prepareGeometryChange();
width = _width; width = _width;
reorganizeCards(); reorganizeCards();

View file

@ -1,5 +1,6 @@
#include "pile_zone.h" #include "pile_zone.h"
#include "../../client/settings/cache_settings.h"
#include "../../game/player/player_actions.h" #include "../../game/player/player_actions.h"
#include "../../game/player/player_logic.h" #include "../../game/player/player_logic.h"
#include "../../game/zones/pile_zone_logic.h" #include "../../game/zones/pile_zone_logic.h"
@ -11,6 +12,7 @@
#include <QGraphicsSceneMouseEvent> #include <QGraphicsSceneMouseEvent>
#include <QPainter> #include <QPainter>
#include <libcockatrice/protocol/pb/command_move_card.pb.h> #include <libcockatrice/protocol/pb/command_move_card.pb.h>
#include <libcockatrice/settings/cards_display_settings.h>
PileZone::PileZone(PileZoneLogic *_logic, QGraphicsItem *parent) : CardZone(_logic, parent) PileZone::PileZone(PileZoneLogic *_logic, QGraphicsItem *parent) : CardZone(_logic, parent)
{ {
@ -23,12 +25,13 @@ PileZone::PileZone(PileZoneLogic *_logic, QGraphicsItem *parent) : CardZone(_log
.rotate(90) .rotate(90)
.translate(-CardDimensions::WIDTH_HALF_F, -CardDimensions::HEIGHT_HALF_F)); .translate(-CardDimensions::WIDTH_HALF_F, -CardDimensions::HEIGHT_HALF_F));
connect(&SettingsCache::instance(), &SettingsCache::roundCardCornersChanged, this, [this](bool _roundCardCorners) { connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::roundCardCornersChanged, this,
Q_UNUSED(_roundCardCorners); [this](bool _roundCardCorners) {
Q_UNUSED(_roundCardCorners);
prepareGeometryChange(); prepareGeometryChange();
update(); update();
}); });
} }
QRectF PileZone::boundingRect() const QRectF PileZone::boundingRect() const
@ -39,7 +42,8 @@ QRectF PileZone::boundingRect() const
QPainterPath PileZone::shape() const QPainterPath PileZone::shape() const
{ {
QPainterPath shape; QPainterPath shape;
qreal cardCornerRadius = SettingsCache::instance().getRoundCardCorners() ? 0.05 * CardDimensions::WIDTH_F : 0.0; qreal cardCornerRadius =
SettingsCache::instance().cardsDisplay().getRoundCardCorners() ? 0.05 * CardDimensions::WIDTH_F : 0.0;
shape.addRoundedRect(boundingRect(), cardCornerRadius, cardCornerRadius); shape.addRoundedRect(boundingRect(), cardCornerRadius, cardCornerRadius);
return shape; return shape;
} }

View file

@ -7,10 +7,11 @@
#include <QGraphicsRectItem> #include <QGraphicsRectItem>
#include <QGraphicsSceneMouseEvent> #include <QGraphicsSceneMouseEvent>
#include <QtMath> #include <QtMath>
#include <libcockatrice/settings/cards_display_settings.h>
static qreal stackingOffset(qreal cardHeight) static qreal stackingOffset(qreal cardHeight)
{ {
const qreal overlapPercent = SettingsCache::instance().getStackCardOverlapPercent(); const qreal overlapPercent = SettingsCache::instance().cardsDisplay().getStackCardOverlapPercent();
return cardHeight * (100.0 - overlapPercent) / 100.0; return cardHeight * (100.0 - overlapPercent) / 100.0;
} }
@ -21,40 +22,32 @@ SelectZone::ZoneLayout SelectZone::computeZoneLayout(const StackLayoutParams &pa
} }
qreal effectiveOffset = params.desiredOffset; qreal effectiveOffset = params.desiredOffset;
if (params.cardCount > 1) { if (params.cardCount > 1) {
qreal fitOffset; qreal reservedForBottomCard;
if (params.totalHeight < params.cardHeight && params.minOffset > 0.0) { if (params.allowBottomOverflow) {
// Zone is shorter than a card (e.g. minimized). Compress offsets so // Allow the bottom card to partially overflow in tight zones, scaling the
// every card has at least minOffset pixels of its top visible. // overflow allowance by sqrt(cardCount-1) so offsets decrease smoothly
fitOffset = (params.totalHeight - params.minOffset) / (params.cardCount - 1); // as cards are added rather than dropping by 1/(n-1) each time.
effectiveOffset = qMax(0.0, qMin(params.desiredOffset, fitOffset)); // The 0.75 ratio was tuned experimentally to balance card visibility vs. overflow.
constexpr qreal bottomCardZoneRatio = 0.75;
const qreal adjustedRatio = bottomCardZoneRatio / qSqrt(static_cast<qreal>(params.cardCount - 1));
reservedForBottomCard = qMin(params.cardHeight, params.totalHeight * adjustedRatio);
} else { } else {
qreal reservedForBottomCard; // No overflow: reserve full card height for the bottom card
if (params.allowBottomOverflow) { reservedForBottomCard = params.cardHeight;
// Allow the bottom card to partially overflow in tight zones, scaling the }
// overflow allowance by sqrt(cardCount-1) so offsets decrease smoothly qreal fitOffset = (params.totalHeight - reservedForBottomCard) / (params.cardCount - 1);
// as cards are added rather than dropping by 1/(n-1) each time.
// The 0.75 ratio was tuned experimentally to balance card visibility vs. overflow.
constexpr qreal bottomCardZoneRatio = 0.75;
const qreal adjustedRatio = bottomCardZoneRatio / qSqrt(static_cast<qreal>(params.cardCount - 1));
reservedForBottomCard = qMin(params.cardHeight, params.totalHeight * adjustedRatio);
} else {
// No overflow: reserve full card height for the bottom card
reservedForBottomCard = params.cardHeight;
}
fitOffset = (params.totalHeight - reservedForBottomCard) / (params.cardCount - 1);
if (!params.allowBottomOverflow) { if (!params.allowBottomOverflow) {
// Constrain offset so all card tops remain within zone bounds. // Constrain offset so all card tops remain within zone bounds.
// With start=0, last card top at (cardCount-1) * effectiveOffset must be < totalHeight. // With start=0, last card top at (cardCount-1) * effectiveOffset must be < totalHeight.
qreal maxOffsetForTops = params.totalHeight / (params.cardCount - 1); qreal maxOffsetForTops = params.totalHeight / (params.cardCount - 1);
fitOffset = qMin(fitOffset, maxOffsetForTops); fitOffset = qMin(fitOffset, maxOffsetForTops);
} }
// Apply minOffset only if it fits; otherwise compress further to keep all card tops visible. // Apply minOffset only if it fits; otherwise compress further to keep all card tops visible.
effectiveOffset = qMin(params.desiredOffset, fitOffset); effectiveOffset = qMin(params.desiredOffset, fitOffset);
if (fitOffset >= params.minOffset) { if (fitOffset >= params.minOffset) {
effectiveOffset = qMax(params.minOffset, effectiveOffset); effectiveOffset = qMax(params.minOffset, effectiveOffset);
}
} }
} }
qreal stackHeight = (params.cardCount - 1) * effectiveOffset + params.cardHeight; qreal stackHeight = (params.cardCount - 1) * effectiveOffset + params.cardHeight;

View file

@ -43,12 +43,18 @@ void StackZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
return; return;
} }
int index = calcDropIndexFromY(dropPoint.y(), MIN_CARD_VISIBLE);
// Same-zone no-op: don't move a card onto itself
const auto &cards = getLogic()->getCards(); const auto &cards = getLogic()->getCards();
if (!cards.isEmpty() && startZone == getLogic() && cards.at(index)->getId() == dragItems.at(0)->getId()) { int index;
return; 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
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<int>(cards.size());
} }
Command_MoveCard cmd; Command_MoveCard cmd;

View file

@ -8,6 +8,7 @@
#include "../board/arrow_item.h" #include "../board/arrow_item.h"
#include "../board/card_drag_item.h" #include "../board/card_drag_item.h"
#include "../board/card_item.h" #include "../board/card_item.h"
#include "../game_scene.h"
#include "../z_values.h" #include "../z_values.h"
#include <QGraphicsScene> #include <QGraphicsScene>
@ -15,6 +16,7 @@
#include <libcockatrice/card/card_info.h> #include <libcockatrice/card/card_info.h>
#include <libcockatrice/protocol/pb/command_move_card.pb.h> #include <libcockatrice/protocol/pb/command_move_card.pb.h>
#include <libcockatrice/protocol/pb/command_set_card_attr.pb.h> #include <libcockatrice/protocol/pb/command_set_card_attr.pb.h>
#include <libcockatrice/settings/interface_settings.h>
#include <libcockatrice/utility/zone_names.h> #include <libcockatrice/utility/zone_names.h>
const QColor TableZone::BACKGROUND_COLOR = QColor(100, 100, 100); const QColor TableZone::BACKGROUND_COLOR = QColor(100, 100, 100);
@ -28,7 +30,7 @@ TableZone::TableZone(TableZoneLogic *_logic, bool _mirrored, QGraphicsItem *pare
connect(_logic, &TableZoneLogic::contentSizeChanged, this, &TableZone::resizeToContents); connect(_logic, &TableZoneLogic::contentSizeChanged, this, &TableZone::resizeToContents);
connect(_logic, &TableZoneLogic::toggleTapped, this, &TableZone::toggleTapped); connect(_logic, &TableZoneLogic::toggleTapped, this, &TableZone::toggleTapped);
connect(themeManager, &ThemeManager::themeChanged, this, &TableZone::updateBg); connect(themeManager, &ThemeManager::themeChanged, this, &TableZone::updateBg);
connect(&SettingsCache::instance(), &SettingsCache::invertVerticalCoordinateChanged, this, connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::invertVerticalCoordinateChanged, this,
&TableZone::reorganizeCards); &TableZone::reorganizeCards);
updateBg(); updateBg();
@ -46,6 +48,31 @@ void TableZone::updateBg()
update(); update();
} }
void TableZone::triggerDamageShimmer()
{
if (!SettingsCache::instance().userInterface().getBattlefieldFlashEnabled()) {
damageShimmerAlpha = 0.0;
return;
}
damageShimmerAlpha = 1.0;
shimmerClock.start();
if (scene()) {
static_cast<GameScene *>(scene())->registerAnimationItem(this);
}
}
bool TableZone::animationEvent()
{
damageShimmerAlpha = 1.0 - shimmerClock.elapsed() / shimmerDurationMs;
if (damageShimmerAlpha <= 0.0) {
damageShimmerAlpha = 0.0;
return false;
}
update();
return true;
}
QRectF TableZone::boundingRect() const QRectF TableZone::boundingRect() const
{ {
return QRectF(0, 0, width, height); return QRectF(0, 0, width, height);
@ -59,8 +86,8 @@ void TableZone::setMirrored(bool isMirrored)
bool TableZone::isInverted() const bool TableZone::isInverted() const
{ {
return ((mirrored && !SettingsCache::instance().getInvertVerticalCoordinate()) || return ((mirrored && !SettingsCache::instance().userInterface().getInvertVerticalCoordinate()) ||
(!mirrored && SettingsCache::instance().getInvertVerticalCoordinate())); (!mirrored && SettingsCache::instance().userInterface().getInvertVerticalCoordinate()));
} }
void TableZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) void TableZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
@ -76,6 +103,13 @@ void TableZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*opti
painter->fillRect(boundingRect(), FADE_MASK); painter->fillRect(boundingRect(), FADE_MASK);
} }
// Decaying crimson wash from taking damage.
if (damageShimmerAlpha > 0.0) {
QColor shimmerColor(239, 68, 68);
shimmerColor.setAlphaF(0.22 * damageShimmerAlpha);
painter->fillRect(boundingRect(), shimmerColor);
}
paintLandDivider(painter); paintLandDivider(painter);
} }

View file

@ -8,16 +8,19 @@
#define TABLEZONE_H #define TABLEZONE_H
#include "../../game/zones/table_zone_logic.h" #include "../../game/zones/table_zone_logic.h"
#include "../animated_item.h"
#include "../board/abstract_card_item.h" #include "../board/abstract_card_item.h"
#include "select_zone.h" #include "select_zone.h"
#include <QElapsedTimer>
/** /**
* @brief TableZone is the grid based rect where CardItems may be placed. * @brief TableZone is the grid based rect where CardItems may be placed.
* *
* It is the main play zone and can be customized with background images. * It is the main play zone and can be customized with background images.
*/ */
//! \todo Refactor methods to make more readable, extract logic to private methods (especially reorganizeCards()). //! \todo Refactor methods to make more readable, extract logic to private methods (especially reorganizeCards()).
class TableZone : public SelectZone class TableZone : public SelectZone, public IAnimatedItem
{ {
Q_OBJECT Q_OBJECT
@ -121,6 +124,16 @@ public:
*/ */
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
/**
Flashes the table surface after a player loses life.
Wired up through the life counter so the battlefield glows when life drops.
*/
void triggerDamageShimmer();
/** @brief Decays the damage shimmer by one timer tick. */
bool animationEvent() override;
/** /**
Toggles the selected items as tapped. Toggles the selected items as tapped.
*/ */
@ -185,6 +198,11 @@ public:
} }
private: private:
static constexpr qreal shimmerDurationMs = 450.0;
QElapsedTimer shimmerClock;
qreal damageShimmerAlpha = 0.0;
void paintZoneOutline(QPainter *painter); void paintZoneOutline(QPainter *painter);
void paintLandDivider(QPainter *painter); void paintLandDivider(QPainter *painter);

View file

@ -21,6 +21,7 @@
#include <QStyle> #include <QStyle>
#include <QStyleOption> #include <QStyleOption>
#include <libcockatrice/protocol/pb/command_shuffle.pb.h> #include <libcockatrice/protocol/pb/command_shuffle.pb.h>
#include <libcockatrice/settings/interface_settings.h>
namespace namespace
{ {
@ -65,7 +66,7 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player,
connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(&searchEdit); }); connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(&searchEdit); });
if (SettingsCache::instance().getFocusCardViewSearchBar()) { if (SettingsCache::instance().userInterface().getFocusCardViewSearchBar()) {
this->setActive(true); this->setActive(true);
searchEdit.setFocus(); searchEdit.setFocus();
} }
@ -75,6 +76,11 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player,
searchEditProxy->setZValue(ZValues::DRAG_ITEM); searchEditProxy->setZValue(ZValues::DRAG_ITEM);
vbox->addItem(searchEditProxy); vbox->addItem(searchEditProxy);
// hide search bar if chat autofocus setting is enabled, since typing into it will no longer work anyway
searchEditProxy->setVisible(!SettingsCache::instance().userInterface().getKeepGameChatFocus());
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::keepGameChatFocusChanged,
searchEditProxy, [searchEditProxy](bool keepFocus) { searchEditProxy->setVisible(!keepFocus); });
// top row // top row
QGraphicsLinearLayout *hTopRow = new QGraphicsLinearLayout(Qt::Horizontal); QGraphicsLinearLayout *hTopRow = new QGraphicsLinearLayout(Qt::Horizontal);
@ -153,9 +159,9 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player,
connect(&sortBySelector, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, connect(&sortBySelector, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this,
&ZoneViewWidget::processSortBy); &ZoneViewWidget::processSortBy);
connect(&pileViewCheckBox, &QCheckBox::QT_STATE_CHANGED, this, &ZoneViewWidget::processSetPileView); connect(&pileViewCheckBox, &QCheckBox::QT_STATE_CHANGED, this, &ZoneViewWidget::processSetPileView);
groupBySelector.setCurrentIndex(SettingsCache::instance().getZoneViewGroupByIndex()); groupBySelector.setCurrentIndex(SettingsCache::instance().userInterface().getZoneViewGroupByIndex());
sortBySelector.setCurrentIndex(SettingsCache::instance().getZoneViewSortByIndex()); sortBySelector.setCurrentIndex(SettingsCache::instance().userInterface().getZoneViewSortByIndex());
pileViewCheckBox.setChecked(SettingsCache::instance().getZoneViewPileView()); pileViewCheckBox.setChecked(SettingsCache::instance().userInterface().getZoneViewPileView());
if (CardList::NoSort == static_cast<CardList::SortOption>(groupBySelector.currentData().toInt())) { if (CardList::NoSort == static_cast<CardList::SortOption>(groupBySelector.currentData().toInt())) {
pileViewCheckBox.setEnabled(false); pileViewCheckBox.setEnabled(false);
@ -185,7 +191,7 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player,
void ZoneViewWidget::processGroupBy(int index) void ZoneViewWidget::processGroupBy(int index)
{ {
auto option = static_cast<CardList::SortOption>(groupBySelector.itemData(index).toInt()); auto option = static_cast<CardList::SortOption>(groupBySelector.itemData(index).toInt());
SettingsCache::instance().setZoneViewGroupByIndex(index); SettingsCache::instance().userInterface().setZoneViewGroupByIndex(index);
zone->setGroupBy(option); zone->setGroupBy(option);
// disable pile view checkbox if we're not grouping by anything // disable pile view checkbox if we're not grouping by anything
@ -209,13 +215,13 @@ void ZoneViewWidget::processSortBy(int index)
return; return;
} }
SettingsCache::instance().setZoneViewSortByIndex(index); SettingsCache::instance().userInterface().setZoneViewSortByIndex(index);
zone->setSortBy(option); zone->setSortBy(option);
} }
void ZoneViewWidget::processSetPileView(QT_STATE_CHANGED_T value) void ZoneViewWidget::processSetPileView(QT_STATE_CHANGED_T value)
{ {
SettingsCache::instance().setZoneViewPileView(value); SettingsCache::instance().userInterface().setZoneViewPileView(value);
zone->setPileView(value); zone->setPileView(value);
} }
@ -472,7 +478,7 @@ static qreal rowsToHeight(int rows)
**/ **/
static qreal calcMaxInitialHeight() static qreal calcMaxInitialHeight()
{ {
return rowsToHeight(SettingsCache::instance().getCardViewInitialRowsMax()); return rowsToHeight(SettingsCache::instance().userInterface().getCardViewInitialRowsMax());
} }
/** /**
@ -554,7 +560,7 @@ void ZoneViewWidget::initStyleOption(QStyleOption *option) const
void ZoneViewWidget::expandWindow() void ZoneViewWidget::expandWindow()
{ {
qreal maxInitialHeight = calcMaxInitialHeight(); qreal maxInitialHeight = calcMaxInitialHeight();
qreal maxExpandedHeight = rowsToHeight(SettingsCache::instance().getCardViewExpandedRowsMax()); qreal maxExpandedHeight = rowsToHeight(SettingsCache::instance().userInterface().getCardViewExpandedRowsMax());
qreal height = rect().height() - extraHeight - 10; qreal height = rect().height() - extraHeight - 10;
qreal maxHeight = maximumHeight() - extraHeight - 10; qreal maxHeight = maximumHeight() - extraHeight - 10;

View file

@ -1,6 +1,8 @@
#include "card_picture_loader.h" #include "card_picture_loader.h"
#include "../../client/settings/cache_settings.h" #include "../../client/settings/cache_settings.h"
#include "card_picture_loader_cache_method.h"
#include "card_picture_loader_local_schemes.h"
#include <QApplication> #include <QApplication>
#include <QBuffer> #include <QBuffer>
@ -16,16 +18,23 @@
#include <QStatusBar> #include <QStatusBar>
#include <QThread> #include <QThread>
#include <algorithm> #include <algorithm>
#include <libcockatrice/settings/cache_storage_settings.h>
#include <libcockatrice/settings/download_settings.h>
#include <libcockatrice/settings/paths_settings.h>
#include <utility> #include <utility>
// never cache more than 300 cards at once for a single deck // never cache more than 300 cards at once for a single deck
#define CACHED_CARD_PER_DECK_MAX 300 #define CACHED_CARD_PER_DECK_MAX 300
// wait at least this long before retrying a card whose picture failed to load
static constexpr int RETRY_FAILED_CARDS_SECS = 300;
CardPictureLoader::CardPictureLoader() : QObject(nullptr) CardPictureLoader::CardPictureLoader() : QObject(nullptr)
{ {
worker = new CardPictureLoaderWorker; worker = new CardPictureLoaderWorker;
connect(&SettingsCache::instance(), &SettingsCache::picsPathChanged, this, &CardPictureLoader::picsPathChanged); connect(&SettingsCache::instance().paths(), &PathsSettings::picsPathChanged, this,
connect(&SettingsCache::instance(), &SettingsCache::picDownloadChanged, this, &CardPictureLoader::picsPathChanged);
connect(&SettingsCache::instance().downloads(), &DownloadSettings::picDownloadChanged, this,
&CardPictureLoader::picDownloadChanged); &CardPictureLoader::picDownloadChanged);
qRegisterMetaType<ExactCard>(); qRegisterMetaType<ExactCard>();
@ -129,7 +138,14 @@ void CardPictureLoader::getPixmap(QPixmap &pixmap, const ExactCard &card, QSize
QPixmap bigPixmap; QPixmap bigPixmap;
if (QPixmapCache::find(key, &bigPixmap)) { if (QPixmapCache::find(key, &bigPixmap)) {
if (bigPixmap.isNull()) { if (bigPixmap.isNull()) {
qCDebug(CardPictureLoaderLog) << "Cached pixmap for key" << key << "is NULL!"; getCardBackLoadingFailedPixmap(pixmap, size);
QDateTime failedAtTime = getInstance().failedAt.value(key);
if (!failedAtTime.isValid() ||
failedAtTime.addSecs(RETRY_FAILED_CARDS_SECS) < QDateTime::currentDateTime()) {
getInstance().failedAt.remove(key);
QPixmapCache::remove(key);
getInstance().worker->enqueueImageLoad(card);
}
return; return;
} }
@ -153,8 +169,10 @@ void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image)
QPixmap finalPixmap; QPixmap finalPixmap;
if (image.isNull()) { if (image.isNull()) {
getInstance().failedAt.insert(card.getPixmapCacheKey(), QDateTime::currentDateTime());
qCDebug(CardPictureLoaderLog) << "Caching NULL pixmap for" << card.getName(); qCDebug(CardPictureLoaderLog) << "Caching NULL pixmap for" << card.getName();
} else { } else {
getInstance().failedAt.remove(card.getPixmapCacheKey());
if (card.getInfo().getUiAttributes().upsideDownArt) { if (card.getInfo().getUiAttributes().upsideDownArt) {
#if (QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)) #if (QT_VERSION >= QT_VERSION_CHECK(6, 9, 0))
QImage mirrorImage = image.flipped(Qt::Horizontal | Qt::Vertical); QImage mirrorImage = image.flipped(Qt::Horizontal | Qt::Vertical);
@ -169,7 +187,8 @@ void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image)
QPixmapCache::insert(card.getPixmapCacheKey(), finalPixmap); QPixmapCache::insert(card.getPixmapCacheKey(), finalPixmap);
if (SettingsCache::instance().getCardPictureLoaderCacheMethod() == if (static_cast<CardPictureLoaderCacheMethod::CacheMethod>(
SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) ==
CardPictureLoaderCacheMethod::CacheMethod::FILESYSTEM_CACHE) { CardPictureLoaderCacheMethod::CacheMethod::FILESYSTEM_CACHE) {
saveCardImageToLocalStorage(card, finalPixmap); saveCardImageToLocalStorage(card, finalPixmap);
} }
@ -177,8 +196,10 @@ void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image)
// imageLoaded should only be reached if the exactCard isn't already in cache. // imageLoaded should only be reached if the exactCard isn't already in cache.
// (plus there's a deduplication mechanism in CardPictureLoaderWorker) // (plus there's a deduplication mechanism in CardPictureLoaderWorker)
// It should be safe to connect the CardInfo here without worrying about redundant connections. // It should be safe to connect the CardInfo here without worrying about redundant connections.
connect(card.getCardPtr().data(), &QObject::destroyed, this, connect(card.getCardPtr().data(), &QObject::destroyed, this, [cacheKey = card.getPixmapCacheKey()] {
[cacheKey = card.getPixmapCacheKey()] { QPixmapCache::remove(cacheKey); }); QPixmapCache::remove(cacheKey);
getInstance().failedAt.remove(cacheKey);
});
card.emitPixmapUpdated(); card.emitPixmapUpdated();
} }
@ -189,9 +210,9 @@ void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card, const
return; return;
} }
const QString picsRoot = SettingsCache::instance().getPicsPath(); const QString picsRoot = SettingsCache::instance().paths().getPicsPath();
CardPictureLoaderLocalSchemes::NamingScheme scheme = CardPictureLoaderLocalSchemes::NamingScheme scheme = static_cast<CardPictureLoaderLocalSchemes::NamingScheme>(
SettingsCache::instance().getLocalCardImageStorageNamingScheme(); SettingsCache::instance().cacheStorage().getLocalCardImageStorageNamingScheme());
QString pattern; QString pattern;
@ -306,7 +327,7 @@ void CardPictureLoader::picsPathChanged()
bool CardPictureLoader::hasCustomArt() bool CardPictureLoader::hasCustomArt()
{ {
auto picsPath = SettingsCache::instance().getPicsPath(); auto picsPath = SettingsCache::instance().paths().getPicsPath();
QDirIterator it(picsPath, QDir::Dirs | QDir::NoDotAndDotDot); QDirIterator it(picsPath, QDir::Dirs | QDir::NoDotAndDotDot);
// Check if there is at least one non-directory file in the pics path, other // Check if there is at least one non-directory file in the pics path, other

View file

@ -4,6 +4,8 @@
#include "card_picture_loader_status_bar.h" #include "card_picture_loader_status_bar.h"
#include "card_picture_loader_worker.h" #include "card_picture_loader_worker.h"
#include <QDateTime>
#include <QHash>
#include <QLoggingCategory> #include <QLoggingCategory>
inline Q_LOGGING_CATEGORY(CardPictureLoaderLog, "card_picture_loader"); inline Q_LOGGING_CATEGORY(CardPictureLoaderLog, "card_picture_loader");
@ -56,6 +58,7 @@ private:
CardPictureLoaderWorker *worker; ///< Worker thread for async image loading CardPictureLoaderWorker *worker; ///< Worker thread for async image loading
CardPictureLoaderStatusBar *statusBar; ///< Status bar widget showing load progress CardPictureLoaderStatusBar *statusBar; ///< Status bar widget showing load progress
QHash<QString, QDateTime> failedAt; ///< Timestamp of the last failed load attempt per pixmap cache key
public: public:
/** /**

View file

@ -7,15 +7,16 @@
#include <QDirIterator> #include <QDirIterator>
#include <QMovie> #include <QMovie>
#include <libcockatrice/card/database/card_database_manager.h> #include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/settings/paths_settings.h>
static constexpr int REFRESH_INTERVAL_MS = 10 * 1000; static constexpr int REFRESH_INTERVAL_MS = 10 * 1000;
CardPictureLoaderLocal::CardPictureLoaderLocal(QObject *parent) CardPictureLoaderLocal::CardPictureLoaderLocal(QObject *parent)
: QObject(parent), picsPath(SettingsCache::instance().getPicsPath()), : QObject(parent), picsPath(SettingsCache::instance().paths().getPicsPath()),
customPicsPath(SettingsCache::instance().getCustomPicsPath()) customPicsPath(SettingsCache::instance().paths().getCustomPicsPath())
{ {
// Hook up signals to settings // Hook up signals to settings
connect(&SettingsCache::instance(), &SettingsCache::picsPathChanged, this, connect(&SettingsCache::instance().paths(), &PathsSettings::picsPathChanged, this,
&CardPictureLoaderLocal::picsPathChanged); &CardPictureLoaderLocal::picsPathChanged);
refreshIndex(); refreshIndex();
@ -127,6 +128,6 @@ QImage CardPictureLoaderLocal::tryLoadCardImageFromDisk(const QString &setName,
void CardPictureLoaderLocal::picsPathChanged() void CardPictureLoaderLocal::picsPathChanged()
{ {
picsPath = SettingsCache::instance().getPicsPath(); picsPath = SettingsCache::instance().paths().getPicsPath();
customPicsPath = SettingsCache::instance().getCustomPicsPath(); customPicsPath = SettingsCache::instance().paths().getCustomPicsPath();
} }

View file

@ -1,6 +1,7 @@
#include "card_picture_loader_worker.h" #include "card_picture_loader_worker.h"
#include "../../client/settings/cache_settings.h" #include "../../client/settings/cache_settings.h"
#include "card_picture_loader_cache_method.h"
#include "card_picture_loader_local.h" #include "card_picture_loader_local.h"
#include "card_picture_loader_worker_work.h" #include "card_picture_loader_worker_work.h"
@ -9,13 +10,19 @@
#include <QNetworkDiskCache> #include <QNetworkDiskCache>
#include <QNetworkReply> #include <QNetworkReply>
#include <QThread> #include <QThread>
#include <libcockatrice/settings/cache_storage_settings.h>
#include <libcockatrice/settings/download_settings.h>
#include <libcockatrice/settings/paths_settings.h>
#include <utility> #include <utility>
#include <version_string.h> #include <version_string.h>
static constexpr int MAX_REQUESTS_PER_SEC = 10; 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() CardPictureLoaderWorker::CardPictureLoaderWorker()
: QObject(nullptr), picDownload(SettingsCache::instance().getPicDownload()), requestQuota(MAX_REQUESTS_PER_SEC) : QObject(nullptr), picDownload(SettingsCache::instance().downloads().getPicDownload()),
requestQuota(MAX_REQUESTS_PER_SEC)
{ {
networkManager = new QNetworkAccessManager(this); networkManager = new QNetworkAccessManager(this);
// We need a timeout to ensure requests don't hang indefinitely in case of // We need a timeout to ensure requests don't hang indefinitely in case of
@ -25,13 +32,14 @@ CardPictureLoaderWorker::CardPictureLoaderWorker()
cache = new QNetworkDiskCache(this); cache = new QNetworkDiskCache(this);
cache->setCacheDirectory(SettingsCache::instance().getNetworkCachePath()); cache->setCacheDirectory(SettingsCache::instance().getNetworkCachePath());
cache->setMaximumCacheSize(1024L * 1024L * cache->setMaximumCacheSize(1024L * 1024L *
static_cast<qint64>(SettingsCache::instance().getNetworkCacheSizeInMB())); static_cast<qint64>(SettingsCache::instance().cacheStorage().getNetworkCacheSizeInMB()));
connect(&SettingsCache::instance(), &SettingsCache::networkCacheSizeChanged, cache, [this](int newSizeInMB) { connect(&SettingsCache::instance().cacheStorage(), &CacheStorageSettings::networkCacheSizeChanged, cache,
if (cache) { [this](int newSizeInMB) {
cache->setMaximumCacheSize(1024L * 1024L * static_cast<qint64>(newSizeInMB)); if (cache) {
} cache->setMaximumCacheSize(1024L * 1024L * static_cast<qint64>(newSizeInMB));
}); }
});
networkManager->setCache(cache); networkManager->setCache(cache);
@ -39,7 +47,7 @@ CardPictureLoaderWorker::CardPictureLoaderWorker()
// We can't use NoLessSafeRedirectPolicy because it is not applied with AlwaysCache // We can't use NoLessSafeRedirectPolicy because it is not applied with AlwaysCache
networkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy); networkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy);
cacheFilePath = SettingsCache::instance().getRedirectCachePath() + REDIRECT_CACHE_FILENAME; cacheFilePath = SettingsCache::instance().paths().getRedirectCachePath() + REDIRECT_CACHE_FILENAME;
loadRedirectCache(); loadRedirectCache();
cleanStaleEntries(); cleanStaleEntries();
@ -72,7 +80,8 @@ void CardPictureLoaderWorker::queueRequest(const QUrl &url, CardPictureLoaderWor
queueRequest(cachedRedirect, worker); queueRequest(cachedRedirect, worker);
return; return;
} }
if (SettingsCache::instance().getCardPictureLoaderCacheMethod() == if (static_cast<CardPictureLoaderCacheMethod::CacheMethod>(
SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) ==
CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE && CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE &&
cache->metaData(url).isValid()) { cache->metaData(url).isValid()) {
// If we hit a cached url, we get to make the request for free, since it won't contribute towards the // If we hit a cached url, we get to make the request for free, since it won't contribute towards the
@ -98,8 +107,10 @@ QNetworkReply *CardPictureLoaderWorker::makeRequest(const QUrl &url, CardPicture
req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING)); req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
req.setRawHeader("Accept", "image/avif,image/webp,image/apng,image/,/*;q=0.8"); req.setRawHeader("Accept", "image/avif,image/webp,image/apng,image/,/*;q=0.8");
bool useNetworkCache = !picDownload && SettingsCache::instance().getCardPictureLoaderCacheMethod() == bool useNetworkCache =
CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE; !picDownload && static_cast<CardPictureLoaderCacheMethod::CacheMethod>(
SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) ==
CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE;
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute, req.setAttribute(QNetworkRequest::CacheLoadControlAttribute,
useNetworkCache ? QNetworkRequest::AlwaysCache : QNetworkRequest::AlwaysNetwork); useNetworkCache ? QNetworkRequest::AlwaysCache : QNetworkRequest::AlwaysNetwork);
@ -115,6 +126,19 @@ QNetworkReply *CardPictureLoaderWorker::makeRequest(const QUrl &url, CardPicture
void CardPictureLoaderWorker::resetRequestQuota() void CardPictureLoaderWorker::resetRequestQuota()
{ {
requestQuota = MAX_REQUESTS_PER_SEC; requestQuota = MAX_REQUESTS_PER_SEC;
QDateTime now = QDateTime::currentDateTime();
for (auto it = hostRequestQuota.begin(); it != hostRequestQuota.end(); ++it) {
if (!hostLast429.contains(it.key()) || now.msecsTo(hostLast429.value(it.key())) < -QUOTA_RECOVER_MS) {
it.value() = qMin(MAX_REQUESTS_PER_SEC, it.value() + 1);
}
}
for (const auto &request : requestLoadQueue) {
const QString host = request.first.host();
hostQuotaRemaining.insert(host, hostRequestQuota.value(host, MAX_REQUESTS_PER_SEC));
}
processQueuedRequests(); processQueuedRequests();
} }
@ -127,14 +151,26 @@ void CardPictureLoaderWorker::processQueuedRequests()
bool CardPictureLoaderWorker::processSingleRequest() bool CardPictureLoaderWorker::processSingleRequest()
{ {
if (!requestLoadQueue.isEmpty()) { for (int i = 0; i < requestLoadQueue.size(); ++i) {
auto request = requestLoadQueue.takeFirst(); const auto &request = requestLoadQueue.at(i);
makeRequest(request.first, request.second); QString host = request.first.host();
return true; int allowance = hostQuotaRemaining.value(host, MAX_REQUESTS_PER_SEC);
if (allowance > 0) {
hostQuotaRemaining.insert(host, allowance - 1);
makeRequest(request.first, request.second);
requestLoadQueue.removeAt(i);
return true;
}
} }
return false; return false;
} }
void CardPictureLoaderWorker::onHostRateLimited(const QString &host)
{
hostRequestQuota.insert(host, qMax(MIN_HOST_QUOTA, hostRequestQuota.value(host, MAX_REQUESTS_PER_SEC) / 2));
hostLast429.insert(host, QDateTime::currentDateTime());
}
void CardPictureLoaderWorker::enqueueImageLoad(const ExactCard &card) void CardPictureLoaderWorker::enqueueImageLoad(const ExactCard &card)
{ {
// Send call through a connection to ensure the handling is run on the pictureLoader thread // Send call through a connection to ensure the handling is run on the pictureLoader thread
@ -229,7 +265,7 @@ void CardPictureLoaderWorker::cleanStaleEntries()
auto it = redirectCache.begin(); auto it = redirectCache.begin();
while (it != redirectCache.end()) { while (it != redirectCache.end()) {
if (it.value().second.addDays(SettingsCache::instance().getRedirectCacheTtl()) < now) { if (it.value().second.addDays(SettingsCache::instance().cacheStorage().getRedirectCacheTtl()) < now) {
it = redirectCache.erase(it); // Remove stale entry it = redirectCache.erase(it); // Remove stale entry
} else { } else {
++it; ++it;

View file

@ -5,6 +5,8 @@
#include "card_picture_loader_worker_work.h" #include "card_picture_loader_worker_work.h"
#include "card_picture_to_load.h" #include "card_picture_to_load.h"
#include <QDateTime>
#include <QHash>
#include <QLoggingCategory> #include <QLoggingCategory>
#include <QMutex> #include <QMutex>
#include <QNetworkAccessManager> #include <QNetworkAccessManager>
@ -66,6 +68,12 @@ public:
*/ */
void queueRequest(const QUrl &url, CardPictureLoaderWorkerWork *worker); void queueRequest(const QUrl &url, CardPictureLoaderWorkerWork *worker);
/**
* @brief Handles a server returning HTTP 429 by reducing that host's request quota.
* @param host The host that returned 429
*/
void onHostRateLimited(const QString &host);
/** @brief Clears the network cache and redirect cache. */ /** @brief Clears the network cache and redirect cache. */
void clearNetworkCache(); void clearNetworkCache();
@ -110,8 +118,11 @@ private:
bool picDownload; ///< Whether downloading images from network is enabled bool picDownload; ///< Whether downloading images from network is enabled
QQueue<QPair<QUrl, CardPictureLoaderWorkerWork *>> requestLoadQueue; ///< Queue of pending network requests QQueue<QPair<QUrl, CardPictureLoaderWorkerWork *>> requestLoadQueue; ///< Queue of pending network requests
int requestQuota; ///< Remaining requests allowed per second int requestQuota; ///< Remaining requests allowed per second
QTimer requestTimer; ///< Timer to reset the request quota QTimer requestTimer; ///< Timer to reset the request quota
QHash<QString, int> hostRequestQuota; ///< Sustained per-host request allowance
QHash<QString, int> hostQuotaRemaining; ///< Per-host allowance left in the current second
QHash<QString, QDateTime> hostLast429; ///< When each host was last rate limited
CardPictureLoaderLocal *localLoader; ///< Loader for local images CardPictureLoaderLocal *localLoader; ///< Loader for local images
QSet<QString> currentlyLoading; ///< Deduplication: contains pixmapCacheKey currently being loaded QSet<QString> currentlyLoading; ///< Deduplication: contains pixmapCacheKey currently being loaded

View file

@ -8,8 +8,13 @@
#include <QLoggingCategory> #include <QLoggingCategory>
#include <QMovie> #include <QMovie>
#include <QNetworkReply> #include <QNetworkReply>
#include <QRandomGenerator>
#include <QThread> #include <QThread>
#include <QThreadPool> #include <QThreadPool>
#include <QTimer>
ServerRateLimiter CardPictureLoaderWorkerWork::s_rateLimiter;
#include <libcockatrice/settings/download_settings.h>
// Card back returned by gatherer when card is not found // Card back returned by gatherer when card is not found
static const QStringList MD5_BLACKLIST = { static const QStringList MD5_BLACKLIST = {
@ -19,7 +24,7 @@ static const QStringList MD5_BLACKLIST = {
CardPictureLoaderWorkerWork::CardPictureLoaderWorkerWork(const CardPictureLoaderWorker *worker, const ExactCard &toLoad) CardPictureLoaderWorkerWork::CardPictureLoaderWorkerWork(const CardPictureLoaderWorker *worker, const ExactCard &toLoad)
: QObject(nullptr), cardToDownload(CardPictureToLoad(toLoad)), : QObject(nullptr), cardToDownload(CardPictureToLoad(toLoad)),
picDownload(SettingsCache::instance().getPicDownload()) picDownload(SettingsCache::instance().downloads().getPicDownload())
{ {
// Hook up signals to the orchestrator // Hook up signals to the orchestrator
connect(this, &CardPictureLoaderWorkerWork::requestImageDownload, worker, &CardPictureLoaderWorker::queueRequest); connect(this, &CardPictureLoaderWorkerWork::requestImageDownload, worker, &CardPictureLoaderWorker::queueRequest);
@ -29,19 +34,48 @@ CardPictureLoaderWorkerWork::CardPictureLoaderWorkerWork(const CardPictureLoader
connect(this, &CardPictureLoaderWorkerWork::imageLoaded, worker, &CardPictureLoaderWorker::handleImageLoaded); connect(this, &CardPictureLoaderWorkerWork::imageLoaded, worker, &CardPictureLoaderWorker::handleImageLoaded);
connect(this, &CardPictureLoaderWorkerWork::requestSucceeded, worker, connect(this, &CardPictureLoaderWorkerWork::requestSucceeded, worker,
&CardPictureLoaderWorker::imageRequestSucceeded); &CardPictureLoaderWorker::imageRequestSucceeded);
connect(this, &CardPictureLoaderWorkerWork::rateLimited, worker, &CardPictureLoaderWorker::onHostRateLimited);
// Hook up signals to settings // Hook up signals to settings
connect(&SettingsCache::instance(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged())); connect(&SettingsCache::instance().downloads(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged()));
startNextPicDownload(); startNextPicDownload();
} }
void CardPictureLoaderWorkerWork::startNextPicDownload() void CardPictureLoaderWorkerWork::startNextPicDownload()
{ {
QDateTime now = QDateTime::currentDateTime();
while (!cardToDownload.getCurrentUrl().isEmpty() &&
s_rateLimiter.isRateLimited(QUrl(cardToDownload.getCurrentUrl()).host(), now)) {
QString host = QUrl(cardToDownload.getCurrentUrl()).host();
if (s_rateLimiter.rounds(host) == 1) {
// First 429 round for this server: wait out the backoff and give it
// one more chance instead of immediately falling through to a worse
// source. A second 429 makes us fall through instead.
qCDebug(CardPictureLoaderWorkerWorkLog).nospace()
<< "PictureLoader: [card: " << cardToDownload.getCard().getInfo().getCorrectedName()
<< " set: " << cardToDownload.getSetName() << "]: Waiting out backoff for " << host << " to retry "
<< cardToDownload.getCurrentUrl();
scheduleDeferredRetry();
return;
}
// The server has already 429'd us at least twice, so further retries are
// unlikely to succeed: move on to the other configured sources.
qCDebug(CardPictureLoaderWorkerWorkLog).nospace()
<< "PictureLoader: [card: " << cardToDownload.getCard().getInfo().getCorrectedName()
<< " set: " << cardToDownload.getSetName() << "]: Skipping rate-limited URL "
<< cardToDownload.getCurrentUrl() << " (server " << host << " still rate limiting)";
if (!cardToDownload.nextUrl() && !cardToDownload.nextSet()) {
scheduleDeferredRetry();
return;
}
}
QString picUrl = cardToDownload.getCurrentUrl(); QString picUrl = cardToDownload.getCurrentUrl();
if (picUrl.isEmpty()) { if (picUrl.isEmpty()) {
picDownloadFailed(); scheduleDeferredRetry();
} else { } else {
QUrl url(picUrl); QUrl url(picUrl);
qCDebug(CardPictureLoaderWorkerWorkLog).nospace() qCDebug(CardPictureLoaderWorkerWorkLog).nospace()
@ -107,7 +141,41 @@ static bool imageIsBlackListed(const QByteArray &picData)
void CardPictureLoaderWorkerWork::handleFailedReply(const QNetworkReply *reply) void CardPictureLoaderWorkerWork::handleFailedReply(const QNetworkReply *reply)
{ {
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 429) { if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 429) {
qCWarning(CardPictureLoaderWorkerWorkLog) << "Too many requests."; QString host = reply->url().host();
QDateTime now = QDateTime::currentDateTime();
qint64 retryAfterMs = 0;
const QByteArray retryAfterHeader = reply->rawHeader("Retry-After");
if (!retryAfterHeader.isEmpty()) {
bool ok = false;
int seconds = retryAfterHeader.toInt(&ok);
if (ok && seconds > 0) {
retryAfterMs = static_cast<qint64>(seconds) * 1000;
} else {
QDateTime retryAfterDate =
QDateTime::fromString(QString::fromLatin1(retryAfterHeader), Qt::RFC2822Date);
if (retryAfterDate.isValid()) {
retryAfterMs = qMax<qint64>(0, now.msecsTo(retryAfterDate));
}
}
}
QDateTime backoffUntil = s_rateLimiter.on429(host, now, retryAfterMs);
emit rateLimited(host);
if (s_rateLimiter.rounds(host) == 1) {
qCWarning(CardPictureLoaderWorkerWorkLog).nospace()
<< "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();
} else {
qCWarning(CardPictureLoaderWorkerWorkLog).nospace()
<< "PictureLoader: [card: " << cardToDownload.getCard().getName()
<< " set: " << cardToDownload.getSetName() << "]: Too many requests from " << host
<< ", retry already attempted, falling through to other sources";
picDownloadFailed();
}
} else { } else {
bool isFromCache = reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool(); bool isFromCache = reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool();
@ -148,6 +216,9 @@ void CardPictureLoaderWorkerWork::handleSuccessfulReply(QNetworkReply *reply)
return; return;
} }
// A non-redirect successful response means the server is not rate limiting us anymore.
s_rateLimiter.onSuccess(reply->url().host());
// peek is used to keep the data in the buffer for use by QImageReader // peek is used to keep the data in the buffer for use by QImageReader
const QByteArray &picData = reply->peek(reply->size()); const QByteArray &picData = reply->peek(reply->size());
@ -202,6 +273,42 @@ QImage CardPictureLoaderWorkerWork::tryLoadImageFromReply(QNetworkReply *reply)
return imgReader.read(); return imgReader.read();
} }
void CardPictureLoaderWorkerWork::scheduleDeferredRetry()
{
QDateTime now = QDateTime::currentDateTime();
// 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);
}
if (!backoffUntil.isValid()) {
qCWarning(CardPictureLoaderWorkerWorkLog).nospace()
<< "PictureLoader: [card: " << cardToDownload.getCard().getInfo().getCorrectedName()
<< " set: " << cardToDownload.getSetName() << "]: All URLs exhausted, no servers in backoff: BAILING OUT";
concludeImageLoad(QImage());
return;
}
qint64 waitMs = qMax<qint64>(0, now.msecsTo(backoffUntil));
// Add some jitter to desynchronize concurrent retries and avoid a thundering herd.
waitMs += QRandomGenerator::global()->bounded(5000);
qCDebug(CardPictureLoaderWorkerWorkLog).nospace()
<< "PictureLoader: [card: " << cardToDownload.getCard().getInfo().getCorrectedName()
<< " set: " << cardToDownload.getSetName() << "]: All URLs exhausted, scheduling deferred retry in " << waitMs
<< "ms";
QTimer::singleShot(waitMs, this, [this] {
s_rateLimiter.clearExpired(QDateTime::currentDateTime());
cardToDownload.resetIndices();
startNextPicDownload();
});
}
void CardPictureLoaderWorkerWork::concludeImageLoad(const QImage &image) void CardPictureLoaderWorkerWork::concludeImageLoad(const QImage &image)
{ {
emit imageLoaded(cardToDownload.getCard(), image); emit imageLoaded(cardToDownload.getCard(), image);
@ -210,5 +317,5 @@ void CardPictureLoaderWorkerWork::concludeImageLoad(const QImage &image)
void CardPictureLoaderWorkerWork::picDownloadChanged() void CardPictureLoaderWorkerWork::picDownloadChanged()
{ {
picDownload = SettingsCache::instance().getPicDownload(); picDownload = SettingsCache::instance().downloads().getPicDownload();
} }

View file

@ -4,12 +4,15 @@
#include "card_picture_loader_worker.h" #include "card_picture_loader_worker.h"
#include "card_picture_to_load.h" #include "card_picture_to_load.h"
#include <QDateTime>
#include <QLoggingCategory> #include <QLoggingCategory>
#include <QMutex> #include <QMutex>
#include <QNetworkAccessManager> #include <QNetworkAccessManager>
#include <QObject> #include <QObject>
#include <QRandomGenerator>
#include <QThread> #include <QThread>
#include <libcockatrice/card/database/card_database.h> #include <libcockatrice/card/database/card_database.h>
#include <libcockatrice/utility/server_rate_limiter.h>
inline Q_LOGGING_CATEGORY(CardPictureLoaderWorkerWorkLog, "card_picture_loader.worker"); inline Q_LOGGING_CATEGORY(CardPictureLoaderWorkerWorkLog, "card_picture_loader.worker");
@ -50,6 +53,8 @@ public slots:
private: private:
bool picDownload; ///< Whether network downloading is enabled bool picDownload; ///< Whether network downloading is enabled
static ServerRateLimiter s_rateLimiter; ///< Shared per-server 429 backoff state
/** @brief Starts downloading the next URL for this card. */ /** @brief Starts downloading the next URL for this card. */
void startNextPicDownload(); void startNextPicDownload();
@ -77,6 +82,16 @@ private:
*/ */
void concludeImageLoad(const QImage &image); 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: private slots:
/** @brief Updates the picDownload setting when it changes. */ /** @brief Updates the picDownload setting when it changes. */
void picDownloadChanged(); void picDownloadChanged();
@ -100,6 +115,9 @@ signals:
/** @brief Emitted when a URL has been redirected. */ /** @brief Emitted when a URL has been redirected. */
void urlRedirected(const QUrl &originalUrl, const QUrl &redirectUrl); void urlRedirected(const QUrl &originalUrl, const QUrl &redirectUrl);
/** @brief Emitted when a server returned HTTP 429. */
void rateLimited(const QString &host);
/** @brief Emitted when a cached URL is invalid and must be removed. */ /** @brief Emitted when a cached URL is invalid and must be removed. */
void cachedUrlInvalidated(const QUrl &url); void cachedUrlInvalidated(const QUrl &url);
}; };

View file

@ -9,14 +9,17 @@
#include <algorithm> #include <algorithm>
#include <libcockatrice/card/set/card_set_comparator.h> #include <libcockatrice/card/set/card_set_comparator.h>
#include <libcockatrice/interfaces/noop_card_set_priority_controller.h> #include <libcockatrice/interfaces/noop_card_set_priority_controller.h>
#include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/settings/download_settings.h>
CardPictureToLoad::CardPictureToLoad(const ExactCard &_card) CardPictureToLoad::CardPictureToLoad(const ExactCard &_card)
: card(_card), urlTemplates(SettingsCache::instance().downloads().getAllURLs()) : card(_card), urlTemplates(SettingsCache::instance().downloads().getAllURLs())
{ {
if (card) { if (card) {
sortedSets = extractSetsSorted(card); sortedSets = extractSetsSorted(card);
// The first time called, nextSet will also populate the Urls for the first set. currentSetIndex = 0;
nextSet(); currentSet = sortedSets.first();
populateSetUrls();
} }
} }
@ -34,7 +37,7 @@ QList<CardSetPtr> CardPictureToLoad::extractSetsSorted(const ExactCard &card)
std::sort(sortedSets.begin(), sortedSets.end(), SetPriorityComparator()); std::sort(sortedSets.begin(), sortedSets.end(), SetPriorityComparator());
// If the user hasn't disabled arts other than their personal preference... // If the user hasn't disabled arts other than their personal preference...
if (!SettingsCache::instance().getOverrideAllCardArtWithPersonalPreference()) { if (!SettingsCache::instance().cardsDisplay().getOverrideAllCardArtWithPersonalPreference()) {
// If the pixmapCacheKey corresponds to a specific set, we have to try to load it first. // If the pixmapCacheKey corresponds to a specific set, we have to try to load it first.
qsizetype setIndex = sortedSets.indexOf(card.getPrinting().getSet()); qsizetype setIndex = sortedSets.indexOf(card.getPrinting().getSet());
if (setIndex > 0) { // we don't need to move the set if it's already first if (setIndex > 0) { // we don't need to move the set if it's already first
@ -99,15 +102,19 @@ void CardPictureToLoad::populateSetUrls()
} }
} }
/* Call nextUrl to make sure currentUrl is up-to-date currentUrlIndex = 0;
but we don't need the result here. */ if (!currentSetUrls.isEmpty()) {
(void)nextUrl(); currentUrl = currentSetUrls.first();
} else {
currentUrl = QString();
}
} }
bool CardPictureToLoad::nextSet() bool CardPictureToLoad::nextSet()
{ {
if (!sortedSets.isEmpty()) { currentSetIndex++;
currentSet = sortedSets.takeFirst(); if (currentSetIndex < sortedSets.size()) {
currentSet = sortedSets.at(currentSetIndex);
populateSetUrls(); populateSetUrls();
return true; return true;
} }
@ -117,8 +124,9 @@ bool CardPictureToLoad::nextSet()
bool CardPictureToLoad::nextUrl() bool CardPictureToLoad::nextUrl()
{ {
if (!currentSetUrls.isEmpty()) { currentUrlIndex++;
currentUrl = currentSetUrls.takeFirst(); if (currentUrlIndex < currentSetUrls.size()) {
currentUrl = currentSetUrls.at(currentUrlIndex);
return true; return true;
} }
currentUrl = QString(); currentUrl = QString();
@ -134,6 +142,28 @@ QString CardPictureToLoad::getSetName() const
} }
} }
QString CardPictureToLoad::peekNextUrl() const
{
int nextIndex = currentUrlIndex + 1;
if (nextIndex < currentSetUrls.size()) {
return currentSetUrls.at(nextIndex);
}
return QString();
}
void CardPictureToLoad::resetIndices()
{
currentSetIndex = 0;
if (!sortedSets.isEmpty()) {
currentSet = sortedSets.first();
populateSetUrls();
} else {
currentSet = {};
currentSetUrls.clear();
currentUrl = QString();
}
}
static int parse(const QString &urlTemplate, static int parse(const QString &urlTemplate,
const QString &propType, const QString &propType,
const QString &cardName, const QString &cardName,

View file

@ -25,6 +25,8 @@ private:
QList<QString> currentSetUrls; ///< URLs for the current set being attempted QList<QString> currentSetUrls; ///< URLs for the current set being attempted
QString currentUrl; ///< Currently active URL to download QString currentUrl; ///< Currently active URL to download
CardSetPtr currentSet; ///< Currently active set CardSetPtr currentSet; ///< Currently active set
int currentSetIndex = 0; ///< Current position in sortedSets
int currentUrlIndex = 0; ///< Current position in currentSetUrls
public: public:
/** /**
@ -56,6 +58,9 @@ public:
/** @return The short name of the current set, or empty string if no set. */ /** @return The short name of the current set, or empty string if no set. */
[[nodiscard]] QString getSetName() const; [[nodiscard]] QString getSetName() const;
/** @return The next URL in the current set's list without advancing, or empty if at end. */
[[nodiscard]] QString peekNextUrl() const;
/** /**
* @brief Transforms a URL template into a concrete URL for this card/set. * @brief Transforms a URL template into a concrete URL for this card/set.
* @param urlTemplate The URL template to transform * @param urlTemplate The URL template to transform
@ -88,6 +93,14 @@ public:
*/ */
void populateSetUrls(); void populateSetUrls();
/**
* @brief Resets iteration indices to the beginning.
*
* Restarts URL/set iteration from the first set and first URL.
* Used for deferred retry after server backoff expires.
*/
void resetIndices();
/** /**
* @brief Extract all sets from the card and sort them by priority. * @brief Extract all sets from the card and sort them by priority.
* @param card The card to extract sets from * @param card The card to extract sets from

View file

@ -0,0 +1,14 @@
#ifndef COCKATRICE_CONTEXT_CONNECT_TO_SERVER_H
#define COCKATRICE_CONTEXT_CONNECT_TO_SERVER_H
#include <QString>
struct ContextConnectToServer
{
QString hostname;
QString port;
QString username;
QString password;
};
#endif // COCKATRICE_CONTEXT_CONNECT_TO_SERVER_H

View file

@ -0,0 +1,11 @@
#ifndef COCKATRICE_CONTEXT_JOIN_GAME_H
#define COCKATRICE_CONTEXT_JOIN_GAME_H
#include "context_join_room.h"
struct ContextJoinGame
{
ContextJoinRoom roomContext;
int gameId;
};
#endif // COCKATRICE_CONTEXT_JOIN_GAME_H

Some files were not shown because too many files have changed in this diff Show more