mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-21 09:05:10 -07:00
Merge branch 'Cockatrice:master' into toggle-normal-untapping-once
This commit is contained in:
commit
fdd7d4eac0
543 changed files with 82935 additions and 48240 deletions
|
|
@ -159,6 +159,7 @@ if [[ $PACKAGE_TYPE ]]; then
|
|||
fi
|
||||
if [[ $USE_VCPKG ]]; then
|
||||
flags+=("-DUSE_VCPKG=1")
|
||||
flags+=("-DVCPKG_INSTALL_OPTIONS=--x-abi-tools-use-exact-versions")
|
||||
fi
|
||||
|
||||
# Add cmake --build flags
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
exclude_paths:
|
||||
- '**/translations/*.ts'
|
||||
|
||||
# codacy config documentation: https://support.codacy.com/hc/en-us/articles/115002130625-Codacy-Configuration-File
|
||||
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
|
|
@ -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);
|
||||
```
|
||||
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
|
||||
string in the code, you don't need to take care of adding the new strings to
|
||||
|
|
|
|||
102
.github/workflows/desktop-build.yml
vendored
102
.github/workflows/desktop-build.yml
vendored
|
|
@ -3,7 +3,7 @@ name: Build Desktop
|
|||
permissions:
|
||||
actions: write # needed to delete entries in GHA cache (update ccache)
|
||||
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
|
||||
|
||||
on:
|
||||
|
|
@ -47,26 +47,21 @@ jobs:
|
|||
tag: ${{ steps.configure.outputs.tag }}
|
||||
sha: ${{ steps.configure.outputs.sha }}
|
||||
|
||||
steps:
|
||||
steps:
|
||||
- name: "Configure"
|
||||
env:
|
||||
RESOLVED_SHA: ${{ case(github.event_name == 'pull_request', github.event.pull_request.head.sha, github.sha) }}
|
||||
id: configure
|
||||
shell: bash
|
||||
run: |
|
||||
tag_regex='^refs/tags/'
|
||||
if [[ $GITHUB_EVENT_NAME == pull-request ]]; then # pull request
|
||||
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"
|
||||
if [[ "$GITHUB_REF_TYPE" == 'tag' ]]; then # release
|
||||
echo "tag=$GITHUB_REF_NAME" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
echo "sha=$sha" >>"$GITHUB_OUTPUT"
|
||||
echo "sha=$RESOLVED_SHA" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: "Checkout"
|
||||
if: steps.configure.outputs.tag != null
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0 # fetch all history for all branches and tags
|
||||
|
||||
|
|
@ -92,7 +87,7 @@ jobs:
|
|||
run: |
|
||||
args=()
|
||||
[[ $prerelease == yes ]] && args+=(--prerelease)
|
||||
|
||||
|
||||
gh release create "$tag_name" --verify-tag --draft "${args[@]}" \
|
||||
--target "$target" \
|
||||
--title "$release_name" \
|
||||
|
|
@ -105,48 +100,48 @@ jobs:
|
|||
# The files in ".ci/$distro$version" correspond to the values given here
|
||||
include:
|
||||
- distro: Arch
|
||||
|
||||
|
||||
allow-failure: yes
|
||||
package: skip # We are packaged in Arch already
|
||||
|
||||
- distro: Servatrice_Debian
|
||||
version: 12
|
||||
|
||||
|
||||
package: DEB
|
||||
server_only: yes
|
||||
test: skip
|
||||
|
||||
- distro: Debian
|
||||
version: 12
|
||||
|
||||
|
||||
package: DEB
|
||||
test: skip # Running tests on all distros is superfluous
|
||||
|
||||
- distro: Debian
|
||||
version: 13
|
||||
|
||||
|
||||
package: DEB
|
||||
|
||||
- distro: Fedora
|
||||
version: 43
|
||||
|
||||
|
||||
package: RPM
|
||||
test: skip # Running tests on all distros is superfluous
|
||||
|
||||
- distro: Fedora
|
||||
version: 44
|
||||
|
||||
|
||||
package: RPM
|
||||
|
||||
- distro: Ubuntu
|
||||
version: 24.04
|
||||
|
||||
|
||||
package: DEB
|
||||
test: skip # Running tests on all distros is superfluous
|
||||
|
||||
- distro: Ubuntu
|
||||
version: 26.04
|
||||
|
||||
|
||||
package: DEB
|
||||
|
||||
name: ${{ matrix.distro }} ${{ matrix.version }}
|
||||
|
|
@ -163,11 +158,11 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: "Restore compiler cache (ccache)"
|
||||
id: ccache_restore
|
||||
uses: actions/cache/restore@v5
|
||||
uses: actions/cache/restore@v6
|
||||
env:
|
||||
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
|
||||
with:
|
||||
|
|
@ -203,7 +198,7 @@ jobs:
|
|||
args+=(--ccache "$CCACHE_SIZE")
|
||||
args+=(--cmake-generator "$CMAKE_GENERATOR")
|
||||
args+=(--suffix "$SUFFIX")
|
||||
|
||||
|
||||
RUN --server --release --package "$package" "${args[@]}"
|
||||
|
||||
# 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
|
||||
continue-on-error: true
|
||||
env:
|
||||
CACHE_PRIMARY_KEY: ${{ steps.ccache_restore.outputs.cache-primary-key }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
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"
|
||||
fi
|
||||
|
||||
- name: "Save updated compiler cache (ccache)"
|
||||
if: github.ref == 'refs/heads/master'
|
||||
uses: actions/cache/save@v5
|
||||
uses: actions/cache/save@v6
|
||||
with:
|
||||
key: ${{ steps.ccache_restore.outputs.cache-primary-key }}
|
||||
path: ${{ env.CACHE }}
|
||||
|
|
@ -256,8 +252,9 @@ jobs:
|
|||
if: steps.attestation.outcome == 'success'
|
||||
shell: bash
|
||||
env:
|
||||
BUILD_PATH: ${{ steps.build.outputs.path }}
|
||||
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:
|
||||
strategy:
|
||||
|
|
@ -267,14 +264,13 @@ jobs:
|
|||
- os: macOS
|
||||
target: 13
|
||||
runner: macos-15-intel
|
||||
|
||||
|
||||
ccache_eviction_age: 7d
|
||||
cmake_generator: Ninja
|
||||
make_package: 1
|
||||
override_target: 13
|
||||
package_suffix: "-macOS13_Intel"
|
||||
qt_version: 6.11.0
|
||||
qt_arch: clang_64
|
||||
qt_modules: qtimageformats qtmultimedia qtwebsockets
|
||||
soc: Intel
|
||||
type: Release
|
||||
|
|
@ -284,13 +280,12 @@ jobs:
|
|||
- os: macOS
|
||||
target: 14
|
||||
runner: macos-14
|
||||
|
||||
|
||||
ccache_eviction_age: 7d
|
||||
cmake_generator: Ninja
|
||||
make_package: 1
|
||||
package_suffix: "-macOS14"
|
||||
qt_version: 6.11.0
|
||||
qt_arch: clang_64
|
||||
qt_modules: qtimageformats qtmultimedia qtwebsockets
|
||||
soc: Apple
|
||||
type: Release
|
||||
|
|
@ -300,13 +295,12 @@ jobs:
|
|||
- os: macOS
|
||||
target: 15
|
||||
runner: macos-15
|
||||
|
||||
|
||||
ccache_eviction_age: 7d
|
||||
cmake_generator: Ninja
|
||||
make_package: 1
|
||||
package_suffix: "-macOS15"
|
||||
qt_version: 6.11.0
|
||||
qt_arch: clang_64
|
||||
qt_modules: qtimageformats qtmultimedia qtwebsockets
|
||||
soc: Apple
|
||||
type: Release
|
||||
|
|
@ -316,11 +310,10 @@ jobs:
|
|||
- os: macOS
|
||||
target: 15
|
||||
runner: macos-15
|
||||
|
||||
|
||||
ccache_eviction_age: 7d
|
||||
cmake_generator: Ninja
|
||||
qt_version: 6.11.0
|
||||
qt_arch: clang_64
|
||||
qt_modules: qtimageformats qtmultimedia qtwebsockets
|
||||
soc: Apple
|
||||
type: Debug
|
||||
|
|
@ -330,13 +323,12 @@ jobs:
|
|||
- os: Windows
|
||||
target: 10
|
||||
runner: windows-2025
|
||||
|
||||
cmake_generator: "Visual Studio 17 2022"
|
||||
|
||||
cmake_generator: "Visual Studio 18 2026"
|
||||
cmake_generator_platform: x64
|
||||
make_package: 1
|
||||
package_suffix: "-Win10"
|
||||
qt_version: 6.11.0
|
||||
qt_arch: win64_msvc2022_64
|
||||
qt_modules: qtimageformats qtmultimedia qtwebsockets
|
||||
type: Release
|
||||
|
||||
|
|
@ -350,7 +342,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
|
|
@ -368,7 +360,7 @@ jobs:
|
|||
- name: "[macOS] Restore compiler cache (ccache)"
|
||||
if: matrix.os == 'macOS' && matrix.use_ccache == 1
|
||||
id: ccache_restore
|
||||
uses: actions/cache/restore@v5
|
||||
uses: actions/cache/restore@v6
|
||||
env:
|
||||
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
|
||||
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
|
||||
- name: "Resolve latest Qt patch version"
|
||||
env:
|
||||
QT_VERSION: ${{ matrix.qt_version }}
|
||||
id: resolve_qt_version
|
||||
shell: bash
|
||||
run: .ci/resolve_latest_aqt_qt_version.sh "${{ 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"
|
||||
if: matrix.os == 'macOS'
|
||||
id: restore_qt
|
||||
uses: actions/cache/restore@v5
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }}
|
||||
path: ${{ github.workspace }}/Qt
|
||||
|
|
@ -399,7 +393,6 @@ jobs:
|
|||
if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true'
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
arch: ${{ matrix.qt_arch }}
|
||||
cache: false
|
||||
dir: ${{ github.workspace }}
|
||||
modules: ${{ matrix.qt_modules }}
|
||||
|
|
@ -411,7 +404,7 @@ jobs:
|
|||
|
||||
- name: "[macOS] Cache thin Qt libraries"
|
||||
if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@v5
|
||||
uses: actions/cache/save@v6
|
||||
with:
|
||||
key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }}
|
||||
path: ${{ github.workspace }}/Qt
|
||||
|
|
@ -422,7 +415,6 @@ jobs:
|
|||
with:
|
||||
# Qt 6.11.0 only works with aqtinstall directly from git until aqtinstall 3.4 is released
|
||||
aqtsource: git+https://github.com/miurahr/aqtinstall.git
|
||||
arch: ${{ matrix.qt_arch }}
|
||||
cache: true
|
||||
modules: ${{ matrix.qt_modules }}
|
||||
version: ${{ steps.resolve_qt_version.outputs.version }}
|
||||
|
|
@ -448,6 +440,7 @@ jobs:
|
|||
CMAKE_GENERATOR: ${{ matrix.cmake_generator }}
|
||||
CMAKE_GENERATOR_PLATFORM: ${{ matrix.cmake_generator_platform }}
|
||||
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_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
|
||||
MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }}
|
||||
|
|
@ -458,6 +451,7 @@ jobs:
|
|||
USE_CCACHE: ${{ matrix.use_ccache }}
|
||||
VCPKG_BINARY_SOURCES: 'clear;files,${{ steps.vcpkg-cache.outputs.path }},readwrite'
|
||||
VCPKG_DISABLE_METRICS: 1
|
||||
VCPKG_FEATURE_FLAGS: dependencygraph
|
||||
run: .ci/compile.sh --server --test --vcpkg
|
||||
|
||||
# Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342
|
||||
|
|
@ -465,15 +459,16 @@ jobs:
|
|||
if: matrix.os == 'macOS' && matrix.use_ccache == 1 && github.ref == 'refs/heads/master' && steps.ccache_restore.outputs.cache-hit
|
||||
continue-on-error: true
|
||||
env:
|
||||
CACHE_PRIMARY_KEY: ${{ steps.ccache_restore.outputs.cache-primary-key }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
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"
|
||||
fi
|
||||
|
||||
- name: "[macOS] Save updated compiler cache (ccache)"
|
||||
if: matrix.os == 'macOS' && matrix.use_ccache == 1 && github.ref == 'refs/heads/master'
|
||||
uses: actions/cache/save@v5
|
||||
uses: actions/cache/save@v6
|
||||
with:
|
||||
key: ${{ steps.ccache_restore.outputs.cache-primary-key }}
|
||||
path: ${{ env.CCACHE_DIR }}
|
||||
|
|
@ -482,18 +477,20 @@ jobs:
|
|||
if: matrix.os == 'macOS' && matrix.make_package && needs.configure.outputs.tag != null
|
||||
id: sign_macos
|
||||
env:
|
||||
BUILD_PATH: ${{ steps.build.outputs.path }}
|
||||
MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
|
||||
MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}
|
||||
run: |
|
||||
if [[ -n "$MACOS_CERTIFICATE_NAME" ]]
|
||||
then
|
||||
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
|
||||
|
||||
- name: "[macOS] Notarize app bundle"
|
||||
if: matrix.os == 'macOS' && steps.sign_macos.outcome == 'success'
|
||||
env:
|
||||
BUILD_PATH: ${{ steps.build.outputs.path }}
|
||||
MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }}
|
||||
MACOS_NOTARIZATION_PWD: ${{ secrets.PROD_MACOS_NOTARIZATION_PWD }}
|
||||
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
|
||||
# notarization service
|
||||
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.
|
||||
# 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
|
||||
# validated by macOS even when an internet connection is not available.
|
||||
echo "Attach staple"
|
||||
xcrun stapler staple "${{ steps.build.outputs.path }}"
|
||||
xcrun stapler staple "$BUILD_PATH"
|
||||
fi
|
||||
|
||||
- name: "Upload artifact"
|
||||
|
|
@ -566,5 +563,6 @@ jobs:
|
|||
if: steps.attestation.outcome == 'success'
|
||||
shell: bash
|
||||
env:
|
||||
BUILD_PATH: ${{ steps.build.outputs.path }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: gh attestation verify "${{ steps.build.outputs.path }}" --repo Cockatrice/Cockatrice
|
||||
run: gh attestation verify "$BUILD_PATH" --repo Cockatrice/Cockatrice
|
||||
|
|
|
|||
2
.github/workflows/desktop-lint.yml
vendored
2
.github/workflows/desktop-lint.yml
vendored
|
|
@ -22,7 +22,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 20 # should be enough to find merge base
|
||||
|
||||
|
|
|
|||
9
.github/workflows/docker-release.yml
vendored
9
.github/workflows/docker-release.yml
vendored
|
|
@ -31,7 +31,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: "Docker metadata"
|
||||
id: metadata
|
||||
|
|
@ -56,8 +56,9 @@ jobs:
|
|||
- name: "Set up Docker buildx"
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: "Login to GitHub Container Registry"
|
||||
if: contains(github.event.release.tag_name, 'Release') && github.event.release.target_commitish == 'master'
|
||||
- name: "Login to GitHub Container Registry (GHCR)"
|
||||
if: github.event_name == 'release' && github.event.release.prerelease == false
|
||||
id: login
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
password: ${{ github.token }}
|
||||
|
|
@ -73,5 +74,5 @@ jobs:
|
|||
context: .
|
||||
labels: ${{ steps.metadata.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: ${{ github.ref_type == 'tag' }}
|
||||
push: ${{ steps.login.outcome == 'success' }}
|
||||
tags: ${{ steps.metadata.outputs.tags }}
|
||||
|
|
|
|||
2
.github/workflows/documentation-build.yml
vendored
2
.github/workflows/documentation-build.yml
vendored
|
|
@ -21,7 +21,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
|
|
|
|||
20
.github/workflows/translations-pull.yml
vendored
20
.github/workflows/translations-pull.yml
vendored
|
|
@ -20,9 +20,11 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: "Checkout repo"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- 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
|
||||
with:
|
||||
# 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
|
||||
body: |
|
||||
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>
|
||||
*After merging, all new languages and translations are available in the next build.*
|
||||
|
||||
|
||||
[1]: https://explore.transifex.com/cockatrice/cockatrice/
|
||||
[2]: https://github.com/Cockatrice/Cockatrice/actions/workflows/translations-pull.yml?query=branch%3Amaster
|
||||
branch: ci-update_translations
|
||||
|
|
@ -61,11 +63,9 @@ jobs:
|
|||
if: github.event_name != 'pull_request'
|
||||
shell: bash
|
||||
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: |
|
||||
if [[ "$STATUS" == "none" ]]; then
|
||||
echo "PR #${{ steps.create_pr.outputs.pull-request-number }} unchanged!" >> $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
|
||||
echo "PR #$PR_NUMBER $STATUS!" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "URL: $PR_URL" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
|
|
|||
23
.github/workflows/translations-push.yml
vendored
23
.github/workflows/translations-push.yml
vendored
|
|
@ -20,7 +20,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: "Checkout repo"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: "Install lupdate"
|
||||
shell: bash
|
||||
|
|
@ -29,12 +29,13 @@ jobs:
|
|||
sudo apt-get install -y --no-install-recommends qttools5-dev-tools
|
||||
|
||||
- name: "Update Cockatrice translation source"
|
||||
env:
|
||||
FILE: cockatrice/cockatrice_en@source.ts
|
||||
id: cockatrice
|
||||
shell: bash
|
||||
run: |
|
||||
FILE="cockatrice/cockatrice_en@source.ts"
|
||||
export DIRS="cockatrice/src $(find . -maxdepth 1 -type d -name 'libcockatrice_*')"
|
||||
FILE="$FILE" DIRS="$DIRS" .ci/update_translation_source_strings.sh
|
||||
run: >
|
||||
DIRS="cockatrice/src $(find . -maxdepth 1 -type d -name 'libcockatrice_*')"
|
||||
.ci/update_translation_source_strings.sh
|
||||
|
||||
- name: "Update Oracle translation source"
|
||||
id: oracle
|
||||
|
|
@ -77,11 +78,9 @@ jobs:
|
|||
if: github.event_name != 'pull_request'
|
||||
shell: bash
|
||||
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: |
|
||||
if [[ "$STATUS" == "none" ]]; then
|
||||
echo "PR #${{ steps.create_pr.outputs.pull-request-number }} unchanged!" >> $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
|
||||
echo "PR #$PR_NUMBER $STATUS!" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "URL: $PR_URL" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -6,6 +6,7 @@ mysql.cnf
|
|||
.DS_Store
|
||||
.idea/
|
||||
*.aps
|
||||
*.cache
|
||||
cmake-build*
|
||||
preferences
|
||||
compile_commands.json
|
||||
|
|
|
|||
|
|
@ -64,10 +64,11 @@ if(WIN32 OR USE_VCPKG)
|
|||
else()
|
||||
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(
|
||||
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()
|
||||
|
|
@ -174,7 +175,7 @@ elseif(CMAKE_COMPILER_IS_GNUCXX)
|
|||
-Wno-error=delete-non-virtual-dtor
|
||||
-Wno-error=sign-compare
|
||||
-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})
|
||||
|
|
@ -280,11 +281,7 @@ if(UNIX)
|
|||
if(CPACK_GENERATOR STREQUAL "RPM")
|
||||
set(CPACK_RPM_PACKAGE_LICENSE "GPLv2")
|
||||
set(CPACK_RPM_MAIN_COMPONENT "cockatrice")
|
||||
if(Qt6_FOUND)
|
||||
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_REQUIRES "protobuf, qt6-qttools, qt6-qtsvg, qt6-qtmultimedia, qt6-qtimageformats")
|
||||
set(CPACK_RPM_PACKAGE_GROUP "Amusements/Games")
|
||||
set(CPACK_RPM_PACKAGE_URL "http://github.com/Cockatrice/Cockatrice")
|
||||
# stop directories from making package conflicts
|
||||
|
|
@ -302,12 +299,8 @@ if(UNIX)
|
|||
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
|
||||
set(CPACK_DEBIAN_PACKAGE_SECTION "games")
|
||||
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_RECOMMENDS "libqt6sql6-mysql") # for connecting servatrice to a mysql db
|
||||
elseif(Qt5_FOUND)
|
||||
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt5multimedia5-plugins, libqt5svg5")
|
||||
endif()
|
||||
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt6multimedia6, libqt6svg6, qt6-qpa-plugins, qt6-image-formats-plugins")
|
||||
set(CPACK_DEBIAN_PACKAGE_RECOMMENDS "libqt6sql6-mysql") # for connecting servatrice to a mysql db
|
||||
endif()
|
||||
endif()
|
||||
elseif(WIN32)
|
||||
|
|
|
|||
8
Doxyfile
8
Doxyfile
|
|
@ -349,7 +349,7 @@ OPTIMIZE_OUTPUT_SLICE = NO
|
|||
#
|
||||
# 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
|
||||
# according to the Markdown format, which allows for more readable
|
||||
|
|
@ -1086,7 +1086,8 @@ FILE_PATTERNS = *.cc \
|
|||
*.h++ \
|
||||
*.markdown \
|
||||
*.md \
|
||||
*.dox
|
||||
*.dox \
|
||||
*.proto
|
||||
|
||||
# The RECURSIVE tag can be used to specify whether or not subdirectories should
|
||||
# be searched for input files as well.
|
||||
|
|
@ -1103,6 +1104,7 @@ RECURSIVE = YES
|
|||
|
||||
EXCLUDE = build/ \
|
||||
cmake/ \
|
||||
cmake-build-debug/ \
|
||||
doc/doxygen/theme/docs/ \
|
||||
doc/doxygen/theme/include/ \
|
||||
vcpkg/
|
||||
|
|
@ -1195,7 +1197,7 @@ INPUT_FILTER =
|
|||
# need to set EXTENSION_MAPPING for the extension otherwise the files are not
|
||||
# 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
|
||||
# INPUT_FILTER) will also be used to filter the input files that are used for
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
| `-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 |
|
||||
| `-DFORCE_USE_QT5=1` | Skip looking for Qt6 before trying to find Qt5 |
|
||||
|
||||
|
||||
# Run
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
# 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: QT5_DIR -- Hint as to where Qt5 lives on the system
|
||||
# Output: COCKATRICE_QT_VERSION_NAME -- Example values: Qt5, Qt6
|
||||
# Output: COCKATRICE_QT_VERSION_NAME -- Example values: Qt6
|
||||
# Output: SERVATRICE_QT_MODULES
|
||||
# Output: COCKATRICE_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)
|
||||
|
||||
if(NOT FORCE_USE_QT5)
|
||||
# Linguist is now a component in Qt6 instead of an external package
|
||||
find_package(
|
||||
Qt6 6.4.2
|
||||
COMPONENTS ${REQUIRED_QT_COMPONENTS} Linguist
|
||||
QUIET HINTS ${Qt6_DIR}
|
||||
)
|
||||
# Linguist is now a component in Qt6 instead of an external package
|
||||
find_package(
|
||||
Qt6 6.4.2
|
||||
COMPONENTS ${REQUIRED_QT_COMPONENTS} Linguist
|
||||
QUIET HINTS ${Qt6_DIR}
|
||||
)
|
||||
if(NOT Qt6_FOUND)
|
||||
message(FATAL_ERROR "No suitable version of Qt was found")
|
||||
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)
|
||||
if(QT6_LRELEASE_INDEX EQUAL -1)
|
||||
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()
|
||||
list(FIND Qt6LinguistTools_TARGETS Qt6::lrelease QT6_LRELEASE_INDEX)
|
||||
if(QT6_LRELEASE_INDEX EQUAL -1)
|
||||
message(WARNING "Qt6 lrelease not found.")
|
||||
endif()
|
||||
|
||||
if(Qt5_POSITION_INDEPENDENT_CODE OR Qt6_FOUND)
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
list(FIND Qt6LinguistTools_TARGETS Qt6::lupdate QT6_LUPDATE_INDEX)
|
||||
if(QT6_LUPDATE_INDEX EQUAL -1)
|
||||
message(WARNING "Qt6 lupdate not found.")
|
||||
endif()
|
||||
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
|
||||
# Establish Qt Plugins directory & Library directories
|
||||
get_target_property(QT_LIBRARY_DIR ${COCKATRICE_QT_VERSION_NAME}::Core LOCATION)
|
||||
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_LIBRARY_DIR "${QT_LIBRARY_DIR}/../../.." ABSOLUTE)
|
||||
if(UNIX AND APPLE)
|
||||
# Mac needs a bit more help finding all necessary components
|
||||
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)
|
||||
get_filename_component(QT_PLUGINS_DIR "${Qt6Core_DIR}/../../../${QT6_INSTALL_PLUGINS}" ABSOLUTE)
|
||||
get_filename_component(QT_LIBRARY_DIR "${QT_LIBRARY_DIR}/../../.." ABSOLUTE)
|
||||
if(UNIX AND APPLE)
|
||||
# Mac needs a bit more help finding all necessary components
|
||||
list(APPEND QT_LIBRARY_DIR "/usr/local/lib")
|
||||
endif()
|
||||
message(DEBUG "QT_PLUGINS_DIR = ${QT_PLUGINS_DIR}")
|
||||
message(DEBUG "QT_LIBRARY_DIR = ${QT_LIBRARY_DIR}")
|
||||
|
|
|
|||
|
|
@ -1,38 +1,118 @@
|
|||
<?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">
|
||||
<dict>
|
||||
|
||||
<!-- ========================= -->
|
||||
<!-- CORE BUNDLE METADATA -->
|
||||
<!-- ========================= -->
|
||||
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>English</string>
|
||||
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
|
||||
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>${MACOSX_BUNDLE_INFO_STRING}</string>
|
||||
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>${MACOSX_BUNDLE_ICON_FILE}</string>
|
||||
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
|
||||
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
|
||||
<key>CFBundleLongVersionString</key>
|
||||
<string>${MACOSX_BUNDLE_LONG_VERSION_STRING}</string>
|
||||
|
||||
<key>CFBundleName</key>
|
||||
<string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
|
||||
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
|
||||
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
|
||||
<key>CSResourcesFileMapped</key>
|
||||
<true/>
|
||||
<key>LSRequiresCarbon</key>
|
||||
<true/>
|
||||
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>${MACOSX_BUNDLE_COPYRIGHT}</string>
|
||||
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<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>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -117,21 +117,22 @@ ${If} $InstDir == ""
|
|||
; we need to set a default based on the install mode
|
||||
StrCpy $InstDir $0
|
||||
${EndIf}
|
||||
Call SetModeDestinationFromInstdir
|
||||
|
||||
; --- Detect portable install when using /R ---
|
||||
; --- Detect portable install when using /R (must come BEFORE SetModeDestinationFromInstdir) ---
|
||||
${If} $ReinstallMode = 1
|
||||
IfFileExists "$InstDir\portable.dat" 0 not_portable
|
||||
StrCpy $PortableMode 1
|
||||
Goto portable_done
|
||||
|
||||
not_portable:
|
||||
StrCpy $PortableMode 0
|
||||
|
||||
portable_done:
|
||||
${EndIf}
|
||||
|
||||
; Now that $PortableMode reflects reality, commit InstDir into the correct slot
|
||||
Call SetModeDestinationFromInstdir
|
||||
|
||||
${If} $ReinstallMode = 1
|
||||
${AndIf} $PortableMode = 0
|
||||
Call AutoUninstallIfNeeded
|
||||
${EndIf}
|
||||
|
||||
|
|
|
|||
|
|
@ -97,7 +97,11 @@ set(cockatrice_SOURCES
|
|||
src/game_graphics/player/menu/rfg_menu.cpp
|
||||
src/game_graphics/player/menu/say_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/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_graphics/player/player_area.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_work.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/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/reversed_completer_model.cpp
|
||||
src/interface/pixel_map_generator.cpp
|
||||
src/interface/theme_config.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_popup_widget.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_widget.cpp
|
||||
src/interface/widgets/server/chat_view/chat_view.cpp
|
||||
src/interface/widgets/server/game_filter_configs.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/remote/remote_decklist_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_info_box.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_painter.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/deck_editor_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/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/sound_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/window_main.cpp
|
||||
src/main.cpp
|
||||
src/single_instance_manager.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/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_listings_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/average_deck/edhrec_average_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_account.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_storage.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/key_signals.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.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.h
|
||||
src/interface/widgets/utility/compact_push_button.cpp
|
||||
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)
|
||||
|
|
@ -390,11 +441,7 @@ if(APPLE)
|
|||
set(cockatrice_SOURCES ${cockatrice_SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/resources/appicon.icns)
|
||||
endif(APPLE)
|
||||
|
||||
if(Qt6_FOUND)
|
||||
qt6_add_resources(cockatrice_RESOURCES_RCC ${cockatrice_RESOURCES})
|
||||
elseif(Qt5_FOUND)
|
||||
qt5_add_resources(cockatrice_RESOURCES_RCC ${cockatrice_RESOURCES})
|
||||
endif()
|
||||
qt6_add_resources(cockatrice_RESOURCES_RCC ${cockatrice_RESOURCES})
|
||||
|
||||
# Declare path variables
|
||||
set(ICONDIR
|
||||
|
|
@ -406,71 +453,37 @@ set(DESKTOPDIR
|
|||
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_UNIX_QM_INSTALL_DIR "share/cockatrice/translations")
|
||||
set(COCKATRICE_WIN32_QM_INSTALL_DIR "translations")
|
||||
|
||||
if(Qt6_FOUND)
|
||||
qt6_add_executable(
|
||||
cockatrice
|
||||
WIN32
|
||||
MACOSX_BUNDLE
|
||||
${cockatrice_SOURCES}
|
||||
${cockatrice_RESOURCES_RCC}
|
||||
${cockatrice_MOC_SRCS}
|
||||
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()
|
||||
qt6_add_executable(
|
||||
cockatrice
|
||||
WIN32
|
||||
MACOSX_BUNDLE
|
||||
${cockatrice_SOURCES}
|
||||
${cockatrice_RESOURCES_RCC}
|
||||
${cockatrice_MOC_SRCS}
|
||||
MANUAL_FINALIZATION
|
||||
)
|
||||
|
||||
if(Qt5_FOUND)
|
||||
target_link_libraries(
|
||||
cockatrice
|
||||
libcockatrice_card
|
||||
libcockatrice_deck_list
|
||||
libcockatrice_filters
|
||||
libcockatrice_utility
|
||||
libcockatrice_network
|
||||
libcockatrice_models
|
||||
libcockatrice_rng
|
||||
libcockatrice_settings
|
||||
${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()
|
||||
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}
|
||||
)
|
||||
|
||||
if(UNIX)
|
||||
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.svg DESTINATION ${ICONDIR}/hicolor/scalable/apps)
|
||||
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()
|
||||
elseif(WIN32)
|
||||
install(TARGETS cockatrice RUNTIME DESTINATION ./)
|
||||
|
|
@ -500,7 +530,7 @@ if(APPLE)
|
|||
set(plugin_dest_dir cockatrice.app/Contents/Plugins)
|
||||
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(
|
||||
DIRECTORY "${QT_PLUGINS_DIR}/"
|
||||
DESTINATION ${plugin_dest_dir}
|
||||
|
|
@ -567,7 +597,7 @@ if(WIN32)
|
|||
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(
|
||||
DIRECTORY "${QT_PLUGINS_DIR}/"
|
||||
DESTINATION ${plugin_dest_dir}
|
||||
|
|
@ -623,7 +653,7 @@ Data = Resources\")
|
|||
endif()
|
||||
endif()
|
||||
|
||||
if(Qt6_FOUND AND Qt6LinguistTools_FOUND)
|
||||
if(Qt6LinguistTools_FOUND)
|
||||
#Qt6 Translations happen after the executable is built up
|
||||
if(UPDATE_TRANSLATIONS)
|
||||
qt6_add_translations(
|
||||
|
|
@ -650,6 +680,4 @@ if(Qt6_FOUND AND Qt6LinguistTools_FOUND)
|
|||
endif()
|
||||
endif()
|
||||
|
||||
if(Qt6_FOUND)
|
||||
qt6_finalize_target(cockatrice)
|
||||
endif()
|
||||
qt6_finalize_target(cockatrice)
|
||||
|
|
|
|||
7
cockatrice/cockatrice-cod.xml
Normal file
7
cockatrice/cockatrice-cod.xml
Normal 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>
|
||||
|
|
@ -3,6 +3,8 @@
|
|||
Version=1.0
|
||||
Type=Application
|
||||
Name=Cockatrice
|
||||
Exec=cockatrice
|
||||
Exec=cockatrice %U
|
||||
Icon=cockatrice
|
||||
Categories=Game;CardGame;
|
||||
MimeType=application/x-cockatrice;
|
||||
X-Scheme-Handler/cockatrice=true
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -14,6 +14,7 @@
|
|||
#include <QThread>
|
||||
#include <libcockatrice/network/client/remote/remote_client.h>
|
||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||
#include <libcockatrice/settings/servers_settings.h>
|
||||
|
||||
ConnectionController::ConnectionController(QWidget *dialogParent, QObject *parent)
|
||||
: QObject(parent), dialogParent(dialogParent)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@
|
|||
#include <QtConcurrent>
|
||||
#include <libcockatrice/card/database/card_database.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>
|
||||
|
||||
#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)
|
||||
{
|
||||
isSpoilerDownloadEnabled = SettingsCache::instance().getDownloadSpoilersStatus();
|
||||
isSpoilerDownloadEnabled = SettingsCache::instance().downloads().getDownloadSpoilersStatus();
|
||||
if (isSpoilerDownloadEnabled) {
|
||||
// Start the process of checking if we're in spoiler season
|
||||
// File exists means we're in spoiler season
|
||||
|
|
@ -75,7 +77,7 @@ void SpoilerBackgroundUpdater::actDownloadFinishedSpoilersFile()
|
|||
|
||||
bool SpoilerBackgroundUpdater::deleteSpoilerFile()
|
||||
{
|
||||
QString fileName = SettingsCache::instance().getSpoilerCardDatabasePath();
|
||||
QString fileName = SettingsCache::instance().paths().getSpoilerCardDatabasePath();
|
||||
QFileInfo fi(fileName);
|
||||
QDir fileDir(fi.path());
|
||||
QFile file(fileName);
|
||||
|
|
@ -126,7 +128,7 @@ void SpoilerBackgroundUpdater::actCheckIfSpoilerSeasonEnabled()
|
|||
|
||||
bool SpoilerBackgroundUpdater::saveDownloadedFile(QByteArray data)
|
||||
{
|
||||
QString fileName = SettingsCache::instance().getSpoilerCardDatabasePath();
|
||||
QString fileName = SettingsCache::instance().paths().getSpoilerCardDatabasePath();
|
||||
QFileInfo fi(fileName);
|
||||
QDir fileDir(fi.path());
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -5,7 +5,7 @@
|
|||
#include <QtMath>
|
||||
|
||||
CardCounterSettings::CardCounterSettings(const QString &settingsPath, QObject *parent)
|
||||
: SettingsManager(settingsPath + "global.ini", "cards", "counters", parent)
|
||||
: SettingsManager(settingsPath + "card_counters.ini", "cards", "counters", parent)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -223,6 +223,10 @@ private:
|
|||
{"TabDeckEditor/aLoadDeck", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Load Deck..."),
|
||||
parseSequenceString("Ctrl+O"),
|
||||
ShortcutGroup::Deck_Editor)},
|
||||
{"TabDeckEditor/aLoadDeckFromWebsite",
|
||||
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Load deck from online service..."),
|
||||
parseSequenceString("Ctrl+Shift+O"),
|
||||
ShortcutGroup::Deck_Editor)},
|
||||
{"TabDeckEditor/aLoadDeckFromClipboard",
|
||||
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Load Deck from Clipboard..."),
|
||||
parseSequenceString("Ctrl+Shift+V"),
|
||||
|
|
@ -283,6 +287,10 @@ private:
|
|||
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Load Deck from Clipboard..."),
|
||||
parseSequenceString("Ctrl+Shift+V"),
|
||||
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"),
|
||||
parseSequenceString("Ctrl+Alt+U"),
|
||||
ShortcutGroup::Game_Lobby)},
|
||||
|
|
|
|||
|
|
@ -2,12 +2,11 @@
|
|||
|
||||
#include "settings/cache_settings.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QAudioOutput>
|
||||
#include <QDir>
|
||||
#include <QMediaPlayer>
|
||||
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
|
||||
#include <QAudioOutput>
|
||||
#endif
|
||||
#include <libcockatrice/settings/sound_settings.h>
|
||||
|
||||
#define DEFAULT_THEME_NAME "Default"
|
||||
#define TEST_SOUND_FILENAME "player_join"
|
||||
|
|
@ -15,8 +14,10 @@
|
|||
SoundEngine::SoundEngine(QObject *parent) : QObject(parent), audioOutput(nullptr), player(nullptr)
|
||||
{
|
||||
ensureThemeDirectoryExists();
|
||||
connect(&SettingsCache::instance(), &SettingsCache::soundThemeChanged, this, &SoundEngine::themeChangedSlot);
|
||||
connect(&SettingsCache::instance(), &SettingsCache::soundEnabledChanged, this, &SoundEngine::soundEnabledChanged);
|
||||
connect(&SettingsCache::instance().sound(), &SoundSettings::soundThemeChanged, this,
|
||||
&SoundEngine::themeChangedSlot);
|
||||
connect(&SettingsCache::instance().sound(), &SoundSettings::soundEnabledChanged, this,
|
||||
&SoundEngine::soundEnabledChanged);
|
||||
|
||||
soundEnabledChanged();
|
||||
themeChangedSlot();
|
||||
|
|
@ -36,14 +37,12 @@ SoundEngine::~SoundEngine()
|
|||
|
||||
void SoundEngine::soundEnabledChanged()
|
||||
{
|
||||
if (SettingsCache::instance().getSoundEnabled()) {
|
||||
if (SettingsCache::instance().sound().getSoundEnabled()) {
|
||||
qCInfo(SoundEngineLog) << "SoundEngine: enabling sound with" << audioData.size() << "sounds";
|
||||
if (!player) {
|
||||
player = new QMediaPlayer;
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
|
||||
audioOutput = new QAudioOutput(player);
|
||||
player->setAudioOutput(audioOutput);
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
qCInfo(SoundEngineLog) << "SoundEngine: disabling sound";
|
||||
|
|
@ -70,14 +69,9 @@ void SoundEngine::playSound(const QString &fileName)
|
|||
}
|
||||
|
||||
player->stop();
|
||||
int volumeSliderValue = SettingsCache::instance().getMasterVolume();
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
|
||||
int volumeSliderValue = SettingsCache::instance().sound().getMasterVolume();
|
||||
player->audioOutput()->setVolume(qreal(volumeSliderValue) / 100);
|
||||
player->setSource(QUrl::fromLocalFile(audioData[fileName]));
|
||||
#else
|
||||
player->setVolume(volumeSliderValue);
|
||||
player->setMedia(QUrl::fromLocalFile(audioData[fileName]));
|
||||
#endif
|
||||
player->play();
|
||||
}
|
||||
|
||||
|
|
@ -88,10 +82,10 @@ void SoundEngine::testSound()
|
|||
|
||||
void SoundEngine::ensureThemeDirectoryExists()
|
||||
{
|
||||
if (SettingsCache::instance().getSoundThemeName().isEmpty() ||
|
||||
!getAvailableThemes().contains(SettingsCache::instance().getSoundThemeName())) {
|
||||
if (SettingsCache::instance().sound().getSoundThemeName().isEmpty() ||
|
||||
!getAvailableThemes().contains(SettingsCache::instance().sound().getSoundThemeName())) {
|
||||
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()
|
||||
{
|
||||
QString themeName = SettingsCache::instance().getSoundThemeName();
|
||||
QString themeName = SettingsCache::instance().sound().getSoundThemeName();
|
||||
qCInfo(SoundEngineLog) << "Sound theme changed:" << themeName;
|
||||
|
||||
QDir dir = getAvailableThemes().value(themeName);
|
||||
|
|
|
|||
69
cockatrice/src/client/url_scheme_event_filter.h
Normal file
69
cockatrice/src/client/url_scheme_event_filter.h
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
@ -3,6 +3,8 @@
|
|||
#include "../../client/settings/cache_settings.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
|
||||
{
|
||||
|
|
@ -14,7 +16,7 @@ public:
|
|||
|
||||
[[nodiscard]] bool getIncludeRebalancedCards() const override
|
||||
{
|
||||
return SettingsCache::instance().getIncludeRebalancedCards();
|
||||
return SettingsCache::instance().cardsDisplay().getIncludeRebalancedCards();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -52,18 +52,14 @@ static void setupParserRules()
|
|||
|
||||
search["Start"] = passthru;
|
||||
search["QueryPartList"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) {
|
||||
auto matchesFilter = [&deck, &info](const std::any &query) {
|
||||
return std::any_cast<DeckFilter>(query)(deck, info);
|
||||
};
|
||||
return [=](const DeckSearchData &data) {
|
||||
auto matchesFilter = [&data](const std::any &query) { return std::any_cast<DeckFilter>(query)(data); };
|
||||
return std::all_of(sv.begin(), sv.end(), matchesFilter);
|
||||
};
|
||||
};
|
||||
search["ComplexQueryPart"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) {
|
||||
auto matchesFilter = [&deck, &info](const std::any &query) {
|
||||
return std::any_cast<DeckFilter>(query)(deck, info);
|
||||
};
|
||||
return [=](const DeckSearchData &data) {
|
||||
auto matchesFilter = [&data](const std::any &query) { return std::any_cast<DeckFilter>(query)(data); };
|
||||
return std::any_of(sv.begin(), sv.end(), matchesFilter);
|
||||
};
|
||||
};
|
||||
|
|
@ -71,9 +67,7 @@ static void setupParserRules()
|
|||
search["QueryPart"] = passthru;
|
||||
search["NotQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||
const auto dependent = std::any_cast<DeckFilter>(sv[0]);
|
||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) -> bool {
|
||||
return !dependent(deck, info);
|
||||
};
|
||||
return [=](const DeckSearchData &data) -> bool { return !dependent(data); };
|
||||
};
|
||||
|
||||
search["String"] = [](const peg::SemanticValues &sv) -> QString {
|
||||
|
|
@ -125,9 +119,9 @@ static void setupParserRules()
|
|||
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; };
|
||||
|
||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) -> bool {
|
||||
return [=](const DeckSearchData &data) -> bool {
|
||||
int count = 0;
|
||||
auto cardNodes = deck->deckLoader->getDeck().deckList.getCardNodes();
|
||||
auto cardNodes = data.deck->deckList.getCardNodes();
|
||||
for (auto node : cardNodes) {
|
||||
auto cardInfoPtr = CardDatabaseManager::query()->getCardInfo(node->getName());
|
||||
if (!cardInfoPtr.isNull() && cardFilter.check(cardInfoPtr)) {
|
||||
|
|
@ -146,53 +140,49 @@ static void setupParserRules()
|
|||
|
||||
search["DeckNameQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||
auto name = std::any_cast<QString>(sv[0]);
|
||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) {
|
||||
return deck->deckLoader->getDeck().deckList.getName().contains(name, Qt::CaseInsensitive);
|
||||
return [=](const DeckSearchData &data) {
|
||||
return data.deck->deckList.getName().contains(name, Qt::CaseInsensitive);
|
||||
};
|
||||
};
|
||||
|
||||
search["FileNameQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||
auto name = std::any_cast<QString>(sv[0]);
|
||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) {
|
||||
auto filename = QFileInfo(deck->filePath).fileName();
|
||||
return [=](const DeckSearchData &data) {
|
||||
auto filename = QFileInfo(data.filePath).fileName();
|
||||
return filename.contains(name, Qt::CaseInsensitive);
|
||||
};
|
||||
};
|
||||
|
||||
search["PathQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||
auto name = std::any_cast<QString>(sv[0]);
|
||||
return [=](const DeckPreviewWidget *, const ExtraDeckSearchInfo &info) {
|
||||
return info.relativeFilePath.contains(name, Qt::CaseInsensitive);
|
||||
};
|
||||
return [=](const DeckSearchData &data) { return data.relativeFilePath.contains(name, Qt::CaseInsensitive); };
|
||||
};
|
||||
|
||||
search["FormatQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||
auto format = std::any_cast<QString>(sv[0]);
|
||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) {
|
||||
auto gameFormat = deck->deckLoader->getDeck().deckList.getGameFormat();
|
||||
return [=](const DeckSearchData &data) {
|
||||
auto gameFormat = data.deck->deckList.getGameFormat();
|
||||
return QString::compare(format, gameFormat, Qt::CaseInsensitive) == 0;
|
||||
};
|
||||
};
|
||||
|
||||
search["CommentQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||
auto value = std::any_cast<QString>(sv[0]);
|
||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) {
|
||||
auto comments = deck->deckLoader->getDeck().deckList.getComments();
|
||||
return [=](const DeckSearchData &data) {
|
||||
auto comments = data.deck->deckList.getComments();
|
||||
return comments.contains(value, Qt::CaseInsensitive);
|
||||
};
|
||||
};
|
||||
|
||||
search["GenericQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||
auto name = std::any_cast<QString>(sv[0]);
|
||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) {
|
||||
return deck->getDisplayName().contains(name, Qt::CaseInsensitive);
|
||||
};
|
||||
return [=](const DeckSearchData &data) { return data.displayName.contains(name, Qt::CaseInsensitive); };
|
||||
};
|
||||
}
|
||||
|
||||
DeckFilterString::DeckFilterString()
|
||||
{
|
||||
filter = [](const DeckPreviewWidget *, const ExtraDeckSearchInfo &) { return false; };
|
||||
filter = [](const DeckSearchData &) { return false; };
|
||||
_error = "Not initialized";
|
||||
}
|
||||
|
||||
|
|
@ -205,7 +195,7 @@ DeckFilterString::DeckFilterString(const QString &expr)
|
|||
_error = QString();
|
||||
|
||||
if (ba.isEmpty()) {
|
||||
filter = [](const DeckPreviewWidget *, const ExtraDeckSearchInfo &) { return true; };
|
||||
filter = [](const DeckSearchData &) { return true; };
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -215,6 +205,6 @@ DeckFilterString::DeckFilterString(const QString &expr)
|
|||
|
||||
if (!search.parse(ba.data(), filter)) {
|
||||
qCInfo(DeckFilterStringLog).nospace() << "DeckFilterString error for " << expr << "; " << qPrintable(_error);
|
||||
filter = [](const DeckPreviewWidget *, const ExtraDeckSearchInfo &) { return false; };
|
||||
filter = [](const DeckSearchData &) { return false; };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
#ifndef 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 <QString>
|
||||
|
|
@ -16,26 +16,29 @@
|
|||
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
|
||||
{
|
||||
/**
|
||||
* The relative filepath starting from the deck folder
|
||||
*/
|
||||
QString relativeFilePath;
|
||||
const LoadedDeck *deck = nullptr; ///< The loaded deck. Must not be null.
|
||||
QString filePath; ///< Absolute path of the deck file.
|
||||
QString displayName; ///< Deck name, or the file name if the deck has no name.
|
||||
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
|
||||
{
|
||||
public:
|
||||
DeckFilterString();
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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->setSpectatorsOmniscient(true);
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ public:
|
|||
|
||||
AbstractClient *getClientForPlayer(int playerId) const;
|
||||
|
||||
void loadReplay(GameReplay *replay);
|
||||
void loadReplay(const GameReplay *replay);
|
||||
|
||||
CardItem *getCard(int playerId, const QString &zoneName, int cardId) const;
|
||||
|
||||
|
|
|
|||
|
|
@ -287,6 +287,9 @@ void GameEventHandler::eventGameStateChanged(const Event_GameStateChanged &event
|
|||
if (!game->getGameMetaInfo()->proto().share_decklists_on_load()) {
|
||||
continue;
|
||||
}
|
||||
if (!playerInfo.has_deck_list()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
opponentDecksToDisplay.append(
|
||||
qMakePair(playerId, qMakePair(playerName, QString::fromStdString(playerInfo.deck_list()))));
|
||||
|
|
|
|||
|
|
@ -1,8 +1,16 @@
|
|||
/**
|
||||
* @file game_event_handler.h
|
||||
* @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
|
||||
#define COCKATRICE_GAME_EVENT_HANDLER_H
|
||||
|
|
@ -15,92 +23,310 @@
|
|||
#include <libcockatrice/protocol/pb/serverinfo_player.pb.h>
|
||||
|
||||
class AbstractClient;
|
||||
class Response;
|
||||
class AbstractGame;
|
||||
class CommandContainer;
|
||||
class GameCommand;
|
||||
class GameEventContainer;
|
||||
class GameEventContext;
|
||||
class GameCommand;
|
||||
class GameState;
|
||||
class MessageLogWidget;
|
||||
class CommandContainer;
|
||||
class Event_GameJoined;
|
||||
class PendingCommand;
|
||||
class PlayerLogic;
|
||||
class Response;
|
||||
|
||||
class Event_GameStateChanged;
|
||||
class Event_PlayerPropertiesChanged;
|
||||
class Event_Join;
|
||||
class Event_Leave;
|
||||
class Event_GameHostChanged;
|
||||
class Event_GameClosed;
|
||||
class Event_GameStart;
|
||||
class Event_SetActivePlayer;
|
||||
class Event_SetActivePhase;
|
||||
class Event_Ping;
|
||||
class Event_GameSay;
|
||||
class Event_Kicked;
|
||||
class Event_ReverseTurn;
|
||||
class AbstractGame;
|
||||
class PendingCommand;
|
||||
class PlayerLogic;
|
||||
class Event_Ping;
|
||||
|
||||
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
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
/** Pointer to the owning game instance. */
|
||||
AbstractGame *game;
|
||||
|
||||
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);
|
||||
|
||||
/** @} */
|
||||
|
||||
/** @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();
|
||||
|
||||
/** @brief Request reversing the current turn order. */
|
||||
void handleReverseTurn();
|
||||
|
||||
/** @brief Concede the game for the currently active local player. */
|
||||
void handleActiveLocalPlayerConceded();
|
||||
|
||||
/** @brief Undo a previous concede for the active local player. */
|
||||
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);
|
||||
|
||||
/** @brief Leave the current game session. */
|
||||
void handleGameLeft();
|
||||
|
||||
/**
|
||||
* @brief Send a chat message to all players and spectators.
|
||||
*
|
||||
* @param chatMessage Message text.
|
||||
*/
|
||||
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 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);
|
||||
|
||||
/**
|
||||
* @brief Handle a spectator leaving the game.
|
||||
*/
|
||||
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);
|
||||
|
||||
/**
|
||||
* @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);
|
||||
|
||||
/** @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,
|
||||
int eventPlayerId,
|
||||
const GameEventContext &context);
|
||||
|
||||
/** @brief Handle a player or spectator joining the game. */
|
||||
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);
|
||||
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 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);
|
||||
void eventPing(const Event_Ping &event, int eventPlayerId, const GameEventContext &context);
|
||||
void eventReverseTurn(const Event_ReverseTurn &event, int eventPlayerId, const GameEventContext & /*context*/);
|
||||
/**
|
||||
* @brief Convert a leave reason enum to a human-readable string.
|
||||
*/
|
||||
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:
|
||||
/** @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);
|
||||
|
||||
/**
|
||||
* @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);
|
||||
|
||||
/**
|
||||
* @brief Called when a PendingCommand finishes execution.
|
||||
*
|
||||
* Used to detect server-side errors such as chat flood protection.
|
||||
*/
|
||||
void commandFinished(const Response &response);
|
||||
|
||||
/** @} */
|
||||
|
||||
signals:
|
||||
/** @name Core state signals
|
||||
* @{
|
||||
*/
|
||||
|
||||
void emitUserEvent();
|
||||
void containerProcessingStarted(GameEventContext context);
|
||||
void containerProcessingDone();
|
||||
void gameFlooded();
|
||||
void setContextJudgeName(QString judgeName);
|
||||
|
||||
/** @} */
|
||||
|
||||
/** @name Player and spectator signals
|
||||
* @{
|
||||
*/
|
||||
|
||||
void addPlayerToAutoCompleteList(QString playerName);
|
||||
void localPlayerDeckSelected(PlayerLogic *localPlayer, int playerId, ServerInfo_Player playerInfo);
|
||||
void remotePlayerDeckSelected(QString deckList, int playerId, QString playerName);
|
||||
void remotePlayersDecksSelected(QVector<QPair<int, QPair<QString, QString>>> opponentDecks);
|
||||
void localPlayerSideboardLocked(int playerId, bool sideboardLocked);
|
||||
void localPlayerReadyStateChanged(int playerId, bool ready);
|
||||
|
||||
/** @} */
|
||||
|
||||
/** @name Game flow signals
|
||||
* @{
|
||||
*/
|
||||
|
||||
void gameStopped();
|
||||
void gameClosed();
|
||||
void playerPropertiesChanged(const ServerInfo_PlayerProperties &prop, int playerId);
|
||||
|
|
@ -109,11 +335,15 @@ signals:
|
|||
void playerKicked();
|
||||
void spectatorJoined(const ServerInfo_PlayerProperties &spectatorInfo);
|
||||
void spectatorLeft(int leavingSpectatorId);
|
||||
void gameFlooded();
|
||||
void containerProcessingStarted(GameEventContext context);
|
||||
void setContextJudgeName(QString judgeName);
|
||||
void containerProcessingDone();
|
||||
void arrowDeleted(int creatorId, int arrowId);
|
||||
|
||||
/** @} */
|
||||
|
||||
/** @name Logging signals
|
||||
* Signals consumed by MessageLogWidget.
|
||||
* @{
|
||||
*/
|
||||
|
||||
void logSpectatorSay(ServerInfo_User userInfo, QString message);
|
||||
void logSpectatorLeave(QString name, QString reason);
|
||||
void logGameStart();
|
||||
|
|
@ -132,6 +362,8 @@ signals:
|
|||
void logActivePhaseChanged(int activePhase);
|
||||
void logConcede(int playerId);
|
||||
void logUnconcede(int playerId);
|
||||
|
||||
/** @} */
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_GAME_EVENT_HANDLER_H
|
||||
|
|
|
|||
|
|
@ -27,8 +27,11 @@
|
|||
#include <libcockatrice/protocol/pb/command_shuffle.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_undo_draw.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/trice_limits.h>
|
||||
#include <libcockatrice/utility/zone_names.h>
|
||||
|
||||
// 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();
|
||||
|
||||
int tableRow = info.getUiAttributes().tableRow;
|
||||
bool playToStack = SettingsCache::instance().getPlayToStack();
|
||||
bool playToStack = SettingsCache::instance().userInterface().getPlayToStack();
|
||||
QString currentZone = card->getZone()->getName();
|
||||
if (!faceDown && currentZone == ZoneNames::STACK && tableRow == 3) {
|
||||
cmd.set_target_zone(ZoneNames::GRAVE);
|
||||
|
|
@ -309,7 +312,7 @@ void PlayerActions::actDrawCard()
|
|||
|
||||
void PlayerActions::actRequestMulliganDialog()
|
||||
{
|
||||
int startSize = SettingsCache::instance().getStartingHandSize();
|
||||
int startSize = SettingsCache::instance().userInterface().getStartingHandSize();
|
||||
int handSize = player->getHandZone()->getCards().size();
|
||||
int deckSize = player->getDeckZone()->getCards().size() + handSize;
|
||||
|
||||
|
|
@ -325,7 +328,7 @@ void PlayerActions::actMulligan(int number)
|
|||
}
|
||||
|
||||
doMulligan(number);
|
||||
SettingsCache::instance().setStartingHandSize(number);
|
||||
SettingsCache::instance().userInterface().setStartingHandSize(number);
|
||||
}
|
||||
|
||||
void PlayerActions::actMulliganSameSize()
|
||||
|
|
@ -882,7 +885,8 @@ void PlayerActions::actCreateToken(TokenInfo tokenToCreate)
|
|||
ExactCard correctedCard = CardDatabaseManager::query()->guessCard({lastTokenInfo.name, lastTokenInfo.providerId});
|
||||
if (correctedCard) {
|
||||
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()) {
|
||||
lastTokenInfo.pt = correctedCard.getInfo().getPowTough();
|
||||
}
|
||||
|
|
@ -928,13 +932,13 @@ void PlayerActions::setLastTokenInfo(CardInfoPtr cardInfo)
|
|||
return;
|
||||
}
|
||||
|
||||
lastTokenInfo = {.name = cardInfo->getName(),
|
||||
.color = cardInfo->getColors().isEmpty() ? QString() : cardInfo->getColors().left(1).toLower(),
|
||||
.pt = cardInfo->getPowTough(),
|
||||
.annotation = SettingsCache::instance().getAnnotateTokens() ? cardInfo->getText() : "",
|
||||
.destroy = true,
|
||||
.providerId =
|
||||
SettingsCache::instance().cardOverrides().getCardPreferenceOverride(cardInfo->getName())};
|
||||
lastTokenInfo = {
|
||||
.name = cardInfo->getName(),
|
||||
.color = cardInfo->getColors().isEmpty() ? QString() : cardInfo->getColors().left(1).toLower(),
|
||||
.pt = cardInfo->getPowTough(),
|
||||
.annotation = SettingsCache::instance().userInterface().getAnnotateTokens() ? cardInfo->getText() : "",
|
||||
.destroy = true,
|
||||
.providerId = SettingsCache::instance().cardOverrides().getCardPreferenceOverride(cardInfo->getName())};
|
||||
|
||||
lastTokenTableRow = TableZone::tableRowToGridY(cardInfo->getUiAttributes().tableRow);
|
||||
|
||||
|
|
@ -1017,8 +1021,9 @@ void PlayerActions::actCreateAllRelatedCards()
|
|||
if (!cardRelationAll->getDoesAttach() && !cardRelationAll->getIsVariable()) {
|
||||
dbName = cardRelationAll->getName();
|
||||
bool persistent = cardRelationAll->getIsPersistent();
|
||||
bool faceDown = cardRelationAll->getIsFaceDown();
|
||||
for (int i = 0; i < cardRelationAll->getDefaultCount(); ++i) {
|
||||
createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent);
|
||||
createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent, faceDown);
|
||||
}
|
||||
++tokensTypesCreated;
|
||||
if (tokensTypesCreated == 1) {
|
||||
|
|
@ -1033,8 +1038,9 @@ void PlayerActions::actCreateAllRelatedCards()
|
|||
if (!cardRelationNotExcluded->getDoesAttach() && !cardRelationNotExcluded->getIsVariable()) {
|
||||
dbName = cardRelationNotExcluded->getName();
|
||||
bool persistent = cardRelationNotExcluded->getIsPersistent();
|
||||
bool faceDown = cardRelationNotExcluded->getIsFaceDown();
|
||||
for (int i = 0; i < cardRelationNotExcluded->getDefaultCount(); ++i) {
|
||||
createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent);
|
||||
createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent, faceDown);
|
||||
}
|
||||
++tokensTypesCreated;
|
||||
if (tokensTypesCreated == 1) {
|
||||
|
|
@ -1072,6 +1078,7 @@ bool PlayerActions::createRelatedFromRelation(const CardItem *sourceCard,
|
|||
|
||||
const QString dbName = cardRelation->getName();
|
||||
const bool persistent = cardRelation->getIsPersistent();
|
||||
const bool faceDown = cardRelation->getIsFaceDown();
|
||||
|
||||
// Variable relations always use DoesNotAttach, regardless of the count the user
|
||||
// entered.
|
||||
|
|
@ -1080,7 +1087,7 @@ bool PlayerActions::createRelatedFromRelation(const CardItem *sourceCard,
|
|||
return false;
|
||||
}
|
||||
for (int i = 0; i < variableCount; ++i) {
|
||||
createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent);
|
||||
createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent, faceDown);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1089,7 +1096,7 @@ bool PlayerActions::createRelatedFromRelation(const CardItem *sourceCard,
|
|||
|
||||
if (count > 1) {
|
||||
for (int i = 0; i < count; ++i) {
|
||||
createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent);
|
||||
createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent, faceDown);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1109,7 +1116,7 @@ bool PlayerActions::createRelatedFromRelation(const CardItem *sourceCard,
|
|||
playCardToTable(sourceCard, false);
|
||||
}
|
||||
|
||||
createCard(sourceCard, dbName, attachType, persistent);
|
||||
createCard(sourceCard, dbName, attachType, persistent, faceDown);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -1136,7 +1143,8 @@ void PlayerActions::onRelatedCardCreated(const CardItem *sourceCard, const CardR
|
|||
void PlayerActions::createCard(const CardItem *sourceCard,
|
||||
const QString &dbCardName,
|
||||
CardRelationType attachType,
|
||||
bool persistent)
|
||||
bool persistent,
|
||||
bool faceDown)
|
||||
{
|
||||
CardInfoPtr cardInfo = CardDatabaseManager::query()->getCardInfo(dbCardName);
|
||||
|
||||
|
|
@ -1163,7 +1171,7 @@ void PlayerActions::createCard(const CardItem *sourceCard,
|
|||
}
|
||||
|
||||
cmd.set_pt(cardInfo->getPowTough().toStdString());
|
||||
if (SettingsCache::instance().getAnnotateTokens()) {
|
||||
if (SettingsCache::instance().userInterface().getAnnotateTokens()) {
|
||||
cmd.set_annotation(cardInfo->getText().toStdString());
|
||||
} else {
|
||||
cmd.set_annotation("");
|
||||
|
|
@ -1171,6 +1179,7 @@ void PlayerActions::createCard(const CardItem *sourceCard,
|
|||
cmd.set_destroy_on_zone_change(!persistent);
|
||||
cmd.set_x(gridPoint.x());
|
||||
cmd.set_y(gridPoint.y());
|
||||
cmd.set_face_down(faceDown);
|
||||
|
||||
ExactCard relatedCard =
|
||||
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());
|
||||
int ptIter = 0;
|
||||
for (const auto &_item : ptList) {
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
|
||||
if (_item.typeId() == QMetaType::Type::Int) {
|
||||
#else
|
||||
if (_item.type() == QVariant::Int) {
|
||||
#endif
|
||||
int oldItem = ptIter < oldpt.size() ? oldpt.at(ptIter).toInt() : 0;
|
||||
newpt += '/' + QString::number(oldItem + _item.toInt());
|
||||
} else {
|
||||
|
|
@ -1524,12 +1529,15 @@ void PlayerActions::offsetCardCounter(QList<CardItem *> selectedCards, int count
|
|||
QList<const ::google::protobuf::Message *> commandList;
|
||||
for (auto card : selectedCards) {
|
||||
int oldValue = card->getCounters().value(counterId, 0);
|
||||
int newValue = oldValue + offset;
|
||||
|
||||
// Early exit optimization: server enforces [0, MAX_COUNTERS_ON_CARD].
|
||||
// Compare clamped value to allow recovery from invalid states.
|
||||
int clampedValue = qBound(0, newValue, MAX_COUNTERS_ON_CARD);
|
||||
if (clampedValue != oldValue) {
|
||||
// Overflow-safe clamp to the server-enforced range [0, MAX_COUNTER_VALUE];
|
||||
// a result differing from oldValue also corrects an out-of-range cached value.
|
||||
// Callers only ever pass offset == ±1 (actAddCardCounter / actRemoveCardCounter).
|
||||
// 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;
|
||||
cmd->set_zone(card->getZone()->getName().toStdString());
|
||||
cmd->set_card_id(card->getId());
|
||||
|
|
@ -1562,7 +1570,7 @@ void PlayerActions::actSetCardCounter(QList<CardItem *> selectedCards, int count
|
|||
Expression exp(oldValue);
|
||||
double parsed = exp.parse(counterValue);
|
||||
// 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;
|
||||
cmd->set_zone(card->getZone()->getName().toStdString());
|
||||
|
|
@ -1592,7 +1600,7 @@ void PlayerActions::actIncrementAllCardCounters(QList<CardItem *> cardsToUpdate)
|
|||
counterIterator.next();
|
||||
int counterId = counterIterator.key();
|
||||
int currentValue = counterIterator.value();
|
||||
if (currentValue >= MAX_COUNTERS_ON_CARD) {
|
||||
if (currentValue >= MAX_COUNTER_VALUE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -240,7 +240,8 @@ private:
|
|||
void createCard(const CardItem *sourceCard,
|
||||
const QString &dbCardName,
|
||||
CardRelationType attach = CardRelationType::DoesNotAttach,
|
||||
bool persistent = false);
|
||||
bool persistent = false,
|
||||
bool faceDown = false);
|
||||
|
||||
void playSelectedCards(QList<CardItem *> selectedCards, bool faceDown = false);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,24 @@
|
|||
/**
|
||||
* @file player_event_handler.h
|
||||
* @ingroup GameLogicPlayers
|
||||
* @brief Player-scoped game event handler.
|
||||
*
|
||||
* PlayerEventHandler applies game events that affect a single Player’s
|
||||
* 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
|
||||
#define COCKATRICE_PLAYER_EVENT_HANDLER_H
|
||||
|
||||
#include "event_processing_options.h"
|
||||
|
||||
#include <QObject>
|
||||
|
|
@ -16,6 +29,7 @@
|
|||
class CardItem;
|
||||
class CardZoneLogic;
|
||||
class PlayerLogic;
|
||||
|
||||
class Event_AttachCard;
|
||||
class Event_ChangeZoneProperties;
|
||||
class Event_CreateArrow;
|
||||
|
|
@ -37,11 +51,176 @@ class Event_SetCounter;
|
|||
class Event_Shuffle;
|
||||
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
|
||||
{
|
||||
|
||||
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:
|
||||
/** @name Logging signals
|
||||
* @{
|
||||
*/
|
||||
void logSay(PlayerLogic *player, QString message);
|
||||
void logShuffle(PlayerLogic *player, CardZoneLogic *zone, int start, int end);
|
||||
void logRollDie(PlayerLogic *player, int sides, const QList<uint> &rolls);
|
||||
|
|
@ -83,40 +262,13 @@ signals:
|
|||
bool isLentToAnotherPlayer = false);
|
||||
void logAlwaysRevealTopCard(PlayerLogic *player, CardZoneLogic *zone, bool reveal);
|
||||
void logAlwaysLookAtTopCard(PlayerLogic *player, CardZoneLogic *zone, bool reveal);
|
||||
/** @} */
|
||||
|
||||
void cardZoneChanged(CardItem *card, bool sameZone);
|
||||
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:
|
||||
/** Owning player instance. */
|
||||
PlayerLogic *player;
|
||||
|
||||
void setCardAttrHelper(const GameEventContext &context,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
#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);
|
||||
connect(gameMetaInfo, &GameMetaInfo::startedChanged, gameState, &GameState::onStartedChanged);
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class Replay : public AbstractGame
|
|||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit Replay(QObject *_parent, GameReplay *_replay, bool isLocalGame);
|
||||
explicit Replay(QObject *_parent, const GameReplay *_replay, bool isLocalGame);
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_REPLAY_H
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#include "../../client/settings/cache_settings.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 _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
|
||||
|
||||
if (cards.isEmpty() && !doInsert && SettingsCache::instance().getCloseEmptyCardView()) {
|
||||
if (cards.isEmpty() && !doInsert && SettingsCache::instance().userInterface().getCloseEmptyCardView()) {
|
||||
emit closeView();
|
||||
}
|
||||
|
||||
|
|
@ -144,7 +145,7 @@ void ZoneViewZoneLogic::removeCard(int position, bool toNewZone)
|
|||
// 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
|
||||
// unrevealed portion of the same zone.
|
||||
if (cards.isEmpty() && SettingsCache::instance().getCloseEmptyCardView() && toNewZone) {
|
||||
if (cards.isEmpty() && SettingsCache::instance().userInterface().getCloseEmptyCardView() && toNewZone) {
|
||||
emit closeView();
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
26
cockatrice/src/game_graphics/animated_item.h
Normal file
26
cockatrice/src/game_graphics/animated_item.h
Normal 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
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
#include <QDebug>
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
|
||||
const QColor GHOST_MASK = QColor(255, 255, 255, 50);
|
||||
|
||||
|
|
@ -34,12 +35,13 @@ AbstractCardDragItem::AbstractCardDragItem(AbstractCardItem *_item,
|
|||
|
||||
setCacheMode(DeviceCoordinateCache);
|
||||
|
||||
connect(&SettingsCache::instance(), &SettingsCache::roundCardCornersChanged, this, [this](bool _roundCardCorners) {
|
||||
Q_UNUSED(_roundCardCorners);
|
||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::roundCardCornersChanged, this,
|
||||
[this](bool _roundCardCorners) {
|
||||
Q_UNUSED(_roundCardCorners);
|
||||
|
||||
prepareGeometryChange();
|
||||
update();
|
||||
});
|
||||
prepareGeometryChange();
|
||||
update();
|
||||
});
|
||||
|
||||
connect(item, &QObject::destroyed, this, &AbstractCardDragItem::deleteLater);
|
||||
}
|
||||
|
|
@ -47,7 +49,8 @@ AbstractCardDragItem::AbstractCardDragItem(AbstractCardItem *_item,
|
|||
QPainterPath AbstractCardDragItem::shape() const
|
||||
{
|
||||
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);
|
||||
return shape;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@
|
|||
#include <algorithm>
|
||||
#include <libcockatrice/card/database/card_database.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)
|
||||
: 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);
|
||||
setCacheMode(DeviceCoordinateCache);
|
||||
|
||||
connect(&SettingsCache::instance(), &SettingsCache::displayCardNamesChanged, this, [this] { update(); });
|
||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::displayCardNamesChanged, this,
|
||||
[this] { update(); });
|
||||
refreshCardInfo();
|
||||
|
||||
connect(&SettingsCache::instance(), &SettingsCache::roundCardCornersChanged, this, [this](bool _roundCardCorners) {
|
||||
Q_UNUSED(_roundCardCorners);
|
||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::roundCardCornersChanged, this,
|
||||
[this](bool _roundCardCorners) {
|
||||
Q_UNUSED(_roundCardCorners);
|
||||
|
||||
prepareGeometryChange();
|
||||
update();
|
||||
});
|
||||
prepareGeometryChange();
|
||||
update();
|
||||
});
|
||||
}
|
||||
|
||||
AbstractCardItem::~AbstractCardItem()
|
||||
|
|
@ -45,7 +50,8 @@ QRectF AbstractCardItem::boundingRect() const
|
|||
QPainterPath AbstractCardItem::shape() const
|
||||
{
|
||||
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);
|
||||
return shape;
|
||||
}
|
||||
|
|
@ -101,7 +107,7 @@ QSizeF AbstractCardItem::getTranslatedSize(QPainter *painter) const
|
|||
|
||||
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);
|
||||
|
||||
QRectF totalBoundingRect = painter->combinedTransform().mapRect(boundingRect());
|
||||
|
|
@ -151,7 +157,7 @@ void AbstractCardItem::paintPicture(QPainter *painter, const QSizeF &translatedS
|
|||
painter->drawPath(shape());
|
||||
}
|
||||
|
||||
if (translatedPixmap.isNull() || SettingsCache::instance().getDisplayCardNames() || facedown) {
|
||||
if (translatedPixmap.isNull() || SettingsCache::instance().cardsDisplay().getDisplayCardNames() || facedown) {
|
||||
painter->save();
|
||||
transformPainter(painter, translatedSize, angle);
|
||||
painter->setPen(Qt::white);
|
||||
|
|
@ -234,7 +240,7 @@ void AbstractCardItem::setHovered(bool _hovered)
|
|||
|
||||
isHovered = _hovered;
|
||||
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);
|
||||
update();
|
||||
}
|
||||
|
|
@ -287,7 +293,7 @@ void AbstractCardItem::setTapped(bool _tapped, bool canAnimate)
|
|||
}
|
||||
|
||||
tapped = _tapped;
|
||||
if (SettingsCache::instance().getTapAnimation() && canAnimate) {
|
||||
if (SettingsCache::instance().cardsDisplay().getTapAnimation() && canAnimate) {
|
||||
static_cast<GameScene *>(scene())->registerAnimationItem(this);
|
||||
} else {
|
||||
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)
|
||||
{
|
||||
facedown = _facedown;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#ifndef ABSTRACTCARDITEM_H
|
||||
#define ABSTRACTCARDITEM_H
|
||||
|
||||
#include "../animated_item.h"
|
||||
#include "../card_dimensions.h"
|
||||
#include "arrow_target.h"
|
||||
#include "graphics_item_type.h"
|
||||
|
|
@ -16,7 +17,7 @@
|
|||
|
||||
class PlayerLogic;
|
||||
|
||||
class AbstractCardItem : public ArrowTarget
|
||||
class AbstractCardItem : public ArrowTarget, public IAnimatedItem
|
||||
{
|
||||
Q_OBJECT
|
||||
protected:
|
||||
|
|
@ -126,6 +127,9 @@ public:
|
|||
emit deleteCardInfoPopup(cardRef.name);
|
||||
}
|
||||
|
||||
/** @brief Default: no per-tick animation. Subclasses override to animate. */
|
||||
bool animationEvent() override;
|
||||
|
||||
protected:
|
||||
void transformPainter(QPainter *painter, const QSizeF &translatedSize, int angle);
|
||||
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "abstract_counter.h"
|
||||
|
||||
#include "../../client/settings/cache_settings.h"
|
||||
#include "../../client/settings/shortcuts_settings.h"
|
||||
#include "../../game/player/player_actions.h"
|
||||
#include "../../game/player/player_logic.h"
|
||||
#include "../../game_graphics/board/translate_counter_name.h"
|
||||
|
|
@ -28,8 +29,9 @@ AbstractCounter::AbstractCounter(CounterState *state,
|
|||
{
|
||||
setAcceptHoverEvents(true);
|
||||
|
||||
connect(state, &CounterState::valueChanged, this, [this](int, int newValue) {
|
||||
connect(state, &CounterState::valueChanged, this, [this](int oldValue, int newValue) {
|
||||
value = newValue;
|
||||
onValueChanged(oldValue, newValue);
|
||||
update();
|
||||
});
|
||||
|
||||
|
|
@ -227,3 +229,9 @@ void AbstractCounterDialog::changeValue(int diff)
|
|||
curValue += diff;
|
||||
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).
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,13 @@ protected:
|
|||
bool hovered = false;
|
||||
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 hoverEnterEvent(QGraphicsSceneHoverEvent *event) override;
|
||||
void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
#include <libcockatrice/protocol/pb/command_attach_card.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_create_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/zone_names.h>
|
||||
|
||||
|
|
@ -261,7 +262,7 @@ void ArrowDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
|||
if (startZone->getName() == ZoneNames::HAND) {
|
||||
startCard->playCard(false);
|
||||
CardInfoPtr ci = startCard->getCard().getCardPtr();
|
||||
bool playToStack = SettingsCache::instance().getPlayToStack();
|
||||
bool playToStack = SettingsCache::instance().userInterface().getPlayToStack();
|
||||
if (ci && ((!playToStack && ci->getUiAttributes().tableRow == 3) ||
|
||||
(playToStack && ci->getUiAttributes().tableRow != 0 &&
|
||||
startCard->getZone()->getName() != ZoneNames::STACK))) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "card_item.h"
|
||||
|
||||
#include "../../client/settings/cache_settings.h"
|
||||
#include "../../client/settings/card_counter_settings.h"
|
||||
#include "../../game/phase.h"
|
||||
#include "../../game/player/player_actions.h"
|
||||
#include "../../game/player/player_logic.h"
|
||||
|
|
@ -19,6 +20,7 @@
|
|||
#include <QPainter>
|
||||
#include <libcockatrice/card/card_info.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_card.pb.h>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
|
||||
CardItem::CardItem(PlayerLogic *_owner,
|
||||
QGraphicsItem *parent,
|
||||
|
|
@ -301,7 +303,7 @@ void CardItem::drawArrow(const QColor &arrowColor)
|
|||
auto *game = owner->getGame();
|
||||
PlayerLogic *arrowOwner = game->getPlayerManager()->getActiveLocalPlayer(game->getGameState()->getActivePlayer());
|
||||
int phase = 0; // 0 means to not set the phase
|
||||
if (SettingsCache::instance().getDoNotDeleteArrowsInSubPhases()) {
|
||||
if (SettingsCache::instance().userInterface().getDoNotDeleteArrowsInSubPhases()) {
|
||||
int currentPhase = game->getGameState()->getCurrentPhase();
|
||||
phase = Phases::getLastSubphase(currentPhase) + 1;
|
||||
}
|
||||
|
|
@ -420,7 +422,7 @@ void CardItem::playCard(bool faceDown)
|
|||
if (tz) {
|
||||
emit tz->toggleTapped();
|
||||
} else {
|
||||
if (SettingsCache::instance().getClickPlaysAllSelected()) {
|
||||
if (SettingsCache::instance().userInterface().getClickPlaysAllSelected()) {
|
||||
if (faceDown) {
|
||||
emit playSelectedFaceDown(this);
|
||||
} else {
|
||||
|
|
@ -484,7 +486,7 @@ static bool isUnwritableRevealZone(CardZoneLogic *zone)
|
|||
void CardItem::handleClickedToPlay(bool shiftHeld)
|
||||
{
|
||||
if (isUnwritableRevealZone(state->getZone())) {
|
||||
if (SettingsCache::instance().getClickPlaysAllSelected()) {
|
||||
if (SettingsCache::instance().userInterface().getClickPlaysAllSelected()) {
|
||||
emit hideSelected(this);
|
||||
} else {
|
||||
state->getZone()->removeCard(this);
|
||||
|
|
@ -501,7 +503,7 @@ void CardItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
|||
return;
|
||||
}
|
||||
if ((event->modifiers() != Qt::AltModifier) && (event->button() == Qt::LeftButton) &&
|
||||
(!SettingsCache::instance().getDoubleClickToPlay())) {
|
||||
(!SettingsCache::instance().userInterface().getDoubleClickToPlay())) {
|
||||
handleClickedToPlay(event->modifiers().testFlag(Qt::ShiftModifier));
|
||||
}
|
||||
if (owner != nullptr) {
|
||||
|
|
@ -513,7 +515,7 @@ void CardItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
|||
void CardItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
|
||||
{
|
||||
if ((event->modifiers() != Qt::AltModifier) && (event->buttons() == Qt::LeftButton) &&
|
||||
(SettingsCache::instance().getDoubleClickToPlay())) {
|
||||
(SettingsCache::instance().userInterface().getDoubleClickToPlay())) {
|
||||
handleClickedToPlay(event->modifiers().testFlag(Qt::ShiftModifier));
|
||||
}
|
||||
event->accept();
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@
|
|||
#include "abstract_card_item.h"
|
||||
|
||||
#include <libcockatrice/network/server/remote/game/server_card.h>
|
||||
#include <libcockatrice/utility/trice_limits.h>
|
||||
|
||||
class CardDatabase;
|
||||
class CardDragItem;
|
||||
|
|
@ -143,7 +142,7 @@ public:
|
|||
void resetState(bool keepAnnotations = false);
|
||||
void processCardInfo(const ServerInfo_Card &_info);
|
||||
|
||||
bool animationEvent();
|
||||
bool animationEvent() override;
|
||||
CardDragItem *createDragItem(int _id, const QPointF &_pos, const QPointF &_scenePos, bool forceFaceDown);
|
||||
void deleteDragItem();
|
||||
void drawArrow(const QColor &arrowColor);
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
#include <libcockatrice/card/card_info.h>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
|
||||
DeckViewCardDragItem::DeckViewCardDragItem(DeckViewCard *_item,
|
||||
const QPointF &_hotSpot,
|
||||
|
|
@ -77,11 +78,12 @@ DeckViewCard::DeckViewCard(QGraphicsItem *parent, const CardRef &cardRef, const
|
|||
{
|
||||
setAcceptHoverEvents(true);
|
||||
|
||||
connect(&SettingsCache::instance(), &SettingsCache::roundCardCornersChanged, this, [this](bool _roundCardCorners) {
|
||||
Q_UNUSED(_roundCardCorners);
|
||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::roundCardCornersChanged, this,
|
||||
[this](bool _roundCardCorners) {
|
||||
Q_UNUSED(_roundCardCorners);
|
||||
|
||||
update();
|
||||
});
|
||||
update();
|
||||
});
|
||||
}
|
||||
|
||||
DeckViewCard::~DeckViewCard()
|
||||
|
|
@ -99,7 +101,8 @@ void DeckViewCard::paint(QPainter *painter, const QStyleOptionGraphicsItem *opti
|
|||
pen.setJoinStyle(Qt::MiterJoin);
|
||||
pen.setColor(originZone == DECK_ZONE_MAIN ? Qt::green : Qt::red);
|
||||
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,
|
||||
cardRadius);
|
||||
painter->restore();
|
||||
|
|
@ -360,6 +363,16 @@ void DeckViewScene::rebuildTree()
|
|||
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()) {
|
||||
DeckViewCardContainer *container = cardContainers.value(currentZone->getName(), 0);
|
||||
if (!container) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "deck_view_container.h"
|
||||
|
||||
#include "../../client/settings/cache_settings.h"
|
||||
#include "../../client/settings/shortcuts_settings.h"
|
||||
#include "../../interface/card_picture_loader/card_picture_loader.h"
|
||||
#include "../../interface/deck_loader/deck_loader.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/response_deck_download.pb.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)
|
||||
{
|
||||
|
|
@ -95,8 +97,8 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent)
|
|||
&DeckViewContainer::refreshShortcuts);
|
||||
refreshShortcuts();
|
||||
|
||||
connect(&SettingsCache::instance(), &SettingsCache::visualDeckStorageInGameChanged, this,
|
||||
&DeckViewContainer::setVisualDeckStorageExists);
|
||||
connect(&SettingsCache::instance().visualDeckStorage(), &VisualDeckStorageSettings::visualDeckStorageInGameChanged,
|
||||
this, &DeckViewContainer::setVisualDeckStorageExists);
|
||||
|
||||
switchToDeckSelectView();
|
||||
}
|
||||
|
|
@ -138,7 +140,7 @@ static void setVisibility(QPushButton *button, bool visible)
|
|||
|
||||
void DeckViewContainer::switchToDeckSelectView()
|
||||
{
|
||||
if (SettingsCache::instance().getVisualDeckStorageInGame()) {
|
||||
if (SettingsCache::instance().visualDeckStorage().getVisualDeckStorageInGame()) {
|
||||
deckView->setHidden(true);
|
||||
|
||||
tryCreateVisualDeckStorageWidget();
|
||||
|
|
@ -209,6 +211,7 @@ void DeckViewContainer::refreshShortcuts()
|
|||
loadLocalButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadLocalButton"));
|
||||
loadRemoteButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadRemoteButton"));
|
||||
loadFromClipboardButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadFromClipboardButton"));
|
||||
loadFromWebsiteButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadFromWebsiteButton"));
|
||||
unloadDeckButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/unloadDeckButton"));
|
||||
readyStartButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/readyStartButton"));
|
||||
sideboardLockButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/sideboardLockButton"));
|
||||
|
|
|
|||
|
|
@ -20,7 +20,10 @@
|
|||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
#include <libcockatrice/models/database/card_database_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)
|
||||
: QDialog(parent), predefinedTokens(_predefinedTokens)
|
||||
|
|
@ -186,7 +189,7 @@ void DlgCreateToken::tokenSelectionChanged(const QModelIndex ¤t, const QMo
|
|||
const QChar cardColor = cardInfo->getColorChar();
|
||||
colorEdit->setCurrentIndex(colorEdit->findData(cardColor, Qt::UserRole, Qt::MatchFixedString));
|
||||
ptEdit->setText(cardInfo->getPowTough());
|
||||
if (SettingsCache::instance().getAnnotateTokens()) {
|
||||
if (SettingsCache::instance().userInterface().getAnnotateTokens()) {
|
||||
annotationEdit->setText(cardInfo->getText());
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
#include <QSpinBox>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
#include <libcockatrice/utility/trice_limits.h>
|
||||
#include <libcockatrice/utility/dice_limits.h>
|
||||
|
||||
DlgRollDice::DlgRollDice(QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@
|
|||
#include <QDebug>
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
#include <QGraphicsView>
|
||||
#include <QSet>
|
||||
#include <QtMath>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
#include <libcockatrice/utility/zone_names.h>
|
||||
#include <numeric>
|
||||
|
||||
|
|
@ -36,7 +36,7 @@ GameScene::GameScene(PhasesToolbar *_phasesToolbar, QObject *parent)
|
|||
{
|
||||
animationTimer = new QBasicTimer;
|
||||
addItem(phasesToolbar);
|
||||
connect(&SettingsCache::instance(), &SettingsCache::minPlayersForMultiColumnLayoutChanged, this,
|
||||
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::minPlayersForMultiColumnLayoutChanged, this,
|
||||
&GameScene::rearrange);
|
||||
|
||||
rearrange();
|
||||
|
|
@ -44,7 +44,25 @@ GameScene::GameScene(PhasesToolbar *_phasesToolbar, QObject *parent)
|
|||
|
||||
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;
|
||||
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
|
||||
// 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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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*/)
|
||||
{
|
||||
QMutableSetIterator<CardItem *> i(cardsToAnimate);
|
||||
QMutableHashIterator<QObject *, IAnimatedItem *> i(animatedItems);
|
||||
while (i.hasNext()) {
|
||||
i.next();
|
||||
if (!i.value()->animationEvent()) {
|
||||
i.remove();
|
||||
}
|
||||
}
|
||||
if (cardsToAnimate.isEmpty()) {
|
||||
if (animatedItems.isEmpty()) {
|
||||
animationTimer->stop();
|
||||
}
|
||||
}
|
||||
|
||||
void GameScene::registerAnimationItem(AbstractCardItem *card)
|
||||
void GameScene::registerAnimationItem(IAnimatedItem *item)
|
||||
{
|
||||
cardsToAnimate.insert(static_cast<CardItem *>(card));
|
||||
if (!animationTimer->isActive()) {
|
||||
auto *object = dynamic_cast<QObject *>(item);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
void GameScene::unregisterAnimationItem(AbstractCardItem *card)
|
||||
void GameScene::unregisterAnimationItem(IAnimatedItem *item)
|
||||
{
|
||||
cardsToAnimate.remove(static_cast<CardItem *>(card));
|
||||
if (cardsToAnimate.isEmpty()) {
|
||||
animatedItems.remove(dynamic_cast<QObject *>(item));
|
||||
if (animationTimer && animatedItems.isEmpty()) {
|
||||
animationTimer->stop();
|
||||
}
|
||||
}
|
||||
|
||||
void GameScene::removeAnimatedItem(QObject *item)
|
||||
{
|
||||
animatedItems.remove(item);
|
||||
if (animationTimer && animatedItems.isEmpty()) {
|
||||
animationTimer->stop();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,13 +4,14 @@
|
|||
#include "../game/arrow_registry.h"
|
||||
#include "../game/board/arrow_data.h"
|
||||
#include "../game/zones/card_zone_logic.h"
|
||||
#include "animated_item.h"
|
||||
#include "board/arrow_item.h"
|
||||
|
||||
#include <QGraphicsScene>
|
||||
#include <QHash>
|
||||
#include <QList>
|
||||
#include <QLoggingCategory>
|
||||
#include <QPointer>
|
||||
#include <QSet>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(GameSceneLog, "game_scene");
|
||||
inline Q_LOGGING_CATEGORY(GameScenePlayerAdditionRemovalLog, "game_scene.player_addition_removal");
|
||||
|
|
@ -24,6 +25,7 @@ class CardItem;
|
|||
class ServerInfo_Card;
|
||||
class PhasesToolbar;
|
||||
class QBasicTimer;
|
||||
class QObject;
|
||||
|
||||
/**
|
||||
* @class GameScene
|
||||
|
|
@ -50,8 +52,8 @@ private:
|
|||
QList<ZoneViewWidget *> zoneViews; ///< Active zone view widgets
|
||||
QSize viewSize; ///< Current view size
|
||||
QPointer<CardItem> hoveredCard; ///< Currently hovered card
|
||||
QBasicTimer *animationTimer; ///< Timer for card animations
|
||||
QSet<CardItem *> cardsToAnimate; ///< Cards currently animating
|
||||
QBasicTimer *animationTimer; ///< Timer for scene animations
|
||||
QHash<QObject *, IAnimatedItem *> animatedItems; ///< Items currently animating
|
||||
int playerRotation; ///< Rotation offset for player layout
|
||||
|
||||
/**
|
||||
|
|
@ -182,15 +184,24 @@ public:
|
|||
/** @brief Updates hovered card highlighting. */
|
||||
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. */
|
||||
void unregisterAnimationItem(AbstractCardItem *card);
|
||||
/** @brief Unregisters an item from animation updates. */
|
||||
void unregisterAnimationItem(IAnimatedItem *item);
|
||||
void startRubberBand(const QPointF &selectionOrigin);
|
||||
void resizeRubberBand(const QPointF &cursorPoint, int selectedCount);
|
||||
void stopRubberBand();
|
||||
|
||||
private slots:
|
||||
/** @brief Removes a destroyed item from the animation set. */
|
||||
void removeAnimatedItem(QObject *item);
|
||||
|
||||
public slots:
|
||||
void onCardSelectionChanged(AbstractCardItem *card, bool selected);
|
||||
void onCardRightClicked(AbstractCardItem *card, QPoint screenPos);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
#include "game_view.h"
|
||||
|
||||
#include "../client/settings/cache_settings.h"
|
||||
#include "../client/settings/shortcuts_settings.h"
|
||||
#include "game_scene.h"
|
||||
|
||||
#include <QAction>
|
||||
#include <QGridLayout>
|
||||
#include <QLabel>
|
||||
#include <QLayout>
|
||||
#include <QResizeEvent>
|
||||
#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.
|
||||
// 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)));
|
||||
setRenderHints(QPainter::TextAntialiasing | QPainter::Antialiasing);
|
||||
setFocusPolicy(Qt::ClickFocus);
|
||||
setViewportUpdateMode(BoundingRectViewportUpdate);
|
||||
|
||||
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::sigStopRubberBand, this, &GameView::stopRubberBand);
|
||||
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);
|
||||
|
||||
|
|
@ -53,31 +63,40 @@ GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, par
|
|||
refreshShortcuts();
|
||||
rubberBand = new SelectionRubberBand(QRubberBand::Rectangle, this);
|
||||
|
||||
const QString countLabelStyle = "color: white; "
|
||||
"font-size: 14px; "
|
||||
"font-weight: bold; "
|
||||
"background-color: rgba(0, 0, 0, 160); "
|
||||
"border-radius: 3px; "
|
||||
"padding: 1px 2px;";
|
||||
const QString baseProperties = "color: white; "
|
||||
"font-family: monospace; "
|
||||
"background-color: rgba(0, 0, 0, 160); "
|
||||
"border-radius: 3px; "
|
||||
"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->setStyleSheet(countLabelStyle);
|
||||
dragCountLabel->setStyleSheet(dragCountLabelStyle);
|
||||
dragCountLabel->hide();
|
||||
dragCountLabel->raise();
|
||||
|
||||
totalCountLabel = new QLabel(this);
|
||||
totalCountLabel->setStyleSheet(countLabelStyle);
|
||||
totalCountLabel->setStyleSheet(totalCountLabelStyle);
|
||||
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)
|
||||
{
|
||||
QGraphicsView::resizeEvent(event);
|
||||
|
||||
GameScene *s = dynamic_cast<GameScene *>(scene());
|
||||
if (s) {
|
||||
s->processViewSizeChange(event->size());
|
||||
}
|
||||
GameScene *s = static_cast<GameScene *>(scene());
|
||||
s->processViewSizeChange(event->size());
|
||||
|
||||
updateSceneRect(scene()->sceneRect());
|
||||
updateTotalSelectionCount(event->size());
|
||||
|
|
@ -111,7 +130,7 @@ void GameView::resizeRubberBand(const QPointF &cursorPoint, int selectedCount)
|
|||
QRect rect = QRect(mapFromScene(selectionOrigin), cursor).normalized();
|
||||
rubberBand->setGeometry(rect);
|
||||
|
||||
if (!SettingsCache::instance().getShowDragSelectionCount()) {
|
||||
if (!SettingsCache::instance().userInterface().getShowDragSelectionCount()) {
|
||||
dragCountLabel->hide();
|
||||
return;
|
||||
}
|
||||
|
|
@ -162,27 +181,117 @@ void GameView::refreshShortcuts()
|
|||
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)
|
||||
{
|
||||
if (!SettingsCache::instance().getShowTotalSelectionCount()) {
|
||||
totalCountLabel->hide();
|
||||
return;
|
||||
}
|
||||
constexpr int kMarginInPixels = 10;
|
||||
constexpr int kSpacingBetweenLabels = 4;
|
||||
|
||||
int availableWidth = viewSize.isValid() ? viewSize.width() : viewport()->width();
|
||||
int availableHeight = viewSize.isValid() ? viewSize.height() : viewport()->height();
|
||||
|
||||
int count = scene()->selectedItems().count();
|
||||
|
||||
if (count > 1) {
|
||||
if (!SettingsCache::instance().userInterface().getShowTotalSelectionCount() || count <= 1) {
|
||||
totalCountLabel->hide();
|
||||
} else {
|
||||
totalCountLabel->setText(QString::number(count));
|
||||
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 y = availableHeight - totalCountLabel->height() - kMarginInPixels;
|
||||
totalCountLabel->move(x, y);
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,12 @@
|
|||
#ifndef GAMEVIEW_H
|
||||
#define GAMEVIEW_H
|
||||
|
||||
#include "tally/tally.h"
|
||||
|
||||
#include <QGraphicsView>
|
||||
|
||||
class GameScene;
|
||||
class QGridLayout;
|
||||
class QLabel;
|
||||
class QRubberBand;
|
||||
|
||||
|
|
@ -21,7 +24,13 @@ private:
|
|||
QRubberBand *rubberBand;
|
||||
QLabel *dragCountLabel;
|
||||
QLabel *totalCountLabel;
|
||||
QWidget *tallyContainer;
|
||||
QGridLayout *tallyLayout;
|
||||
QPointF selectionOrigin;
|
||||
QList<TallyRow> cachedTallyRows; ///< Cached entries to avoid redundant rebuilds
|
||||
|
||||
QSize rebuildTallyLabels(const QList<TallyRow> &entries);
|
||||
void clearTallyLabels();
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
|
|
@ -31,6 +40,7 @@ private slots:
|
|||
void stopRubberBand();
|
||||
void refreshShortcuts();
|
||||
void updateTotalSelectionCount(const QSize &viewSize = QSize());
|
||||
void setFocusDisabled(bool disabled);
|
||||
public slots:
|
||||
void updateSceneRect(const QRectF &rect);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "card_menu.h"
|
||||
|
||||
#include "../../../client/settings/card_counter_settings.h"
|
||||
#include "../../../client/settings/shortcuts_settings.h"
|
||||
#include "../../../interface/widgets/tabs/tab_game.h"
|
||||
#include "../../board/card_item.h"
|
||||
#include "../../game/player/player_actions.h"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "grave_menu.h"
|
||||
|
||||
#include "../../../client/settings/shortcuts_settings.h"
|
||||
#include "../../game/abstract_game.h"
|
||||
#include "../../game/player/player_actions.h"
|
||||
#include "../../game/player/player_logic.h"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "move_menu.h"
|
||||
|
||||
#include "../../../client/settings/shortcuts_settings.h"
|
||||
#include "../../game/player/player_actions.h"
|
||||
#include "../../game/player/player_logic.h"
|
||||
#include "../card_menu_action_type.h"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "player_menu.h"
|
||||
|
||||
#include "../../../client/settings/shortcuts_settings.h"
|
||||
#include "../../../game_graphics/zones/hand_zone.h"
|
||||
#include "../../../game_graphics/zones/pile_zone.h"
|
||||
#include "../../../game_graphics/zones/table_zone.h"
|
||||
|
|
@ -44,6 +45,8 @@ PlayerMenu::PlayerMenu(PlayerGraphicsItem *_player) : QObject(_player), player(_
|
|||
utilityMenu = nullptr;
|
||||
}
|
||||
|
||||
tallyMenu = addManagedMenu<TallyMenu>();
|
||||
|
||||
if (player->getLogic()->getPlayerInfo()->getLocal()) {
|
||||
sayMenu = addManagedMenu<SayMenu>(player);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -15,11 +15,13 @@
|
|||
#include "rfg_menu.h"
|
||||
#include "say_menu.h"
|
||||
#include "sideboard_menu.h"
|
||||
#include "tally_menu.h"
|
||||
#include "utility_menu.h"
|
||||
|
||||
#include <QList>
|
||||
#include <QMenu>
|
||||
#include <QObject>
|
||||
#include <libcockatrice/utility/card_ref.h>
|
||||
|
||||
class CardItem;
|
||||
class CardMenu;
|
||||
|
|
@ -87,6 +89,7 @@ private:
|
|||
GraveyardMenu *graveMenu;
|
||||
RfgMenu *rfgMenu;
|
||||
UtilityMenu *utilityMenu;
|
||||
TallyMenu *tallyMenu;
|
||||
SayMenu *sayMenu;
|
||||
CustomZoneMenu *customZonesMenu;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "pt_menu.h"
|
||||
|
||||
#include "../../../client/settings/shortcuts_settings.h"
|
||||
#include "../../game/player/player_actions.h"
|
||||
#include "../../game/player/player_logic.h"
|
||||
#include "../player_graphics_item.h"
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
#include "../../game/player/player_logic.h"
|
||||
#include "../player_graphics_item.h"
|
||||
|
||||
#include <libcockatrice/settings/message_settings.h>
|
||||
SayMenu::SayMenu(PlayerGraphicsItem *_player) : player(_player)
|
||||
{
|
||||
connect(&SettingsCache::instance().messages(), &MessageSettings::messageMacrosChanged, this, &SayMenu::initSayMenu);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "sideboard_menu.h"
|
||||
|
||||
#include "../../../client/settings/shortcuts_settings.h"
|
||||
#include "../../game/player/player_actions.h"
|
||||
#include "../../game/player/player_logic.h"
|
||||
#include "../player_graphics_item.h"
|
||||
|
|
|
|||
57
cockatrice/src/game_graphics/player/menu/tally_menu.cpp
Normal file
57
cockatrice/src/game_graphics/player/menu/tally_menu.cpp
Normal 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"));
|
||||
}
|
||||
31
cockatrice/src/game_graphics/player/menu/tally_menu.h
Normal file
31
cockatrice/src/game_graphics/player/menu/tally_menu.h
Normal 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
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
#include "utility_menu.h"
|
||||
|
||||
#include "../../../client/settings/shortcuts_settings.h"
|
||||
#include "../../../interface/deck_loader/deck_loader.h"
|
||||
#include "../../game/player/player_actions.h"
|
||||
#include "../../game/player/player_logic.h"
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
#include <QInputDialog>
|
||||
#include <libcockatrice/card/relation/card_relation.h>
|
||||
#include <libcockatrice/utility/string_limits.h>
|
||||
|
||||
PlayerDialogs::PlayerDialogs(PlayerGraphicsItem *_player, PlayerActions *_playerActions)
|
||||
: QObject(_player), player(_player), playerActions(_playerActions)
|
||||
|
|
|
|||
|
|
@ -13,12 +13,13 @@
|
|||
#include "player_dialogs.h"
|
||||
|
||||
#include <QGraphicsView>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
|
||||
PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player)
|
||||
{
|
||||
connect(&SettingsCache::instance(), &SettingsCache::horizontalHandChanged, this,
|
||||
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::horizontalHandChanged, this,
|
||||
&PlayerGraphicsItem::rearrangeZones);
|
||||
connect(&SettingsCache::instance(), &SettingsCache::handJustificationChanged, this,
|
||||
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::handJustificationChanged, this,
|
||||
&PlayerGraphicsItem::rearrangeZones);
|
||||
connect(player, &PlayerLogic::rearrangeCounters, this, &PlayerGraphicsItem::rearrangeCounters);
|
||||
connect(player, &PlayerLogic::activeChanged, this, &PlayerGraphicsItem::onPlayerActiveChanged);
|
||||
|
|
@ -59,6 +60,9 @@ PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player)
|
|||
|
||||
initializeZones();
|
||||
|
||||
connect(player, &PlayerLogic::addViewCustomZoneActionToCustomZoneMenu, this,
|
||||
&PlayerGraphicsItem::onCustomZoneAdded);
|
||||
|
||||
playerMenu->setMenusForGraphicItems();
|
||||
|
||||
connect(tableZoneGraphicsItem, &TableZone::sizeChanged, this, &PlayerGraphicsItem::updateBoundingRect);
|
||||
|
|
@ -121,6 +125,19 @@ void PlayerGraphicsItem::initializeZones()
|
|||
connect(handZoneGraphicsItem->getLogic(), &HandZoneLogic::cardCountChanged, handCounter,
|
||||
&HandCounter::updateNumber);
|
||||
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
|
||||
|
|
@ -132,7 +149,7 @@ qreal PlayerGraphicsItem::getMinimumWidth() const
|
|||
{
|
||||
qreal result = tableZoneGraphicsItem->getMinimumWidth() + CardDimensions::HEIGHT_F + 15 + counterAreaWidth +
|
||||
stackZoneGraphicsItem->boundingRect().width();
|
||||
if (!SettingsCache::instance().getHorizontalHand()) {
|
||||
if (!SettingsCache::instance().userInterface().getHorizontalHand()) {
|
||||
result += handZoneGraphicsItem->boundingRect().width();
|
||||
}
|
||||
return result;
|
||||
|
|
@ -149,7 +166,7 @@ void PlayerGraphicsItem::processSceneSizeChange(int newPlayerWidth)
|
|||
// Extend table (and hand, if horizontal) to accommodate the new player width.
|
||||
qreal tableWidth = newPlayerWidth - CardDimensions::HEIGHT_F - 15 - counterAreaWidth -
|
||||
stackZoneGraphicsItem->boundingRect().width();
|
||||
if (!SettingsCache::instance().getHorizontalHand()) {
|
||||
if (!SettingsCache::instance().userInterface().getHorizontalHand()) {
|
||||
tableWidth -= handZoneGraphicsItem->boundingRect().width();
|
||||
}
|
||||
|
||||
|
|
@ -171,6 +188,11 @@ void PlayerGraphicsItem::onCounterAdded(CounterState *state)
|
|||
AbstractCounter *widget;
|
||||
if (state->getName() == "life") {
|
||||
widget = playerTarget->addCounter(state);
|
||||
connect(state, &CounterState::valueChanged, this, [this](int oldValue, int newValue) {
|
||||
if (newValue < oldValue) {
|
||||
tableZoneGraphicsItem->triggerDamageShimmer();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
widget = new GeneralCounter(state, player, true, this);
|
||||
}
|
||||
|
|
@ -217,7 +239,7 @@ void PlayerGraphicsItem::rearrangeCounters()
|
|||
void PlayerGraphicsItem::rearrangeZones()
|
||||
{
|
||||
auto base = QPointF(CardDimensions::HEIGHT_F + counterAreaWidth + 15, 0);
|
||||
if (SettingsCache::instance().getHorizontalHand()) {
|
||||
if (SettingsCache::instance().userInterface().getHorizontalHand()) {
|
||||
if (mirrored) {
|
||||
if (player->getHandZone()->contentsKnown()) {
|
||||
handVisible = true;
|
||||
|
|
@ -268,7 +290,7 @@ void PlayerGraphicsItem::updateBoundingRect()
|
|||
{
|
||||
prepareGeometryChange();
|
||||
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;
|
||||
bRect = QRectF(0, 0, width + tableZoneGraphicsItem->boundingRect().width(),
|
||||
tableZoneGraphicsItem->boundingRect().height() + handHeight);
|
||||
|
|
|
|||
|
|
@ -77,6 +77,11 @@ public:
|
|||
return playerTarget;
|
||||
}
|
||||
|
||||
CardZone *getZoneGraphicsItem(const QString &name) const
|
||||
{
|
||||
return zoneGraphicsItems.value(name, nullptr);
|
||||
}
|
||||
|
||||
[[nodiscard]] PileZone *getDeckZoneGraphicsItem() const
|
||||
{
|
||||
return deckZoneGraphicsItem;
|
||||
|
|
@ -110,6 +115,7 @@ public:
|
|||
|
||||
public slots:
|
||||
void onPlayerActiveChanged(bool _active);
|
||||
void onCustomZoneAdded(QString customZoneName);
|
||||
void onCounterAdded(CounterState *state);
|
||||
void onCounterRemoved(int counterId);
|
||||
void rearrangeCounters();
|
||||
|
|
@ -128,6 +134,7 @@ private:
|
|||
PlayerArea *playerArea;
|
||||
PlayerTarget *playerTarget;
|
||||
QMap<int, AbstractCounter *> counterWidgets;
|
||||
QMap<QString, CardZone *> zoneGraphicsItems;
|
||||
PileZone *deckZoneGraphicsItem;
|
||||
PileZone *sideboardGraphicsItem;
|
||||
PileZone *graveyardZoneGraphicsItem;
|
||||
|
|
|
|||
|
|
@ -25,11 +25,7 @@ bool PlayerListItemDelegate::editorEvent(QEvent *event,
|
|||
if ((event->type() == QEvent::MouseButtonPress) && index.isValid()) {
|
||||
auto *const mouseEvent = static_cast<QMouseEvent *>(event);
|
||||
if (mouseEvent->button() == Qt::RightButton) {
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
|
||||
static_cast<PlayerListWidget *>(parent())->showContextMenu(mouseEvent->globalPosition().toPoint(), index);
|
||||
#else
|
||||
static_cast<PlayerListWidget *>(parent())->showContextMenu(mouseEvent->globalPos(), index);
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
#include "player_target.h"
|
||||
|
||||
#include "../../client/settings/cache_settings.h"
|
||||
#include "../../game/player/player_logic.h"
|
||||
#include "../../interface/pixel_map_generator.h"
|
||||
#include "../game_scene.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QDebug>
|
||||
#include <QPainter>
|
||||
#include <QPixmapCache>
|
||||
|
|
@ -21,17 +24,24 @@ QRectF PlayerCounter::boundingRect() const
|
|||
|
||||
void PlayerCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
|
||||
{
|
||||
const int radius = 8;
|
||||
const qreal border = 1;
|
||||
QPainterPath path(QPointF(50 - border / 2, border / 2));
|
||||
path.lineTo(radius, border / 2);
|
||||
path.arcTo(border / 2, border / 2, 2 * radius, 2 * radius, 90, 90);
|
||||
path.lineTo(border / 2, 30 - border / 2);
|
||||
path.lineTo(50 - border / 2, 30 - border / 2);
|
||||
path.closeSubpath();
|
||||
const int radius = 15;
|
||||
const qreal border = 1.5;
|
||||
// The box is drawn with a border-wide stroke straddling the path, so the
|
||||
// visible outline spans [inset, inset + border]. Fills that must not cover
|
||||
// the outline (e.g. the life-change flash) use a path inset by `border`.
|
||||
const auto makePath = [](qreal inset) {
|
||||
QPainterPath path(QPointF(50 - inset, inset));
|
||||
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));
|
||||
pen.setWidth(border);
|
||||
pen.setWidthF(border);
|
||||
painter->setPen(pen);
|
||||
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->setPen(Qt::white);
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -7,21 +7,34 @@
|
|||
#ifndef PLAYERTARGET_H
|
||||
#define PLAYERTARGET_H
|
||||
|
||||
#include "../animated_item.h"
|
||||
#include "../board/abstract_counter.h"
|
||||
#include "../board/arrow_target.h"
|
||||
#include "../board/graphics_item_type.h"
|
||||
|
||||
#include <QElapsedTimer>
|
||||
#include <QPixmap>
|
||||
|
||||
class PlayerLogic;
|
||||
|
||||
class PlayerCounter : public AbstractCounter
|
||||
class PlayerCounter : public AbstractCounter, public IAnimatedItem
|
||||
{
|
||||
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:
|
||||
PlayerCounter(CounterState *state, PlayerLogic *player, QGraphicsItem *parent);
|
||||
QRectF boundingRect() const override;
|
||||
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
|
||||
bool animationEvent() override;
|
||||
};
|
||||
|
||||
class PlayerTarget : public ArrowTarget
|
||||
|
|
|
|||
36
cockatrice/src/game_graphics/tally/stats_tally.cpp
Normal file
36
cockatrice/src/game_graphics/tally/stats_tally.cpp
Normal 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)}};
|
||||
}
|
||||
21
cockatrice/src/game_graphics/tally/stats_tally.h
Normal file
21
cockatrice/src/game_graphics/tally/stats_tally.h
Normal 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
|
||||
77
cockatrice/src/game_graphics/tally/subtype_tally.cpp
Normal file
77
cockatrice/src/game_graphics/tally/subtype_tally.cpp
Normal 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
|
||||
26
cockatrice/src/game_graphics/tally/subtype_tally.h
Normal file
26
cockatrice/src/game_graphics/tally/subtype_tally.h
Normal 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
|
||||
26
cockatrice/src/game_graphics/tally/tally.cpp
Normal file
26
cockatrice/src/game_graphics/tally/tally.cpp
Normal 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 {};
|
||||
}
|
||||
50
cockatrice/src/game_graphics/tally/tally.h
Normal file
50
cockatrice/src/game_graphics/tally/tally.h
Normal 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
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
#include <QPainter>
|
||||
#include <libcockatrice/protocol/pb/command_move_card.pb.h>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
|
||||
HandZone::HandZone(HandZoneLogic *_logic, int _zoneHeight, QGraphicsItem *parent)
|
||||
: SelectZone(_logic, parent), zoneHeight(_zoneHeight)
|
||||
|
|
@ -33,7 +34,7 @@ void HandZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
|
|||
|
||||
QPoint point = dropPoint + scenePos().toPoint();
|
||||
int x = -1;
|
||||
if (SettingsCache::instance().getHorizontalHand()) {
|
||||
if (SettingsCache::instance().userInterface().getHorizontalHand()) {
|
||||
for (x = 0; x < getLogic()->getCards().size(); x++) {
|
||||
if (point.x() < static_cast<CardItem *>(getLogic()->getCards().at(x))->scenePos().x()) {
|
||||
break;
|
||||
|
|
@ -60,7 +61,7 @@ void HandZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
|
|||
|
||||
QRectF HandZone::boundingRect() const
|
||||
{
|
||||
if (SettingsCache::instance().getHorizontalHand()) {
|
||||
if (SettingsCache::instance().userInterface().getHorizontalHand()) {
|
||||
return QRectF(0, 0, width, CardDimensions::HEIGHT_F + 10);
|
||||
} else {
|
||||
return QRectF(0, 0, CardDimensions::WIDTH_F * 1.5, zoneHeight);
|
||||
|
|
@ -77,8 +78,8 @@ void HandZone::reorganizeCards()
|
|||
{
|
||||
if (!getLogic()->getCards().isEmpty()) {
|
||||
const int cardCount = getLogic()->getCards().size();
|
||||
if (SettingsCache::instance().getHorizontalHand()) {
|
||||
bool leftJustified = SettingsCache::instance().getLeftJustified();
|
||||
if (SettingsCache::instance().userInterface().getHorizontalHand()) {
|
||||
bool leftJustified = SettingsCache::instance().userInterface().getLeftJustified();
|
||||
qreal cardWidth = getLogic()->getCards().at(0)->boundingRect().width();
|
||||
const int xPadding = leftJustified ? cardWidth * 1.4 : 5;
|
||||
qreal totalWidth =
|
||||
|
|
@ -126,7 +127,7 @@ void HandZone::sortHand(const QList<CardList::SortOption> &options)
|
|||
|
||||
void HandZone::setWidth(qreal _width)
|
||||
{
|
||||
if (SettingsCache::instance().getHorizontalHand()) {
|
||||
if (SettingsCache::instance().userInterface().getHorizontalHand()) {
|
||||
prepareGeometryChange();
|
||||
width = _width;
|
||||
reorganizeCards();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "pile_zone.h"
|
||||
|
||||
#include "../../client/settings/cache_settings.h"
|
||||
#include "../../game/player/player_actions.h"
|
||||
#include "../../game/player/player_logic.h"
|
||||
#include "../../game/zones/pile_zone_logic.h"
|
||||
|
|
@ -11,6 +12,7 @@
|
|||
#include <QGraphicsSceneMouseEvent>
|
||||
#include <QPainter>
|
||||
#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)
|
||||
{
|
||||
|
|
@ -23,12 +25,13 @@ PileZone::PileZone(PileZoneLogic *_logic, QGraphicsItem *parent) : CardZone(_log
|
|||
.rotate(90)
|
||||
.translate(-CardDimensions::WIDTH_HALF_F, -CardDimensions::HEIGHT_HALF_F));
|
||||
|
||||
connect(&SettingsCache::instance(), &SettingsCache::roundCardCornersChanged, this, [this](bool _roundCardCorners) {
|
||||
Q_UNUSED(_roundCardCorners);
|
||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::roundCardCornersChanged, this,
|
||||
[this](bool _roundCardCorners) {
|
||||
Q_UNUSED(_roundCardCorners);
|
||||
|
||||
prepareGeometryChange();
|
||||
update();
|
||||
});
|
||||
prepareGeometryChange();
|
||||
update();
|
||||
});
|
||||
}
|
||||
|
||||
QRectF PileZone::boundingRect() const
|
||||
|
|
@ -39,7 +42,8 @@ QRectF PileZone::boundingRect() const
|
|||
QPainterPath PileZone::shape() const
|
||||
{
|
||||
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);
|
||||
return shape;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@
|
|||
#include <QGraphicsRectItem>
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
#include <QtMath>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -21,40 +22,32 @@ SelectZone::ZoneLayout SelectZone::computeZoneLayout(const StackLayoutParams &pa
|
|||
}
|
||||
qreal effectiveOffset = params.desiredOffset;
|
||||
if (params.cardCount > 1) {
|
||||
qreal fitOffset;
|
||||
if (params.totalHeight < params.cardHeight && params.minOffset > 0.0) {
|
||||
// Zone is shorter than a card (e.g. minimized). Compress offsets so
|
||||
// every card has at least minOffset pixels of its top visible.
|
||||
fitOffset = (params.totalHeight - params.minOffset) / (params.cardCount - 1);
|
||||
effectiveOffset = qMax(0.0, qMin(params.desiredOffset, fitOffset));
|
||||
qreal reservedForBottomCard;
|
||||
if (params.allowBottomOverflow) {
|
||||
// Allow the bottom card to partially overflow in tight zones, scaling the
|
||||
// overflow allowance by sqrt(cardCount-1) so offsets decrease smoothly
|
||||
// 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 {
|
||||
qreal reservedForBottomCard;
|
||||
if (params.allowBottomOverflow) {
|
||||
// Allow the bottom card to partially overflow in tight zones, scaling the
|
||||
// overflow allowance by sqrt(cardCount-1) so offsets decrease smoothly
|
||||
// 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);
|
||||
// No overflow: reserve full card height for the bottom card
|
||||
reservedForBottomCard = params.cardHeight;
|
||||
}
|
||||
qreal fitOffset = (params.totalHeight - reservedForBottomCard) / (params.cardCount - 1);
|
||||
|
||||
if (!params.allowBottomOverflow) {
|
||||
// Constrain offset so all card tops remain within zone bounds.
|
||||
// With start=0, last card top at (cardCount-1) * effectiveOffset must be < totalHeight.
|
||||
qreal maxOffsetForTops = params.totalHeight / (params.cardCount - 1);
|
||||
fitOffset = qMin(fitOffset, maxOffsetForTops);
|
||||
}
|
||||
if (!params.allowBottomOverflow) {
|
||||
// Constrain offset so all card tops remain within zone bounds.
|
||||
// With start=0, last card top at (cardCount-1) * effectiveOffset must be < totalHeight.
|
||||
qreal maxOffsetForTops = params.totalHeight / (params.cardCount - 1);
|
||||
fitOffset = qMin(fitOffset, maxOffsetForTops);
|
||||
}
|
||||
|
||||
// Apply minOffset only if it fits; otherwise compress further to keep all card tops visible.
|
||||
effectiveOffset = qMin(params.desiredOffset, fitOffset);
|
||||
if (fitOffset >= params.minOffset) {
|
||||
effectiveOffset = qMax(params.minOffset, effectiveOffset);
|
||||
}
|
||||
// Apply minOffset only if it fits; otherwise compress further to keep all card tops visible.
|
||||
effectiveOffset = qMin(params.desiredOffset, fitOffset);
|
||||
if (fitOffset >= params.minOffset) {
|
||||
effectiveOffset = qMax(params.minOffset, effectiveOffset);
|
||||
}
|
||||
}
|
||||
qreal stackHeight = (params.cardCount - 1) * effectiveOffset + params.cardHeight;
|
||||
|
|
|
|||
|
|
@ -43,12 +43,18 @@ void StackZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
|
|||
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();
|
||||
if (!cards.isEmpty() && startZone == getLogic() && cards.at(index)->getId() == dragItems.at(0)->getId()) {
|
||||
return;
|
||||
int index;
|
||||
if (startZone == getLogic()) {
|
||||
// Reordering within the zone: use drop position
|
||||
index = calcDropIndexFromY(dropPoint.y(), MIN_CARD_VISIBLE);
|
||||
// Same-zone no-op: don't move a card onto itself
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
#include "../board/arrow_item.h"
|
||||
#include "../board/card_drag_item.h"
|
||||
#include "../board/card_item.h"
|
||||
#include "../game_scene.h"
|
||||
#include "../z_values.h"
|
||||
|
||||
#include <QGraphicsScene>
|
||||
|
|
@ -15,6 +16,7 @@
|
|||
#include <libcockatrice/card/card_info.h>
|
||||
#include <libcockatrice/protocol/pb/command_move_card.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_card_attr.pb.h>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
#include <libcockatrice/utility/zone_names.h>
|
||||
|
||||
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::toggleTapped, this, &TableZone::toggleTapped);
|
||||
connect(themeManager, &ThemeManager::themeChanged, this, &TableZone::updateBg);
|
||||
connect(&SettingsCache::instance(), &SettingsCache::invertVerticalCoordinateChanged, this,
|
||||
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::invertVerticalCoordinateChanged, this,
|
||||
&TableZone::reorganizeCards);
|
||||
|
||||
updateBg();
|
||||
|
|
@ -46,6 +48,31 @@ void TableZone::updateBg()
|
|||
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
|
||||
{
|
||||
return QRectF(0, 0, width, height);
|
||||
|
|
@ -59,8 +86,8 @@ void TableZone::setMirrored(bool isMirrored)
|
|||
|
||||
bool TableZone::isInverted() const
|
||||
{
|
||||
return ((mirrored && !SettingsCache::instance().getInvertVerticalCoordinate()) ||
|
||||
(!mirrored && SettingsCache::instance().getInvertVerticalCoordinate()));
|
||||
return ((mirrored && !SettingsCache::instance().userInterface().getInvertVerticalCoordinate()) ||
|
||||
(!mirrored && SettingsCache::instance().userInterface().getInvertVerticalCoordinate()));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,16 +8,19 @@
|
|||
#define TABLEZONE_H
|
||||
|
||||
#include "../../game/zones/table_zone_logic.h"
|
||||
#include "../animated_item.h"
|
||||
#include "../board/abstract_card_item.h"
|
||||
#include "select_zone.h"
|
||||
|
||||
#include <QElapsedTimer>
|
||||
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
//! \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
|
||||
|
||||
|
|
@ -121,6 +124,16 @@ public:
|
|||
*/
|
||||
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.
|
||||
*/
|
||||
|
|
@ -185,6 +198,11 @@ public:
|
|||
}
|
||||
|
||||
private:
|
||||
static constexpr qreal shimmerDurationMs = 450.0;
|
||||
|
||||
QElapsedTimer shimmerClock;
|
||||
qreal damageShimmerAlpha = 0.0;
|
||||
|
||||
void paintZoneOutline(QPainter *painter);
|
||||
void paintLandDivider(QPainter *painter);
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
#include <QStyle>
|
||||
#include <QStyleOption>
|
||||
#include <libcockatrice/protocol/pb/command_shuffle.pb.h>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
|
@ -65,7 +66,7 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player,
|
|||
|
||||
connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(&searchEdit); });
|
||||
|
||||
if (SettingsCache::instance().getFocusCardViewSearchBar()) {
|
||||
if (SettingsCache::instance().userInterface().getFocusCardViewSearchBar()) {
|
||||
this->setActive(true);
|
||||
searchEdit.setFocus();
|
||||
}
|
||||
|
|
@ -75,6 +76,11 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player,
|
|||
searchEditProxy->setZValue(ZValues::DRAG_ITEM);
|
||||
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
|
||||
QGraphicsLinearLayout *hTopRow = new QGraphicsLinearLayout(Qt::Horizontal);
|
||||
|
||||
|
|
@ -153,9 +159,9 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player,
|
|||
connect(&sortBySelector, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this,
|
||||
&ZoneViewWidget::processSortBy);
|
||||
connect(&pileViewCheckBox, &QCheckBox::QT_STATE_CHANGED, this, &ZoneViewWidget::processSetPileView);
|
||||
groupBySelector.setCurrentIndex(SettingsCache::instance().getZoneViewGroupByIndex());
|
||||
sortBySelector.setCurrentIndex(SettingsCache::instance().getZoneViewSortByIndex());
|
||||
pileViewCheckBox.setChecked(SettingsCache::instance().getZoneViewPileView());
|
||||
groupBySelector.setCurrentIndex(SettingsCache::instance().userInterface().getZoneViewGroupByIndex());
|
||||
sortBySelector.setCurrentIndex(SettingsCache::instance().userInterface().getZoneViewSortByIndex());
|
||||
pileViewCheckBox.setChecked(SettingsCache::instance().userInterface().getZoneViewPileView());
|
||||
|
||||
if (CardList::NoSort == static_cast<CardList::SortOption>(groupBySelector.currentData().toInt())) {
|
||||
pileViewCheckBox.setEnabled(false);
|
||||
|
|
@ -185,7 +191,7 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player,
|
|||
void ZoneViewWidget::processGroupBy(int index)
|
||||
{
|
||||
auto option = static_cast<CardList::SortOption>(groupBySelector.itemData(index).toInt());
|
||||
SettingsCache::instance().setZoneViewGroupByIndex(index);
|
||||
SettingsCache::instance().userInterface().setZoneViewGroupByIndex(index);
|
||||
zone->setGroupBy(option);
|
||||
|
||||
// disable pile view checkbox if we're not grouping by anything
|
||||
|
|
@ -209,13 +215,13 @@ void ZoneViewWidget::processSortBy(int index)
|
|||
return;
|
||||
}
|
||||
|
||||
SettingsCache::instance().setZoneViewSortByIndex(index);
|
||||
SettingsCache::instance().userInterface().setZoneViewSortByIndex(index);
|
||||
zone->setSortBy(option);
|
||||
}
|
||||
|
||||
void ZoneViewWidget::processSetPileView(QT_STATE_CHANGED_T value)
|
||||
{
|
||||
SettingsCache::instance().setZoneViewPileView(value);
|
||||
SettingsCache::instance().userInterface().setZoneViewPileView(value);
|
||||
zone->setPileView(value);
|
||||
}
|
||||
|
||||
|
|
@ -472,7 +478,7 @@ static qreal rowsToHeight(int rows)
|
|||
**/
|
||||
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()
|
||||
{
|
||||
qreal maxInitialHeight = calcMaxInitialHeight();
|
||||
qreal maxExpandedHeight = rowsToHeight(SettingsCache::instance().getCardViewExpandedRowsMax());
|
||||
qreal maxExpandedHeight = rowsToHeight(SettingsCache::instance().userInterface().getCardViewExpandedRowsMax());
|
||||
qreal height = rect().height() - extraHeight - 10;
|
||||
qreal maxHeight = maximumHeight() - extraHeight - 10;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
#include "card_picture_loader.h"
|
||||
|
||||
#include "../../client/settings/cache_settings.h"
|
||||
#include "card_picture_loader_cache_method.h"
|
||||
#include "card_picture_loader_local_schemes.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QBuffer>
|
||||
|
|
@ -16,16 +18,23 @@
|
|||
#include <QStatusBar>
|
||||
#include <QThread>
|
||||
#include <algorithm>
|
||||
#include <libcockatrice/settings/cache_storage_settings.h>
|
||||
#include <libcockatrice/settings/download_settings.h>
|
||||
#include <libcockatrice/settings/paths_settings.h>
|
||||
#include <utility>
|
||||
|
||||
// never cache more than 300 cards at once for a single deck
|
||||
#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)
|
||||
{
|
||||
worker = new CardPictureLoaderWorker;
|
||||
connect(&SettingsCache::instance(), &SettingsCache::picsPathChanged, this, &CardPictureLoader::picsPathChanged);
|
||||
connect(&SettingsCache::instance(), &SettingsCache::picDownloadChanged, this,
|
||||
connect(&SettingsCache::instance().paths(), &PathsSettings::picsPathChanged, this,
|
||||
&CardPictureLoader::picsPathChanged);
|
||||
connect(&SettingsCache::instance().downloads(), &DownloadSettings::picDownloadChanged, this,
|
||||
&CardPictureLoader::picDownloadChanged);
|
||||
|
||||
qRegisterMetaType<ExactCard>();
|
||||
|
|
@ -129,7 +138,14 @@ void CardPictureLoader::getPixmap(QPixmap &pixmap, const ExactCard &card, QSize
|
|||
QPixmap bigPixmap;
|
||||
if (QPixmapCache::find(key, &bigPixmap)) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -153,8 +169,10 @@ void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image)
|
|||
QPixmap finalPixmap;
|
||||
|
||||
if (image.isNull()) {
|
||||
getInstance().failedAt.insert(card.getPixmapCacheKey(), QDateTime::currentDateTime());
|
||||
qCDebug(CardPictureLoaderLog) << "Caching NULL pixmap for" << card.getName();
|
||||
} else {
|
||||
getInstance().failedAt.remove(card.getPixmapCacheKey());
|
||||
if (card.getInfo().getUiAttributes().upsideDownArt) {
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 9, 0))
|
||||
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);
|
||||
|
||||
if (SettingsCache::instance().getCardPictureLoaderCacheMethod() ==
|
||||
if (static_cast<CardPictureLoaderCacheMethod::CacheMethod>(
|
||||
SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) ==
|
||||
CardPictureLoaderCacheMethod::CacheMethod::FILESYSTEM_CACHE) {
|
||||
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.
|
||||
// (plus there's a deduplication mechanism in CardPictureLoaderWorker)
|
||||
// It should be safe to connect the CardInfo here without worrying about redundant connections.
|
||||
connect(card.getCardPtr().data(), &QObject::destroyed, this,
|
||||
[cacheKey = card.getPixmapCacheKey()] { QPixmapCache::remove(cacheKey); });
|
||||
connect(card.getCardPtr().data(), &QObject::destroyed, this, [cacheKey = card.getPixmapCacheKey()] {
|
||||
QPixmapCache::remove(cacheKey);
|
||||
getInstance().failedAt.remove(cacheKey);
|
||||
});
|
||||
|
||||
card.emitPixmapUpdated();
|
||||
}
|
||||
|
|
@ -189,9 +210,9 @@ void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card, const
|
|||
return;
|
||||
}
|
||||
|
||||
const QString picsRoot = SettingsCache::instance().getPicsPath();
|
||||
CardPictureLoaderLocalSchemes::NamingScheme scheme =
|
||||
SettingsCache::instance().getLocalCardImageStorageNamingScheme();
|
||||
const QString picsRoot = SettingsCache::instance().paths().getPicsPath();
|
||||
CardPictureLoaderLocalSchemes::NamingScheme scheme = static_cast<CardPictureLoaderLocalSchemes::NamingScheme>(
|
||||
SettingsCache::instance().cacheStorage().getLocalCardImageStorageNamingScheme());
|
||||
|
||||
QString pattern;
|
||||
|
||||
|
|
@ -306,7 +327,7 @@ void CardPictureLoader::picsPathChanged()
|
|||
|
||||
bool CardPictureLoader::hasCustomArt()
|
||||
{
|
||||
auto picsPath = SettingsCache::instance().getPicsPath();
|
||||
auto picsPath = SettingsCache::instance().paths().getPicsPath();
|
||||
QDirIterator it(picsPath, QDir::Dirs | QDir::NoDotAndDotDot);
|
||||
|
||||
// Check if there is at least one non-directory file in the pics path, other
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
#include "card_picture_loader_status_bar.h"
|
||||
#include "card_picture_loader_worker.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QHash>
|
||||
#include <QLoggingCategory>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(CardPictureLoaderLog, "card_picture_loader");
|
||||
|
|
@ -56,6 +58,7 @@ private:
|
|||
|
||||
CardPictureLoaderWorker *worker; ///< Worker thread for async image loading
|
||||
CardPictureLoaderStatusBar *statusBar; ///< Status bar widget showing load progress
|
||||
QHash<QString, QDateTime> failedAt; ///< Timestamp of the last failed load attempt per pixmap cache key
|
||||
|
||||
public:
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -7,15 +7,16 @@
|
|||
#include <QDirIterator>
|
||||
#include <QMovie>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/settings/paths_settings.h>
|
||||
|
||||
static constexpr int REFRESH_INTERVAL_MS = 10 * 1000;
|
||||
|
||||
CardPictureLoaderLocal::CardPictureLoaderLocal(QObject *parent)
|
||||
: QObject(parent), picsPath(SettingsCache::instance().getPicsPath()),
|
||||
customPicsPath(SettingsCache::instance().getCustomPicsPath())
|
||||
: QObject(parent), picsPath(SettingsCache::instance().paths().getPicsPath()),
|
||||
customPicsPath(SettingsCache::instance().paths().getCustomPicsPath())
|
||||
{
|
||||
// Hook up signals to settings
|
||||
connect(&SettingsCache::instance(), &SettingsCache::picsPathChanged, this,
|
||||
connect(&SettingsCache::instance().paths(), &PathsSettings::picsPathChanged, this,
|
||||
&CardPictureLoaderLocal::picsPathChanged);
|
||||
|
||||
refreshIndex();
|
||||
|
|
@ -127,6 +128,6 @@ QImage CardPictureLoaderLocal::tryLoadCardImageFromDisk(const QString &setName,
|
|||
|
||||
void CardPictureLoaderLocal::picsPathChanged()
|
||||
{
|
||||
picsPath = SettingsCache::instance().getPicsPath();
|
||||
customPicsPath = SettingsCache::instance().getCustomPicsPath();
|
||||
picsPath = SettingsCache::instance().paths().getPicsPath();
|
||||
customPicsPath = SettingsCache::instance().paths().getCustomPicsPath();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "card_picture_loader_worker.h"
|
||||
|
||||
#include "../../client/settings/cache_settings.h"
|
||||
#include "card_picture_loader_cache_method.h"
|
||||
#include "card_picture_loader_local.h"
|
||||
#include "card_picture_loader_worker_work.h"
|
||||
|
||||
|
|
@ -9,13 +10,19 @@
|
|||
#include <QNetworkDiskCache>
|
||||
#include <QNetworkReply>
|
||||
#include <QThread>
|
||||
#include <libcockatrice/settings/cache_storage_settings.h>
|
||||
#include <libcockatrice/settings/download_settings.h>
|
||||
#include <libcockatrice/settings/paths_settings.h>
|
||||
#include <utility>
|
||||
#include <version_string.h>
|
||||
|
||||
static constexpr int MAX_REQUESTS_PER_SEC = 10;
|
||||
static constexpr int MIN_HOST_QUOTA = 1; ///< Floor for the per-host request allowance
|
||||
static constexpr qint64 QUOTA_RECOVER_MS = 60000; ///< Idle time before a reduced quota starts recovering
|
||||
|
||||
CardPictureLoaderWorker::CardPictureLoaderWorker()
|
||||
: QObject(nullptr), picDownload(SettingsCache::instance().getPicDownload()), requestQuota(MAX_REQUESTS_PER_SEC)
|
||||
: QObject(nullptr), picDownload(SettingsCache::instance().downloads().getPicDownload()),
|
||||
requestQuota(MAX_REQUESTS_PER_SEC)
|
||||
{
|
||||
networkManager = new QNetworkAccessManager(this);
|
||||
// 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->setCacheDirectory(SettingsCache::instance().getNetworkCachePath());
|
||||
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) {
|
||||
if (cache) {
|
||||
cache->setMaximumCacheSize(1024L * 1024L * static_cast<qint64>(newSizeInMB));
|
||||
}
|
||||
});
|
||||
connect(&SettingsCache::instance().cacheStorage(), &CacheStorageSettings::networkCacheSizeChanged, cache,
|
||||
[this](int newSizeInMB) {
|
||||
if (cache) {
|
||||
cache->setMaximumCacheSize(1024L * 1024L * static_cast<qint64>(newSizeInMB));
|
||||
}
|
||||
});
|
||||
|
||||
networkManager->setCache(cache);
|
||||
|
||||
|
|
@ -39,7 +47,7 @@ CardPictureLoaderWorker::CardPictureLoaderWorker()
|
|||
// We can't use NoLessSafeRedirectPolicy because it is not applied with AlwaysCache
|
||||
networkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy);
|
||||
|
||||
cacheFilePath = SettingsCache::instance().getRedirectCachePath() + REDIRECT_CACHE_FILENAME;
|
||||
cacheFilePath = SettingsCache::instance().paths().getRedirectCachePath() + REDIRECT_CACHE_FILENAME;
|
||||
loadRedirectCache();
|
||||
cleanStaleEntries();
|
||||
|
||||
|
|
@ -72,7 +80,8 @@ void CardPictureLoaderWorker::queueRequest(const QUrl &url, CardPictureLoaderWor
|
|||
queueRequest(cachedRedirect, worker);
|
||||
return;
|
||||
}
|
||||
if (SettingsCache::instance().getCardPictureLoaderCacheMethod() ==
|
||||
if (static_cast<CardPictureLoaderCacheMethod::CacheMethod>(
|
||||
SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) ==
|
||||
CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE &&
|
||||
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
|
||||
|
|
@ -98,8 +107,10 @@ QNetworkReply *CardPictureLoaderWorker::makeRequest(const QUrl &url, CardPicture
|
|||
req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
||||
req.setRawHeader("Accept", "image/avif,image/webp,image/apng,image/,/*;q=0.8");
|
||||
|
||||
bool useNetworkCache = !picDownload && SettingsCache::instance().getCardPictureLoaderCacheMethod() ==
|
||||
CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE;
|
||||
bool useNetworkCache =
|
||||
!picDownload && static_cast<CardPictureLoaderCacheMethod::CacheMethod>(
|
||||
SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) ==
|
||||
CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE;
|
||||
|
||||
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute,
|
||||
useNetworkCache ? QNetworkRequest::AlwaysCache : QNetworkRequest::AlwaysNetwork);
|
||||
|
|
@ -115,6 +126,19 @@ QNetworkReply *CardPictureLoaderWorker::makeRequest(const QUrl &url, CardPicture
|
|||
void CardPictureLoaderWorker::resetRequestQuota()
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
@ -127,14 +151,26 @@ void CardPictureLoaderWorker::processQueuedRequests()
|
|||
|
||||
bool CardPictureLoaderWorker::processSingleRequest()
|
||||
{
|
||||
if (!requestLoadQueue.isEmpty()) {
|
||||
auto request = requestLoadQueue.takeFirst();
|
||||
makeRequest(request.first, request.second);
|
||||
return true;
|
||||
for (int i = 0; i < requestLoadQueue.size(); ++i) {
|
||||
const auto &request = requestLoadQueue.at(i);
|
||||
QString host = request.first.host();
|
||||
int allowance = hostQuotaRemaining.value(host, MAX_REQUESTS_PER_SEC);
|
||||
if (allowance > 0) {
|
||||
hostQuotaRemaining.insert(host, allowance - 1);
|
||||
makeRequest(request.first, request.second);
|
||||
requestLoadQueue.removeAt(i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
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)
|
||||
{
|
||||
// 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();
|
||||
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
|
||||
} else {
|
||||
++it;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@
|
|||
#include "card_picture_loader_worker_work.h"
|
||||
#include "card_picture_to_load.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QHash>
|
||||
#include <QLoggingCategory>
|
||||
#include <QMutex>
|
||||
#include <QNetworkAccessManager>
|
||||
|
|
@ -66,6 +68,12 @@ public:
|
|||
*/
|
||||
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. */
|
||||
void clearNetworkCache();
|
||||
|
||||
|
|
@ -110,8 +118,11 @@ private:
|
|||
bool picDownload; ///< Whether downloading images from network is enabled
|
||||
QQueue<QPair<QUrl, CardPictureLoaderWorkerWork *>> requestLoadQueue; ///< Queue of pending network requests
|
||||
|
||||
int requestQuota; ///< Remaining requests allowed per second
|
||||
QTimer requestTimer; ///< Timer to reset the request quota
|
||||
int requestQuota; ///< Remaining requests allowed per second
|
||||
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
|
||||
QSet<QString> currentlyLoading; ///< Deduplication: contains pixmapCacheKey currently being loaded
|
||||
|
|
|
|||
|
|
@ -8,8 +8,13 @@
|
|||
#include <QLoggingCategory>
|
||||
#include <QMovie>
|
||||
#include <QNetworkReply>
|
||||
#include <QRandomGenerator>
|
||||
#include <QThread>
|
||||
#include <QThreadPool>
|
||||
#include <QTimer>
|
||||
|
||||
ServerRateLimiter CardPictureLoaderWorkerWork::s_rateLimiter;
|
||||
#include <libcockatrice/settings/download_settings.h>
|
||||
|
||||
// Card back returned by gatherer when card is not found
|
||||
static const QStringList MD5_BLACKLIST = {
|
||||
|
|
@ -19,7 +24,7 @@ static const QStringList MD5_BLACKLIST = {
|
|||
|
||||
CardPictureLoaderWorkerWork::CardPictureLoaderWorkerWork(const CardPictureLoaderWorker *worker, const ExactCard &toLoad)
|
||||
: QObject(nullptr), cardToDownload(CardPictureToLoad(toLoad)),
|
||||
picDownload(SettingsCache::instance().getPicDownload())
|
||||
picDownload(SettingsCache::instance().downloads().getPicDownload())
|
||||
{
|
||||
// Hook up signals to the orchestrator
|
||||
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::requestSucceeded, worker,
|
||||
&CardPictureLoaderWorker::imageRequestSucceeded);
|
||||
connect(this, &CardPictureLoaderWorkerWork::rateLimited, worker, &CardPictureLoaderWorker::onHostRateLimited);
|
||||
|
||||
// Hook up signals to settings
|
||||
connect(&SettingsCache::instance(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged()));
|
||||
connect(&SettingsCache::instance().downloads(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged()));
|
||||
|
||||
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();
|
||||
|
||||
if (picUrl.isEmpty()) {
|
||||
picDownloadFailed();
|
||||
scheduleDeferredRetry();
|
||||
} else {
|
||||
QUrl url(picUrl);
|
||||
qCDebug(CardPictureLoaderWorkerWorkLog).nospace()
|
||||
|
|
@ -107,7 +141,41 @@ static bool imageIsBlackListed(const QByteArray &picData)
|
|||
void CardPictureLoaderWorkerWork::handleFailedReply(const QNetworkReply *reply)
|
||||
{
|
||||
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 {
|
||||
bool isFromCache = reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool();
|
||||
|
||||
|
|
@ -148,6 +216,9 @@ void CardPictureLoaderWorkerWork::handleSuccessfulReply(QNetworkReply *reply)
|
|||
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
|
||||
const QByteArray &picData = reply->peek(reply->size());
|
||||
|
||||
|
|
@ -202,6 +273,42 @@ QImage CardPictureLoaderWorkerWork::tryLoadImageFromReply(QNetworkReply *reply)
|
|||
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)
|
||||
{
|
||||
emit imageLoaded(cardToDownload.getCard(), image);
|
||||
|
|
@ -210,5 +317,5 @@ void CardPictureLoaderWorkerWork::concludeImageLoad(const QImage &image)
|
|||
|
||||
void CardPictureLoaderWorkerWork::picDownloadChanged()
|
||||
{
|
||||
picDownload = SettingsCache::instance().getPicDownload();
|
||||
picDownload = SettingsCache::instance().downloads().getPicDownload();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,15 @@
|
|||
#include "card_picture_loader_worker.h"
|
||||
#include "card_picture_to_load.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QLoggingCategory>
|
||||
#include <QMutex>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QObject>
|
||||
#include <QRandomGenerator>
|
||||
#include <QThread>
|
||||
#include <libcockatrice/card/database/card_database.h>
|
||||
#include <libcockatrice/utility/server_rate_limiter.h>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(CardPictureLoaderWorkerWorkLog, "card_picture_loader.worker");
|
||||
|
||||
|
|
@ -50,6 +53,8 @@ public slots:
|
|||
private:
|
||||
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. */
|
||||
void startNextPicDownload();
|
||||
|
||||
|
|
@ -77,6 +82,16 @@ private:
|
|||
*/
|
||||
void concludeImageLoad(const QImage &image);
|
||||
|
||||
/**
|
||||
* @brief Schedules a deferred retry after the relevant server backoff expires.
|
||||
*
|
||||
* Waits on the current URL's server when it is the reason we are blocked,
|
||||
* otherwise on the earliest active backoff. If no servers are in backoff,
|
||||
* concludes with failure. Otherwise resets the CardPictureToLoad indices and
|
||||
* retries after the backoff period.
|
||||
*/
|
||||
void scheduleDeferredRetry();
|
||||
|
||||
private slots:
|
||||
/** @brief Updates the picDownload setting when it changes. */
|
||||
void picDownloadChanged();
|
||||
|
|
@ -100,6 +115,9 @@ signals:
|
|||
/** @brief Emitted when a URL has been redirected. */
|
||||
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. */
|
||||
void cachedUrlInvalidated(const QUrl &url);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,14 +9,17 @@
|
|||
#include <algorithm>
|
||||
#include <libcockatrice/card/set/card_set_comparator.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)
|
||||
: card(_card), urlTemplates(SettingsCache::instance().downloads().getAllURLs())
|
||||
{
|
||||
if (card) {
|
||||
sortedSets = extractSetsSorted(card);
|
||||
// The first time called, nextSet will also populate the Urls for the first set.
|
||||
nextSet();
|
||||
currentSetIndex = 0;
|
||||
currentSet = sortedSets.first();
|
||||
populateSetUrls();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -34,7 +37,7 @@ QList<CardSetPtr> CardPictureToLoad::extractSetsSorted(const ExactCard &card)
|
|||
std::sort(sortedSets.begin(), sortedSets.end(), SetPriorityComparator());
|
||||
|
||||
// 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.
|
||||
qsizetype setIndex = sortedSets.indexOf(card.getPrinting().getSet());
|
||||
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
|
||||
but we don't need the result here. */
|
||||
(void)nextUrl();
|
||||
currentUrlIndex = 0;
|
||||
if (!currentSetUrls.isEmpty()) {
|
||||
currentUrl = currentSetUrls.first();
|
||||
} else {
|
||||
currentUrl = QString();
|
||||
}
|
||||
}
|
||||
|
||||
bool CardPictureToLoad::nextSet()
|
||||
{
|
||||
if (!sortedSets.isEmpty()) {
|
||||
currentSet = sortedSets.takeFirst();
|
||||
currentSetIndex++;
|
||||
if (currentSetIndex < sortedSets.size()) {
|
||||
currentSet = sortedSets.at(currentSetIndex);
|
||||
populateSetUrls();
|
||||
return true;
|
||||
}
|
||||
|
|
@ -117,8 +124,9 @@ bool CardPictureToLoad::nextSet()
|
|||
|
||||
bool CardPictureToLoad::nextUrl()
|
||||
{
|
||||
if (!currentSetUrls.isEmpty()) {
|
||||
currentUrl = currentSetUrls.takeFirst();
|
||||
currentUrlIndex++;
|
||||
if (currentUrlIndex < currentSetUrls.size()) {
|
||||
currentUrl = currentSetUrls.at(currentUrlIndex);
|
||||
return true;
|
||||
}
|
||||
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,
|
||||
const QString &propType,
|
||||
const QString &cardName,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ private:
|
|||
QList<QString> currentSetUrls; ///< URLs for the current set being attempted
|
||||
QString currentUrl; ///< Currently active URL to download
|
||||
CardSetPtr currentSet; ///< Currently active set
|
||||
int currentSetIndex = 0; ///< Current position in sortedSets
|
||||
int currentUrlIndex = 0; ///< Current position in currentSetUrls
|
||||
|
||||
public:
|
||||
/**
|
||||
|
|
@ -56,6 +58,9 @@ public:
|
|||
/** @return The short name of the current set, or empty string if no set. */
|
||||
[[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.
|
||||
* @param urlTemplate The URL template to transform
|
||||
|
|
@ -88,6 +93,14 @@ public:
|
|||
*/
|
||||
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.
|
||||
* @param card The card to extract sets from
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue