mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-21 09:05:10 -07:00
Merge branch 'master' into tooomm-qt_minimal
This commit is contained in:
commit
c08be9c5a6
611 changed files with 91544 additions and 49123 deletions
|
|
@ -159,6 +159,7 @@ if [[ $PACKAGE_TYPE ]]; then
|
||||||
fi
|
fi
|
||||||
if [[ $USE_VCPKG ]]; then
|
if [[ $USE_VCPKG ]]; then
|
||||||
flags+=("-DUSE_VCPKG=1")
|
flags+=("-DUSE_VCPKG=1")
|
||||||
|
flags+=("-DVCPKG_INSTALL_OPTIONS=--x-abi-tools-use-exact-versions")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Add cmake --build flags
|
# Add cmake --build flags
|
||||||
|
|
|
||||||
|
|
@ -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);
|
QString message = tr("Everyone draws %n cards", "english hint for translators", amount);
|
||||||
```
|
```
|
||||||
See [Qt's wiki on translations](
|
See [Qt's wiki on translations](
|
||||||
https://doc.qt.io/qt-5/i18n-source-translation.html#handling-plurals)
|
https://doc.qt.io/qt-6/i18n-source-translation.html#handling-plurals)
|
||||||
|
|
||||||
If you're about to propose a change that adds or modifies any translatable
|
If you're about to propose a change that adds or modifies any translatable
|
||||||
string in the code, you don't need to take care of adding the new strings to
|
string in the code, you don't need to take care of adding the new strings to
|
||||||
|
|
|
||||||
48
.github/workflows/desktop-build.yml
vendored
48
.github/workflows/desktop-build.yml
vendored
|
|
@ -3,7 +3,7 @@ name: Build Desktop
|
||||||
permissions:
|
permissions:
|
||||||
actions: write # needed to delete entries in GHA cache (update ccache)
|
actions: write # needed to delete entries in GHA cache (update ccache)
|
||||||
attestations: write # needed to persist the attestation.
|
attestations: write # needed to persist the attestation.
|
||||||
contents: write
|
contents: write # needed for e.g. vcpkg dependency graph updates
|
||||||
id-token: write # needed for signing certificate in attestation
|
id-token: write # needed for signing certificate in attestation
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
|
@ -42,7 +42,7 @@ concurrency:
|
||||||
jobs:
|
jobs:
|
||||||
configure:
|
configure:
|
||||||
name: Configure
|
name: Configure
|
||||||
runs-on: ubuntu-slim
|
runs-on: ubuntu-slim # https://github.com/actions/runner-images/blob/main/images/ubuntu-slim/ubuntu-slim-Readme.md
|
||||||
outputs:
|
outputs:
|
||||||
tag: ${{ steps.configure.outputs.tag }}
|
tag: ${{ steps.configure.outputs.tag }}
|
||||||
sha: ${{ steps.configure.outputs.sha }}
|
sha: ${{ steps.configure.outputs.sha }}
|
||||||
|
|
@ -146,7 +146,7 @@ jobs:
|
||||||
|
|
||||||
name: ${{ matrix.distro }} ${{ matrix.version }}
|
name: ${{ matrix.distro }} ${{ matrix.version }}
|
||||||
needs: configure
|
needs: configure
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest # https://github.com/actions/runner-images
|
||||||
continue-on-error: ${{ matrix.allow-failure == 'yes' }}
|
continue-on-error: ${{ matrix.allow-failure == 'yes' }}
|
||||||
timeout-minutes: 70
|
timeout-minutes: 70
|
||||||
env:
|
env:
|
||||||
|
|
@ -262,84 +262,80 @@ jobs:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: macOS
|
- os: macOS
|
||||||
target: 13
|
target: 13 # EOL 2025-09-15
|
||||||
runner: macos-15-intel
|
runner: macos-15-intel # https://github.com/actions/runner-images/blob/main/images/macos/macos-15-Readme.md
|
||||||
|
|
||||||
ccache_eviction_age: 7d
|
ccache_eviction_age: 7d
|
||||||
cmake_generator: Ninja
|
cmake_generator: Ninja
|
||||||
make_package: 1
|
make_package: 1
|
||||||
override_target: 13
|
override_target: 13
|
||||||
package_suffix: "-macOS13_Intel"
|
package_suffix: "-macOS13_Intel"
|
||||||
qt_arch: clang_64
|
|
||||||
qt_archives: qtbase qtimageformats qtmultimedia qtsvg qttools qttranslations
|
qt_archives: qtbase qtimageformats qtmultimedia qtsvg qttools qttranslations
|
||||||
qt_modules: qtwebsockets
|
qt_modules: qtwebsockets
|
||||||
qt_version: 6.11.0
|
qt_version: 6.11.1
|
||||||
soc: Intel
|
soc: Intel
|
||||||
type: Release
|
type: Release
|
||||||
use_ccache: 1
|
use_ccache: 1
|
||||||
xcode: "16.4"
|
xcode: "26.3"
|
||||||
|
|
||||||
- os: macOS
|
- os: macOS
|
||||||
target: 14
|
target: 14 # EOL 2026-??
|
||||||
runner: macos-14
|
runner: macos-15 # https://github.com/actions/runner-images/blob/main/images/macos/macos-15-arm64-Readme.md
|
||||||
|
|
||||||
ccache_eviction_age: 7d
|
ccache_eviction_age: 7d
|
||||||
cmake_generator: Ninja
|
cmake_generator: Ninja
|
||||||
make_package: 1
|
make_package: 1
|
||||||
|
override_target: 14
|
||||||
package_suffix: "-macOS14"
|
package_suffix: "-macOS14"
|
||||||
qt_arch: clang_64
|
|
||||||
qt_archives: qtbase qtimageformats qtmultimedia qtsvg qttools qttranslations
|
qt_archives: qtbase qtimageformats qtmultimedia qtsvg qttools qttranslations
|
||||||
qt_modules: qtwebsockets
|
qt_modules: qtwebsockets
|
||||||
qt_version: 6.11.0
|
qt_version: 6.11.1
|
||||||
soc: Apple
|
soc: Apple
|
||||||
type: Release
|
type: Release
|
||||||
use_ccache: 1
|
use_ccache: 1
|
||||||
xcode: "15.4"
|
xcode: "26.3"
|
||||||
|
|
||||||
- os: macOS
|
- os: macOS
|
||||||
target: 15
|
target: 15
|
||||||
runner: macos-15
|
runner: macos-15 # https://github.com/actions/runner-images/blob/main/images/macos/macos-15-arm64-Readme.md
|
||||||
|
|
||||||
ccache_eviction_age: 7d
|
ccache_eviction_age: 7d
|
||||||
cmake_generator: Ninja
|
cmake_generator: Ninja
|
||||||
make_package: 1
|
make_package: 1
|
||||||
package_suffix: "-macOS15"
|
package_suffix: "-macOS15"
|
||||||
qt_arch: clang_64
|
|
||||||
qt_archives: qtbase qtimageformats qtmultimedia qtsvg qttools qttranslations
|
qt_archives: qtbase qtimageformats qtmultimedia qtsvg qttools qttranslations
|
||||||
qt_modules: qtwebsockets
|
qt_modules: qtwebsockets
|
||||||
qt_version: 6.11.0
|
qt_version: 6.11.1
|
||||||
soc: Apple
|
soc: Apple
|
||||||
type: Release
|
type: Release
|
||||||
use_ccache: 1
|
use_ccache: 1
|
||||||
xcode: "16.4"
|
xcode: "26.3"
|
||||||
|
|
||||||
- os: macOS
|
- os: macOS
|
||||||
target: 15
|
target: 15
|
||||||
runner: macos-15
|
runner: macos-15 # https://github.com/actions/runner-images/blob/main/images/macos/macos-15-arm64-Readme.md
|
||||||
|
|
||||||
ccache_eviction_age: 7d
|
ccache_eviction_age: 7d
|
||||||
cmake_generator: Ninja
|
cmake_generator: Ninja
|
||||||
qt_arch: clang_64
|
|
||||||
qt_archives: qtbase qtimageformats qtmultimedia qtsvg qttools qttranslations
|
qt_archives: qtbase qtimageformats qtmultimedia qtsvg qttools qttranslations
|
||||||
qt_modules: qtwebsockets
|
qt_modules: qtwebsockets
|
||||||
qt_version: 6.11.0
|
qt_version: 6.11.1
|
||||||
soc: Apple
|
soc: Apple
|
||||||
type: Debug
|
type: Debug
|
||||||
use_ccache: 1
|
use_ccache: 1
|
||||||
xcode: "16.4"
|
xcode: "26.3"
|
||||||
|
|
||||||
- os: Windows
|
- os: Windows
|
||||||
target: 10
|
target: 10
|
||||||
runner: windows-2025
|
runner: windows-2025 # https://github.com/actions/runner-images/blob/main/images/windows/Windows2025-VS2026-Readme.md
|
||||||
|
|
||||||
cmake_generator: "Visual Studio 18 2026"
|
cmake_generator: "Visual Studio 18 2026"
|
||||||
cmake_generator_platform: x64
|
cmake_generator_platform: x64
|
||||||
make_package: 1
|
make_package: 1
|
||||||
package_suffix: "-Win10"
|
package_suffix: "-Win10"
|
||||||
qt_arch: win64_msvc2022_64
|
|
||||||
qt_archives: qtbase qtimageformats qtmultimedia qtsvg qttools qttranslations
|
qt_archives: qtbase qtimageformats qtmultimedia qtsvg qttools qttranslations
|
||||||
qt_modules: qtwebsockets
|
qt_modules: qtwebsockets
|
||||||
qt_version: 6.11.0
|
qt_version: 6.11.1
|
||||||
type: Release
|
type: Release
|
||||||
|
|
||||||
name: ${{ matrix.os }} ${{ matrix.target }}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }}
|
name: ${{ matrix.os }} ${{ matrix.target }}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }}
|
||||||
|
|
@ -403,7 +399,6 @@ jobs:
|
||||||
if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true'
|
if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true'
|
||||||
uses: jurplel/install-qt-action@v4
|
uses: jurplel/install-qt-action@v4
|
||||||
with:
|
with:
|
||||||
arch: ${{ matrix.qt_arch }}
|
|
||||||
archives: ${{ matrix.qt_archives }}
|
archives: ${{ matrix.qt_archives }}
|
||||||
cache: false
|
cache: false
|
||||||
dir: ${{ github.workspace }}
|
dir: ${{ github.workspace }}
|
||||||
|
|
@ -427,7 +422,6 @@ jobs:
|
||||||
with:
|
with:
|
||||||
# Qt 6.11.0 only works with aqtinstall directly from git until aqtinstall 3.4 is released
|
# Qt 6.11.0 only works with aqtinstall directly from git until aqtinstall 3.4 is released
|
||||||
aqtsource: git+https://github.com/miurahr/aqtinstall.git
|
aqtsource: git+https://github.com/miurahr/aqtinstall.git
|
||||||
arch: ${{ matrix.qt_arch }}
|
|
||||||
archives: ${{ matrix.qt_archives }}
|
archives: ${{ matrix.qt_archives }}
|
||||||
cache: true
|
cache: true
|
||||||
modules: ${{ matrix.qt_modules }}
|
modules: ${{ matrix.qt_modules }}
|
||||||
|
|
@ -454,6 +448,7 @@ jobs:
|
||||||
CMAKE_GENERATOR: ${{ matrix.cmake_generator }}
|
CMAKE_GENERATOR: ${{ matrix.cmake_generator }}
|
||||||
CMAKE_GENERATOR_PLATFORM: ${{ matrix.cmake_generator_platform }}
|
CMAKE_GENERATOR_PLATFORM: ${{ matrix.cmake_generator_platform }}
|
||||||
DEVELOPER_DIR: '/Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer'
|
DEVELOPER_DIR: '/Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer'
|
||||||
|
GITHUB_TOKEN: ${{ github.token }} # needed for vcpkg dependency graph updates, see VCPKG_FEATURE_FLAGS
|
||||||
MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }}
|
MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }}
|
||||||
MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
|
MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
|
||||||
MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }}
|
MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }}
|
||||||
|
|
@ -464,6 +459,7 @@ jobs:
|
||||||
USE_CCACHE: ${{ matrix.use_ccache }}
|
USE_CCACHE: ${{ matrix.use_ccache }}
|
||||||
VCPKG_BINARY_SOURCES: 'clear;files,${{ steps.vcpkg-cache.outputs.path }},readwrite'
|
VCPKG_BINARY_SOURCES: 'clear;files,${{ steps.vcpkg-cache.outputs.path }},readwrite'
|
||||||
VCPKG_DISABLE_METRICS: 1
|
VCPKG_DISABLE_METRICS: 1
|
||||||
|
VCPKG_FEATURE_FLAGS: dependencygraph
|
||||||
run: .ci/compile.sh --server --test --vcpkg
|
run: .ci/compile.sh --server --test --vcpkg
|
||||||
|
|
||||||
# Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342
|
# Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342
|
||||||
|
|
|
||||||
7
.github/workflows/docker-release.yml
vendored
7
.github/workflows/docker-release.yml
vendored
|
|
@ -56,8 +56,9 @@ jobs:
|
||||||
- name: "Set up Docker buildx"
|
- name: "Set up Docker buildx"
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
- name: "Login to GitHub Container Registry"
|
- name: "Login to GitHub Container Registry (GHCR)"
|
||||||
if: contains(github.event.release.tag_name, 'Release') && github.event.release.target_commitish == 'master'
|
if: github.event_name == 'release' && github.event.release.prerelease == false
|
||||||
|
id: login
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
password: ${{ github.token }}
|
password: ${{ github.token }}
|
||||||
|
|
@ -73,5 +74,5 @@ jobs:
|
||||||
context: .
|
context: .
|
||||||
labels: ${{ steps.metadata.outputs.labels }}
|
labels: ${{ steps.metadata.outputs.labels }}
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
push: ${{ github.ref_type == 'tag' }}
|
push: ${{ steps.login.outcome == 'success' }}
|
||||||
tags: ${{ steps.metadata.outputs.tags }}
|
tags: ${{ steps.metadata.outputs.tags }}
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -6,6 +6,7 @@ mysql.cnf
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.idea/
|
.idea/
|
||||||
*.aps
|
*.aps
|
||||||
|
*.cache
|
||||||
cmake-build*
|
cmake-build*
|
||||||
preferences
|
preferences
|
||||||
compile_commands.json
|
compile_commands.json
|
||||||
|
|
|
||||||
|
|
@ -64,10 +64,11 @@ if(WIN32 OR USE_VCPKG)
|
||||||
else()
|
else()
|
||||||
set(QTDIR
|
set(QTDIR
|
||||||
""
|
""
|
||||||
CACHE PATH "Path to Qt (e.g. C:/Qt/5.7/msvc2015_64)"
|
CACHE PATH "Path to Qt (e.g. C:/Qt/6.4.2/msvc2019_64)"
|
||||||
)
|
)
|
||||||
message(
|
message(
|
||||||
WARNING "QTDIR variable is missing. Please set this variable to specify path to Qt (e.g. C:/Qt/5.7/msvc2015_64)"
|
WARNING
|
||||||
|
"QTDIR variable is missing. Please set this variable to specify path to Qt (e.g. C:/Qt/6.4.2/msvc2019_64)"
|
||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
@ -174,7 +175,7 @@ elseif(CMAKE_COMPILER_IS_GNUCXX)
|
||||||
-Wno-error=delete-non-virtual-dtor
|
-Wno-error=delete-non-virtual-dtor
|
||||||
-Wno-error=sign-compare
|
-Wno-error=sign-compare
|
||||||
-Wno-error=missing-declarations
|
-Wno-error=missing-declarations
|
||||||
-Wno-error=sfinae-incomplete # GCC 16+: Qt MOC + protobuf forward decls trigger this
|
-Wno-error=sfinae-incomplete # GCC 16+: Qt MOC + protobuf forward decls trigger this
|
||||||
)
|
)
|
||||||
|
|
||||||
foreach(FLAG ${ADDITIONAL_DEBUG_FLAGS})
|
foreach(FLAG ${ADDITIONAL_DEBUG_FLAGS})
|
||||||
|
|
@ -280,11 +281,7 @@ if(UNIX)
|
||||||
if(CPACK_GENERATOR STREQUAL "RPM")
|
if(CPACK_GENERATOR STREQUAL "RPM")
|
||||||
set(CPACK_RPM_PACKAGE_LICENSE "GPLv2")
|
set(CPACK_RPM_PACKAGE_LICENSE "GPLv2")
|
||||||
set(CPACK_RPM_MAIN_COMPONENT "cockatrice")
|
set(CPACK_RPM_MAIN_COMPONENT "cockatrice")
|
||||||
if(Qt6_FOUND)
|
set(CPACK_RPM_PACKAGE_REQUIRES "protobuf, qt6-qttools, qt6-qtsvg, qt6-qtmultimedia, qt6-qtimageformats")
|
||||||
set(CPACK_RPM_PACKAGE_REQUIRES "protobuf, qt6-qttools, qt6-qtsvg, qt6-qtmultimedia, qt6-qtimageformats")
|
|
||||||
elseif(Qt5_FOUND)
|
|
||||||
set(CPACK_RPM_PACKAGE_REQUIRES "protobuf, qt5-qttools, qt5-qtsvg, qt5-qtmultimedia")
|
|
||||||
endif()
|
|
||||||
set(CPACK_RPM_PACKAGE_GROUP "Amusements/Games")
|
set(CPACK_RPM_PACKAGE_GROUP "Amusements/Games")
|
||||||
set(CPACK_RPM_PACKAGE_URL "http://github.com/Cockatrice/Cockatrice")
|
set(CPACK_RPM_PACKAGE_URL "http://github.com/Cockatrice/Cockatrice")
|
||||||
# stop directories from making package conflicts
|
# stop directories from making package conflicts
|
||||||
|
|
@ -302,12 +299,8 @@ if(UNIX)
|
||||||
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
|
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
|
||||||
set(CPACK_DEBIAN_PACKAGE_SECTION "games")
|
set(CPACK_DEBIAN_PACKAGE_SECTION "games")
|
||||||
set(CPACK_DEBIAN_PACKAGE_HOMEPAGE "http://github.com/Cockatrice/Cockatrice")
|
set(CPACK_DEBIAN_PACKAGE_HOMEPAGE "http://github.com/Cockatrice/Cockatrice")
|
||||||
if(Qt6_FOUND)
|
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt6multimedia6, libqt6svg6, qt6-qpa-plugins, qt6-image-formats-plugins")
|
||||||
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt6multimedia6, libqt6svg6, qt6-qpa-plugins, qt6-image-formats-plugins")
|
set(CPACK_DEBIAN_PACKAGE_RECOMMENDS "libqt6sql6-mysql") # for connecting servatrice to a mysql db
|
||||||
set(CPACK_DEBIAN_PACKAGE_RECOMMENDS "libqt6sql6-mysql") # for connecting servatrice to a mysql db
|
|
||||||
elseif(Qt5_FOUND)
|
|
||||||
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt5multimedia5-plugins, libqt5svg5")
|
|
||||||
endif()
|
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
elseif(WIN32)
|
elseif(WIN32)
|
||||||
|
|
|
||||||
8
Doxyfile
8
Doxyfile
|
|
@ -349,7 +349,7 @@ OPTIMIZE_OUTPUT_SLICE = NO
|
||||||
#
|
#
|
||||||
# Note see also the list of default file extension mappings.
|
# Note see also the list of default file extension mappings.
|
||||||
|
|
||||||
EXTENSION_MAPPING =
|
EXTENSION_MAPPING = proto=C++
|
||||||
|
|
||||||
# If the MARKDOWN_SUPPORT tag is enabled then Doxygen pre-processes all comments
|
# If the MARKDOWN_SUPPORT tag is enabled then Doxygen pre-processes all comments
|
||||||
# according to the Markdown format, which allows for more readable
|
# according to the Markdown format, which allows for more readable
|
||||||
|
|
@ -1086,7 +1086,8 @@ FILE_PATTERNS = *.cc \
|
||||||
*.h++ \
|
*.h++ \
|
||||||
*.markdown \
|
*.markdown \
|
||||||
*.md \
|
*.md \
|
||||||
*.dox
|
*.dox \
|
||||||
|
*.proto
|
||||||
|
|
||||||
# The RECURSIVE tag can be used to specify whether or not subdirectories should
|
# The RECURSIVE tag can be used to specify whether or not subdirectories should
|
||||||
# be searched for input files as well.
|
# be searched for input files as well.
|
||||||
|
|
@ -1103,6 +1104,7 @@ RECURSIVE = YES
|
||||||
|
|
||||||
EXCLUDE = build/ \
|
EXCLUDE = build/ \
|
||||||
cmake/ \
|
cmake/ \
|
||||||
|
cmake-build-debug/ \
|
||||||
doc/doxygen/theme/docs/ \
|
doc/doxygen/theme/docs/ \
|
||||||
doc/doxygen/theme/include/ \
|
doc/doxygen/theme/include/ \
|
||||||
vcpkg/
|
vcpkg/
|
||||||
|
|
@ -1195,7 +1197,7 @@ INPUT_FILTER =
|
||||||
# need to set EXTENSION_MAPPING for the extension otherwise the files are not
|
# need to set EXTENSION_MAPPING for the extension otherwise the files are not
|
||||||
# properly processed by Doxygen.
|
# properly processed by Doxygen.
|
||||||
|
|
||||||
FILTER_PATTERNS =
|
FILTER_PATTERNS = "*.proto=python doc/doxygen/filters/proto2cpp.py"
|
||||||
|
|
||||||
# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using
|
# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using
|
||||||
# INPUT_FILTER) will also be used to filter the input files that are used for
|
# INPUT_FILTER) will also be used to filter the input files that are used for
|
||||||
|
|
|
||||||
|
|
@ -158,7 +158,6 @@ The following flags (with their non-default values) can be passed to `cmake`:
|
||||||
| `-DWARNING_AS_ERROR=0` | Don't treat compilation warnings as errors in debug mode |
|
| `-DWARNING_AS_ERROR=0` | Don't treat compilation warnings as errors in debug mode |
|
||||||
| `-DUPDATE_TRANSLATIONS=1` | Configure `make` to update the translation .ts files for new strings in the source code<br> **Note:** `make clean` will remove the .ts files |
|
| `-DUPDATE_TRANSLATIONS=1` | Configure `make` to update the translation .ts files for new strings in the source code<br> **Note:** `make clean` will remove the .ts files |
|
||||||
| `-DTEST=1` | Enable regression tests<br> **Note:** `make test` to run tests, *googletest* will be downloaded if not available |
|
| `-DTEST=1` | Enable regression tests<br> **Note:** `make test` to run tests, *googletest* will be downloaded if not available |
|
||||||
| `-DFORCE_USE_QT5=1` | Skip looking for Qt6 before trying to find Qt5 |
|
|
||||||
|
|
||||||
|
|
||||||
# Run
|
# Run
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
# Find a compatible Qt version
|
# Find a compatible Qt version
|
||||||
# Inputs: WITH_SERVER, WITH_CLIENT, WITH_ORACLE, FORCE_USE_QT5
|
# Inputs: WITH_SERVER, WITH_CLIENT, WITH_ORACLE
|
||||||
# Optional Input: QT6_DIR -- Hint as to where Qt6 lives on the system
|
# Optional Input: QT6_DIR -- Hint as to where Qt6 lives on the system
|
||||||
# Optional Input: QT5_DIR -- Hint as to where Qt5 lives on the system
|
# Output: COCKATRICE_QT_VERSION_NAME -- Example values: Qt6
|
||||||
# Output: COCKATRICE_QT_VERSION_NAME -- Example values: Qt5, Qt6
|
|
||||||
# Output: SERVATRICE_QT_MODULES
|
# Output: SERVATRICE_QT_MODULES
|
||||||
# Output: COCKATRICE_QT_MODULES
|
# Output: COCKATRICE_QT_MODULES
|
||||||
# Output: ORACLE_QT_MODULES
|
# Output: ORACLE_QT_MODULES
|
||||||
|
|
@ -39,69 +38,37 @@ set(REQUIRED_QT_COMPONENTS ${REQUIRED_QT_COMPONENTS} ${_SERVATRICE_NEEDED} ${_CO
|
||||||
)
|
)
|
||||||
list(REMOVE_DUPLICATES REQUIRED_QT_COMPONENTS)
|
list(REMOVE_DUPLICATES REQUIRED_QT_COMPONENTS)
|
||||||
|
|
||||||
if(NOT FORCE_USE_QT5)
|
# Linguist is now a component in Qt6 instead of an external package
|
||||||
# Linguist is now a component in Qt6 instead of an external package
|
find_package(
|
||||||
find_package(
|
Qt6 6.4.2
|
||||||
Qt6 6.4.2
|
COMPONENTS ${REQUIRED_QT_COMPONENTS} Linguist
|
||||||
COMPONENTS ${REQUIRED_QT_COMPONENTS} Linguist
|
QUIET HINTS ${Qt6_DIR}
|
||||||
QUIET HINTS ${Qt6_DIR}
|
)
|
||||||
)
|
if(NOT Qt6_FOUND)
|
||||||
|
message(FATAL_ERROR "No suitable version of Qt was found")
|
||||||
endif()
|
endif()
|
||||||
if(Qt6_FOUND)
|
set(COCKATRICE_QT_VERSION_NAME Qt6)
|
||||||
set(COCKATRICE_QT_VERSION_NAME Qt6)
|
|
||||||
|
|
||||||
list(FIND Qt6LinguistTools_TARGETS Qt6::lrelease QT6_LRELEASE_INDEX)
|
list(FIND Qt6LinguistTools_TARGETS Qt6::lrelease QT6_LRELEASE_INDEX)
|
||||||
if(QT6_LRELEASE_INDEX EQUAL -1)
|
if(QT6_LRELEASE_INDEX EQUAL -1)
|
||||||
message(WARNING "Qt6 lrelease not found.")
|
message(WARNING "Qt6 lrelease not found.")
|
||||||
endif()
|
|
||||||
|
|
||||||
list(FIND Qt6LinguistTools_TARGETS Qt6::lupdate QT6_LUPDATE_INDEX)
|
|
||||||
if(QT6_LUPDATE_INDEX EQUAL -1)
|
|
||||||
message(WARNING "Qt6 lupdate not found.")
|
|
||||||
endif()
|
|
||||||
else()
|
|
||||||
find_package(
|
|
||||||
Qt5 5.15.2
|
|
||||||
COMPONENTS ${REQUIRED_QT_COMPONENTS}
|
|
||||||
QUIET HINTS ${Qt5_DIR}
|
|
||||||
)
|
|
||||||
if(Qt5_FOUND)
|
|
||||||
set(COCKATRICE_QT_VERSION_NAME Qt5)
|
|
||||||
else()
|
|
||||||
message(FATAL_ERROR "No suitable version of Qt was found")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# Qt5 Linguist is in a separate package
|
|
||||||
find_package(Qt5LinguistTools QUIET)
|
|
||||||
if(Qt5LinguistTools_FOUND)
|
|
||||||
if(NOT Qt5_LRELEASE_EXECUTABLE)
|
|
||||||
message(WARNING "Qt5 lrelease not found.")
|
|
||||||
endif()
|
|
||||||
if(NOT Qt5_LUPDATE_EXECUTABLE)
|
|
||||||
message(WARNING "Qt5 lupdate not found.")
|
|
||||||
endif()
|
|
||||||
else()
|
|
||||||
message(WARNING "Linguist Tools not found, cannot handle translations")
|
|
||||||
endif()
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(Qt5_POSITION_INDEPENDENT_CODE OR Qt6_FOUND)
|
list(FIND Qt6LinguistTools_TARGETS Qt6::lupdate QT6_LUPDATE_INDEX)
|
||||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
if(QT6_LUPDATE_INDEX EQUAL -1)
|
||||||
|
message(WARNING "Qt6 lupdate not found.")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||||
|
|
||||||
# Establish Qt Plugins directory & Library directories
|
# Establish Qt Plugins directory & Library directories
|
||||||
get_target_property(QT_LIBRARY_DIR ${COCKATRICE_QT_VERSION_NAME}::Core LOCATION)
|
get_target_property(QT_LIBRARY_DIR ${COCKATRICE_QT_VERSION_NAME}::Core LOCATION)
|
||||||
get_filename_component(QT_LIBRARY_DIR ${QT_LIBRARY_DIR} DIRECTORY)
|
get_filename_component(QT_LIBRARY_DIR ${QT_LIBRARY_DIR} DIRECTORY)
|
||||||
if(Qt6_FOUND)
|
get_filename_component(QT_PLUGINS_DIR "${Qt6Core_DIR}/../../../${QT6_INSTALL_PLUGINS}" ABSOLUTE)
|
||||||
get_filename_component(QT_PLUGINS_DIR "${Qt6Core_DIR}/../../../${QT6_INSTALL_PLUGINS}" ABSOLUTE)
|
get_filename_component(QT_LIBRARY_DIR "${QT_LIBRARY_DIR}/../../.." ABSOLUTE)
|
||||||
get_filename_component(QT_LIBRARY_DIR "${QT_LIBRARY_DIR}/../../.." ABSOLUTE)
|
if(UNIX AND APPLE)
|
||||||
if(UNIX AND APPLE)
|
# Mac needs a bit more help finding all necessary components
|
||||||
# Mac needs a bit more help finding all necessary components
|
list(APPEND QT_LIBRARY_DIR "/usr/local/lib")
|
||||||
list(APPEND QT_LIBRARY_DIR "/usr/local/lib")
|
|
||||||
endif()
|
|
||||||
elseif(Qt5_FOUND)
|
|
||||||
get_filename_component(QT_PLUGINS_DIR "${Qt5Core_DIR}/../../../plugins" ABSOLUTE)
|
|
||||||
get_filename_component(QT_LIBRARY_DIR "${QT_LIBRARY_DIR}/.." ABSOLUTE)
|
|
||||||
endif()
|
endif()
|
||||||
message(DEBUG "QT_PLUGINS_DIR = ${QT_PLUGINS_DIR}")
|
message(DEBUG "QT_PLUGINS_DIR = ${QT_PLUGINS_DIR}")
|
||||||
message(DEBUG "QT_LIBRARY_DIR = ${QT_LIBRARY_DIR}")
|
message(DEBUG "QT_LIBRARY_DIR = ${QT_LIBRARY_DIR}")
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,118 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||||
|
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
|
||||||
|
<!-- ========================= -->
|
||||||
|
<!-- CORE BUNDLE METADATA -->
|
||||||
|
<!-- ========================= -->
|
||||||
|
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>English</string>
|
<string>English</string>
|
||||||
|
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
|
<string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
|
||||||
|
|
||||||
<key>CFBundleGetInfoString</key>
|
<key>CFBundleGetInfoString</key>
|
||||||
<string>${MACOSX_BUNDLE_INFO_STRING}</string>
|
<string>${MACOSX_BUNDLE_INFO_STRING}</string>
|
||||||
|
|
||||||
<key>CFBundleIconFile</key>
|
<key>CFBundleIconFile</key>
|
||||||
<string>${MACOSX_BUNDLE_ICON_FILE}</string>
|
<string>${MACOSX_BUNDLE_ICON_FILE}</string>
|
||||||
|
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
|
<string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
|
||||||
|
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
|
|
||||||
<key>CFBundleLongVersionString</key>
|
<key>CFBundleLongVersionString</key>
|
||||||
<string>${MACOSX_BUNDLE_LONG_VERSION_STRING}</string>
|
<string>${MACOSX_BUNDLE_LONG_VERSION_STRING}</string>
|
||||||
|
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
|
<string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
|
||||||
|
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
|
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
|
<string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
|
||||||
|
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
|
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
|
<string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
|
||||||
<key>CSResourcesFileMapped</key>
|
|
||||||
<true/>
|
|
||||||
<key>LSRequiresCarbon</key>
|
|
||||||
<true/>
|
|
||||||
<key>NSHumanReadableCopyright</key>
|
<key>NSHumanReadableCopyright</key>
|
||||||
<string>${MACOSX_BUNDLE_COPYRIGHT}</string>
|
<string>${MACOSX_BUNDLE_COPYRIGHT}</string>
|
||||||
|
|
||||||
<key>NSHighResolutionCapable</key>
|
<key>NSHighResolutionCapable</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
|
||||||
|
<!-- ========================= -->
|
||||||
|
<!-- FILE TYPE (.cod) SUPPORT -->
|
||||||
|
<!-- ========================= -->
|
||||||
|
|
||||||
|
<key>UTExportedTypeDeclarations</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>UTTypeIdentifier</key>
|
||||||
|
<string>org.cockatrice.deck</string>
|
||||||
|
|
||||||
|
<key>UTTypeDescription</key>
|
||||||
|
<string>Cockatrice Deck</string>
|
||||||
|
|
||||||
|
<key>UTTypeConformsTo</key>
|
||||||
|
<array>
|
||||||
|
<string>public.data</string>
|
||||||
|
</array>
|
||||||
|
|
||||||
|
<key>UTTypeTagSpecification</key>
|
||||||
|
<dict>
|
||||||
|
<key>public.filename-extension</key>
|
||||||
|
<array>
|
||||||
|
<string>cod</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
|
||||||
|
<key>CFBundleDocumentTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>Cockatrice Deck</string>
|
||||||
|
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Default</string>
|
||||||
|
|
||||||
|
<key>LSItemContentTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>org.cockatrice.deck</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
|
||||||
|
<!-- ========================= -->
|
||||||
|
<!-- URL SCHEME (cockatrice://) -->
|
||||||
|
<!-- ========================= -->
|
||||||
|
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>Cockatrice URL Scheme</string>
|
||||||
|
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>cockatrice</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
||||||
|
|
@ -36,13 +36,16 @@ set(cockatrice_SOURCES
|
||||||
src/interface/widgets/dialogs/dlg_forgot_password_challenge.cpp
|
src/interface/widgets/dialogs/dlg_forgot_password_challenge.cpp
|
||||||
src/interface/widgets/dialogs/dlg_forgot_password_request.cpp
|
src/interface/widgets/dialogs/dlg_forgot_password_request.cpp
|
||||||
src/interface/widgets/dialogs/dlg_forgot_password_reset.cpp
|
src/interface/widgets/dialogs/dlg_forgot_password_reset.cpp
|
||||||
|
src/interface/widgets/dialogs/dlg_invite_to_game.cpp
|
||||||
src/interface/widgets/dialogs/dlg_load_deck.cpp
|
src/interface/widgets/dialogs/dlg_load_deck.cpp
|
||||||
src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp
|
src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp
|
||||||
src/interface/widgets/dialogs/dlg_load_deck_from_website.cpp
|
src/interface/widgets/dialogs/dlg_load_deck_from_website.cpp
|
||||||
src/interface/widgets/dialogs/dlg_load_remote_deck.cpp
|
src/interface/widgets/dialogs/dlg_load_remote_deck.cpp
|
||||||
src/interface/widgets/dialogs/dlg_local_game_options.cpp
|
src/interface/widgets/dialogs/dlg_local_game_options.cpp
|
||||||
src/interface/widgets/dialogs/dlg_manage_sets.cpp
|
src/interface/widgets/dialogs/dlg_manage_sets.cpp
|
||||||
|
src/interface/widgets/dialogs/dlg_my_reports.cpp
|
||||||
src/interface/widgets/dialogs/dlg_register.cpp
|
src/interface/widgets/dialogs/dlg_register.cpp
|
||||||
|
src/interface/widgets/dialogs/dlg_report_user.cpp
|
||||||
src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp
|
src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp
|
||||||
src/interface/widgets/dialogs/dlg_settings.cpp
|
src/interface/widgets/dialogs/dlg_settings.cpp
|
||||||
src/interface/widgets/dialogs/dlg_startup_card_check.cpp
|
src/interface/widgets/dialogs/dlg_startup_card_check.cpp
|
||||||
|
|
@ -83,7 +86,6 @@ set(cockatrice_SOURCES
|
||||||
src/game/game_state.cpp
|
src/game/game_state.cpp
|
||||||
src/game_graphics/game_view.cpp
|
src/game_graphics/game_view.cpp
|
||||||
src/game_graphics/hand_counter.cpp
|
src/game_graphics/hand_counter.cpp
|
||||||
src/game/selection_subtype_tally.cpp
|
|
||||||
src/game_graphics/log/message_log_widget.cpp
|
src/game_graphics/log/message_log_widget.cpp
|
||||||
src/game/phase.cpp
|
src/game/phase.cpp
|
||||||
src/game_graphics/phases_toolbar.cpp
|
src/game_graphics/phases_toolbar.cpp
|
||||||
|
|
@ -98,7 +100,11 @@ set(cockatrice_SOURCES
|
||||||
src/game_graphics/player/menu/rfg_menu.cpp
|
src/game_graphics/player/menu/rfg_menu.cpp
|
||||||
src/game_graphics/player/menu/say_menu.cpp
|
src/game_graphics/player/menu/say_menu.cpp
|
||||||
src/game_graphics/player/menu/sideboard_menu.cpp
|
src/game_graphics/player/menu/sideboard_menu.cpp
|
||||||
|
src/game_graphics/player/menu/tally_menu.cpp
|
||||||
src/game_graphics/player/menu/utility_menu.cpp
|
src/game_graphics/player/menu/utility_menu.cpp
|
||||||
|
src/game_graphics/tally/stats_tally.cpp
|
||||||
|
src/game_graphics/tally/subtype_tally.cpp
|
||||||
|
src/game_graphics/tally/tally.cpp
|
||||||
src/game/player/player_actions.cpp
|
src/game/player/player_actions.cpp
|
||||||
src/game_graphics/player/player_area.cpp
|
src/game_graphics/player/player_area.cpp
|
||||||
src/game_graphics/player/player_dialogs.cpp
|
src/game_graphics/player/player_dialogs.cpp
|
||||||
|
|
@ -132,9 +138,19 @@ set(cockatrice_SOURCES
|
||||||
src/interface/card_picture_loader/card_picture_loader_worker.cpp
|
src/interface/card_picture_loader/card_picture_loader_worker.cpp
|
||||||
src/interface/card_picture_loader/card_picture_loader_worker_work.cpp
|
src/interface/card_picture_loader/card_picture_loader_worker_work.cpp
|
||||||
src/interface/card_picture_loader/card_picture_to_load.cpp
|
src/interface/card_picture_loader/card_picture_to_load.cpp
|
||||||
|
src/interface/intents/intent.cpp
|
||||||
|
src/interface/intents/intent.h
|
||||||
|
src/interface/intents/intent_open_local_deck.cpp
|
||||||
|
src/interface/intents/intent_open_local_deck.h
|
||||||
|
src/interface/intents/intent_wait_for_database_load.cpp
|
||||||
|
src/interface/intents/intent_wait_for_database_load.h
|
||||||
src/interface/layouts/flow_layout.cpp
|
src/interface/layouts/flow_layout.cpp
|
||||||
src/interface/layouts/overlap_layout.cpp
|
src/interface/layouts/overlap_layout.cpp
|
||||||
|
src/interface/widgets/utility/card_completer_delegate.cpp
|
||||||
|
src/interface/widgets/utility/card_completer_styler.cpp
|
||||||
|
src/interface/widgets/utility/completer_utils.cpp
|
||||||
src/interface/widgets/utility/line_edit_completer.cpp
|
src/interface/widgets/utility/line_edit_completer.cpp
|
||||||
|
src/interface/widgets/utility/reversed_completer_model.cpp
|
||||||
src/interface/pixel_map_generator.cpp
|
src/interface/pixel_map_generator.cpp
|
||||||
src/interface/theme_config.cpp
|
src/interface/theme_config.cpp
|
||||||
src/interface/theme_manager.cpp
|
src/interface/theme_manager.cpp
|
||||||
|
|
@ -146,6 +162,7 @@ set(cockatrice_SOURCES
|
||||||
src/interface/widgets/cards/additional_info/color_identity_widget.cpp
|
src/interface/widgets/cards/additional_info/color_identity_widget.cpp
|
||||||
src/interface/widgets/cards/additional_info/mana_cost_widget.cpp
|
src/interface/widgets/cards/additional_info/mana_cost_widget.cpp
|
||||||
src/interface/widgets/cards/additional_info/mana_symbol_widget.cpp
|
src/interface/widgets/cards/additional_info/mana_symbol_widget.cpp
|
||||||
|
src/interface/widgets/cards/art_crop_attribution.cpp
|
||||||
src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp
|
src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp
|
||||||
src/interface/widgets/cards/card_group_display_widgets/flat_card_group_display_widget.cpp
|
src/interface/widgets/cards/card_group_display_widgets/flat_card_group_display_widget.cpp
|
||||||
src/interface/widgets/cards/card_group_display_widgets/overlapped_card_group_display_widget.cpp
|
src/interface/widgets/cards/card_group_display_widgets/overlapped_card_group_display_widget.cpp
|
||||||
|
|
@ -225,12 +242,19 @@ set(cockatrice_SOURCES
|
||||||
src/interface/widgets/printing_selector/printing_selector_card_selection_widget.cpp
|
src/interface/widgets/printing_selector/printing_selector_card_selection_widget.cpp
|
||||||
src/interface/widgets/printing_selector/printing_selector_card_sorting_widget.cpp
|
src/interface/widgets/printing_selector/printing_selector_card_sorting_widget.cpp
|
||||||
src/interface/widgets/printing_selector/set_name_and_collectors_number_display_widget.cpp
|
src/interface/widgets/printing_selector/set_name_and_collectors_number_display_widget.cpp
|
||||||
|
src/interface/widgets/playmat/playmat_collection_dialog.cpp
|
||||||
|
src/interface/widgets/playmat/playmat_collection_dialog.h
|
||||||
|
src/interface/widgets/playmat/playmat_preview_widget.cpp
|
||||||
|
src/interface/widgets/playmat/playmat_settings_dialog.cpp
|
||||||
src/interface/widgets/quick_settings/settings_button_widget.cpp
|
src/interface/widgets/quick_settings/settings_button_widget.cpp
|
||||||
src/interface/widgets/quick_settings/settings_popup_widget.cpp
|
src/interface/widgets/quick_settings/settings_popup_widget.cpp
|
||||||
src/interface/widgets/replay/replay_manager.cpp
|
src/interface/widgets/replay/replay_manager.cpp
|
||||||
|
src/interface/widgets/replay/replay_quick_settings_widget.cpp
|
||||||
src/interface/widgets/replay/replay_timeline_widget.cpp
|
src/interface/widgets/replay/replay_timeline_widget.cpp
|
||||||
|
src/interface/widgets/replay/replay_widget.cpp
|
||||||
src/interface/widgets/server/chat_view/chat_view.cpp
|
src/interface/widgets/server/chat_view/chat_view.cpp
|
||||||
src/interface/widgets/server/game_filter_configs.cpp
|
src/interface/widgets/server/game_filter_configs.cpp
|
||||||
|
src/interface/widgets/server/game_link.cpp
|
||||||
src/interface/widgets/server/game_selector.cpp
|
src/interface/widgets/server/game_selector.cpp
|
||||||
src/interface/widgets/server/game_selector_quick_filter_toolbar.cpp
|
src/interface/widgets/server/game_selector_quick_filter_toolbar.cpp
|
||||||
src/interface/widgets/server/games_model.cpp
|
src/interface/widgets/server/games_model.cpp
|
||||||
|
|
@ -245,17 +269,23 @@ set(cockatrice_SOURCES
|
||||||
src/interface/widgets/server/user/user_info_connection.cpp
|
src/interface/widgets/server/user/user_info_connection.cpp
|
||||||
src/interface/widgets/server/user/user_list_manager.cpp
|
src/interface/widgets/server/user/user_list_manager.cpp
|
||||||
src/interface/widgets/server/user/user_list_painter.cpp
|
src/interface/widgets/server/user/user_list_painter.cpp
|
||||||
|
src/interface/widgets/server/user/user_list_panel_widget.cpp
|
||||||
src/interface/widgets/server/user/user_list_widget.cpp
|
src/interface/widgets/server/user/user_list_widget.cpp
|
||||||
|
src/interface/widgets/settings_page/abstract_settings_page.cpp
|
||||||
src/interface/widgets/settings_page/appearance_settings_page.cpp
|
src/interface/widgets/settings_page/appearance_settings_page.cpp
|
||||||
src/interface/widgets/settings_page/deck_editor_settings_page.cpp
|
src/interface/widgets/settings_page/deck_editor_settings_page.cpp
|
||||||
src/interface/widgets/settings_page/general_settings_page.cpp
|
src/interface/widgets/settings_page/general_settings_page.cpp
|
||||||
src/interface/widgets/settings_page/messages_settings_page.cpp
|
src/interface/widgets/settings_page/messages_settings_page.cpp
|
||||||
|
src/interface/widgets/settings_page/settings_search_delegate.cpp
|
||||||
|
src/interface/widgets/settings_page/settings_search_model.cpp
|
||||||
src/interface/widgets/settings_page/shortcut_settings_page.cpp
|
src/interface/widgets/settings_page/shortcut_settings_page.cpp
|
||||||
src/interface/widgets/settings_page/sound_settings_page.cpp
|
src/interface/widgets/settings_page/sound_settings_page.cpp
|
||||||
src/interface/widgets/settings_page/storage_settings_page.cpp
|
src/interface/widgets/settings_page/storage_settings_page.cpp
|
||||||
src/interface/widgets/settings_page/user_interface_settings_page.cpp
|
src/interface/widgets/settings_page/user_interface_settings_page.cpp
|
||||||
src/interface/widgets/utility/custom_line_edit.cpp
|
src/interface/widgets/utility/custom_line_edit.cpp
|
||||||
src/interface/widgets/utility/get_text_with_max.cpp
|
src/interface/widgets/utility/get_text_with_max.cpp
|
||||||
|
src/interface/widgets/utility/report_utils.cpp
|
||||||
|
src/interface/widgets/utility/report_utils.h
|
||||||
src/interface/widgets/utility/sequence_edit.cpp
|
src/interface/widgets/utility/sequence_edit.cpp
|
||||||
src/interface/widgets/utility/visibility_change_listener.cpp
|
src/interface/widgets/utility/visibility_change_listener.cpp
|
||||||
src/interface/widgets/utility/visibility_change_listener.h
|
src/interface/widgets/utility/visibility_change_listener.h
|
||||||
|
|
@ -281,13 +311,16 @@ set(cockatrice_SOURCES
|
||||||
src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_item_widget.cpp
|
src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_item_widget.cpp
|
||||||
src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp
|
src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp
|
||||||
src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.cpp
|
src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.cpp
|
||||||
|
src/interface/widgets/visual_deck_storage/visual_deck_storage_model.cpp
|
||||||
src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.cpp
|
src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.cpp
|
||||||
src/interface/widgets/visual_deck_storage/visual_deck_storage_search_widget.cpp
|
src/interface/widgets/visual_deck_storage/visual_deck_storage_search_widget.cpp
|
||||||
|
src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.cpp
|
||||||
src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_widget.cpp
|
src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_widget.cpp
|
||||||
src/interface/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.cpp
|
src/interface/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.cpp
|
||||||
src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp
|
src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp
|
||||||
src/interface/window_main.cpp
|
src/interface/window_main.cpp
|
||||||
src/main.cpp
|
src/main.cpp
|
||||||
|
src/single_instance_manager.cpp
|
||||||
src/interface/widgets/tabs/abstract_tab_deck_editor.cpp
|
src/interface/widgets/tabs/abstract_tab_deck_editor.cpp
|
||||||
src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp
|
src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp
|
||||||
src/interface/widgets/tabs/api/archidekt/api_response/archidekt_deck_listing_api_response.cpp
|
src/interface/widgets/tabs/api/archidekt/api_response/archidekt_deck_listing_api_response.cpp
|
||||||
|
|
@ -303,6 +336,13 @@ set(cockatrice_SOURCES
|
||||||
src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.cpp
|
src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.cpp
|
||||||
src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_listings_display_widget.cpp
|
src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_listings_display_widget.cpp
|
||||||
src/interface/widgets/tabs/api/archidekt/display/archidekt_deck_preview_image_display_widget.cpp
|
src/interface/widgets/tabs/api/archidekt/display/archidekt_deck_preview_image_display_widget.cpp
|
||||||
|
src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.cpp
|
||||||
|
src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.cpp
|
||||||
|
src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.cpp
|
||||||
|
src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.cpp
|
||||||
|
src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.cpp
|
||||||
|
src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.cpp
|
||||||
|
src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.cpp
|
||||||
src/interface/widgets/tabs/api/edhrec/api_response/archidekt_links/edhrec_api_response_archidekt_links.cpp
|
src/interface/widgets/tabs/api/edhrec/api_response/archidekt_links/edhrec_api_response_archidekt_links.cpp
|
||||||
src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_average_deck_api_response.cpp
|
src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_average_deck_api_response.cpp
|
||||||
src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_deck_api_response.cpp
|
src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_deck_api_response.cpp
|
||||||
|
|
@ -339,6 +379,8 @@ set(cockatrice_SOURCES
|
||||||
src/interface/widgets/tabs/tab_home.cpp
|
src/interface/widgets/tabs/tab_home.cpp
|
||||||
src/interface/widgets/tabs/tab_logs.cpp
|
src/interface/widgets/tabs/tab_logs.cpp
|
||||||
src/interface/widgets/tabs/tab_message.cpp
|
src/interface/widgets/tabs/tab_message.cpp
|
||||||
|
src/interface/widgets/tabs/tab_moderation.cpp
|
||||||
|
src/interface/widgets/tabs/tab_report.cpp
|
||||||
src/interface/widgets/tabs/tab_replays.cpp
|
src/interface/widgets/tabs/tab_replays.cpp
|
||||||
src/interface/widgets/tabs/tab_room.cpp
|
src/interface/widgets/tabs/tab_room.cpp
|
||||||
src/interface/widgets/tabs/tab_server.cpp
|
src/interface/widgets/tabs/tab_server.cpp
|
||||||
|
|
@ -349,12 +391,31 @@ set(cockatrice_SOURCES
|
||||||
src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.cpp
|
src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.cpp
|
||||||
src/interface/key_signals.cpp
|
src/interface/key_signals.cpp
|
||||||
src/interface/logger.cpp
|
src/interface/logger.cpp
|
||||||
|
src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.cpp
|
||||||
|
src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp
|
||||||
|
src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp
|
||||||
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.cpp
|
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.cpp
|
||||||
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.h
|
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.h
|
||||||
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.cpp
|
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.cpp
|
||||||
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.h
|
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.h
|
||||||
src/interface/widgets/utility/compact_push_button.cpp
|
src/interface/widgets/utility/compact_push_button.cpp
|
||||||
src/interface/widgets/utility/compact_push_button.h
|
src/interface/widgets/utility/compact_push_button.h
|
||||||
|
src/single_instance_manager.h
|
||||||
|
src/client/url_scheme_event_filter.h
|
||||||
|
src/interface/intents/intent_connect_to_server.cpp
|
||||||
|
src/interface/intents/intent_connect_to_server.h
|
||||||
|
src/interface/intents/intent_disconnect_from_server.cpp
|
||||||
|
src/interface/intents/intent_disconnect_from_server.h
|
||||||
|
src/interface/intents/intent_join_server_game.cpp
|
||||||
|
src/interface/intents/intent_join_server_game.h
|
||||||
|
src/interface/intents/intent_join_server_room.cpp
|
||||||
|
src/interface/intents/intent_join_server_room.h
|
||||||
|
src/interface/intents/intent_login.cpp
|
||||||
|
src/interface/intents/intent_login.h
|
||||||
|
src/interface/intents/intent_open_server_room_by_name.cpp
|
||||||
|
src/interface/intents/intent_open_server_room_by_name.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.cpp
|
||||||
src/interface/widgets/server/user/user_info_popup.h
|
src/interface/widgets/server/user/user_info_popup.h
|
||||||
)
|
)
|
||||||
|
|
@ -398,11 +459,7 @@ if(APPLE)
|
||||||
set(cockatrice_SOURCES ${cockatrice_SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/resources/appicon.icns)
|
set(cockatrice_SOURCES ${cockatrice_SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/resources/appicon.icns)
|
||||||
endif(APPLE)
|
endif(APPLE)
|
||||||
|
|
||||||
if(Qt6_FOUND)
|
qt6_add_resources(cockatrice_RESOURCES_RCC ${cockatrice_RESOURCES})
|
||||||
qt6_add_resources(cockatrice_RESOURCES_RCC ${cockatrice_RESOURCES})
|
|
||||||
elseif(Qt5_FOUND)
|
|
||||||
qt5_add_resources(cockatrice_RESOURCES_RCC ${cockatrice_RESOURCES})
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# Declare path variables
|
# Declare path variables
|
||||||
set(ICONDIR
|
set(ICONDIR
|
||||||
|
|
@ -414,71 +471,37 @@ set(DESKTOPDIR
|
||||||
CACHE STRING "desktop file destination"
|
CACHE STRING "desktop file destination"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
set(MIMEDIR
|
||||||
|
share/mime/packages
|
||||||
|
CACHE STRING "mime file destination"
|
||||||
|
)
|
||||||
|
|
||||||
set(COCKATRICE_MAC_QM_INSTALL_DIR "cockatrice.app/Contents/Resources/translations")
|
set(COCKATRICE_MAC_QM_INSTALL_DIR "cockatrice.app/Contents/Resources/translations")
|
||||||
set(COCKATRICE_UNIX_QM_INSTALL_DIR "share/cockatrice/translations")
|
set(COCKATRICE_UNIX_QM_INSTALL_DIR "share/cockatrice/translations")
|
||||||
set(COCKATRICE_WIN32_QM_INSTALL_DIR "translations")
|
set(COCKATRICE_WIN32_QM_INSTALL_DIR "translations")
|
||||||
|
|
||||||
if(Qt6_FOUND)
|
qt6_add_executable(
|
||||||
qt6_add_executable(
|
cockatrice
|
||||||
cockatrice
|
WIN32
|
||||||
WIN32
|
MACOSX_BUNDLE
|
||||||
MACOSX_BUNDLE
|
${cockatrice_SOURCES}
|
||||||
${cockatrice_SOURCES}
|
${cockatrice_RESOURCES_RCC}
|
||||||
${cockatrice_RESOURCES_RCC}
|
${cockatrice_MOC_SRCS}
|
||||||
${cockatrice_MOC_SRCS}
|
MANUAL_FINALIZATION
|
||||||
MANUAL_FINALIZATION
|
)
|
||||||
)
|
|
||||||
elseif(Qt5_FOUND)
|
|
||||||
# Qt5 Translations need to be linked at executable creation time
|
|
||||||
if(Qt5LinguistTools_FOUND)
|
|
||||||
if(UPDATE_TRANSLATIONS)
|
|
||||||
qt5_create_translation(cockatrice_QM ${translate_SRCS} ${cockatrice_TS})
|
|
||||||
else()
|
|
||||||
qt5_add_translation(cockatrice_QM ${cockatrice_TS})
|
|
||||||
endif()
|
|
||||||
endif()
|
|
||||||
add_executable(
|
|
||||||
cockatrice WIN32 MACOSX_BUNDLE ${cockatrice_MOC_SRCS} ${cockatrice_QM} ${cockatrice_RESOURCES_RCC}
|
|
||||||
${cockatrice_SOURCES}
|
|
||||||
)
|
|
||||||
if(UNIX)
|
|
||||||
if(APPLE)
|
|
||||||
install(FILES ${cockatrice_QM} DESTINATION ${COCKATRICE_MAC_QM_INSTALL_DIR})
|
|
||||||
else()
|
|
||||||
install(FILES ${cockatrice_QM} DESTINATION ${COCKATRICE_UNIX_QM_INSTALL_DIR})
|
|
||||||
endif()
|
|
||||||
elseif(WIN32)
|
|
||||||
install(FILES ${cockatrice_QM} DESTINATION ${COCKATRICE_WIN32_QM_INSTALL_DIR})
|
|
||||||
endif()
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(Qt5_FOUND)
|
target_link_libraries(
|
||||||
target_link_libraries(
|
cockatrice
|
||||||
cockatrice
|
PUBLIC libcockatrice_card
|
||||||
libcockatrice_card
|
libcockatrice_deck_list
|
||||||
libcockatrice_deck_list
|
libcockatrice_filters
|
||||||
libcockatrice_filters
|
libcockatrice_utility
|
||||||
libcockatrice_utility
|
libcockatrice_network
|
||||||
libcockatrice_network
|
libcockatrice_models
|
||||||
libcockatrice_models
|
libcockatrice_rng
|
||||||
libcockatrice_rng
|
libcockatrice_settings
|
||||||
libcockatrice_settings
|
${COCKATRICE_QT_MODULES}
|
||||||
${COCKATRICE_QT_MODULES}
|
)
|
||||||
)
|
|
||||||
else()
|
|
||||||
target_link_libraries(
|
|
||||||
cockatrice
|
|
||||||
PUBLIC libcockatrice_card
|
|
||||||
libcockatrice_deck_list
|
|
||||||
libcockatrice_filters
|
|
||||||
libcockatrice_utility
|
|
||||||
libcockatrice_network
|
|
||||||
libcockatrice_models
|
|
||||||
libcockatrice_rng
|
|
||||||
libcockatrice_settings
|
|
||||||
${COCKATRICE_QT_MODULES}
|
|
||||||
)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(UNIX)
|
if(UNIX)
|
||||||
if(APPLE)
|
if(APPLE)
|
||||||
|
|
@ -498,6 +521,23 @@ if(UNIX)
|
||||||
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/resources/cockatrice.png DESTINATION ${ICONDIR}/hicolor/48x48/apps)
|
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/resources/cockatrice.png DESTINATION ${ICONDIR}/hicolor/48x48/apps)
|
||||||
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/resources/cockatrice.svg DESTINATION ${ICONDIR}/hicolor/scalable/apps)
|
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/resources/cockatrice.svg DESTINATION ${ICONDIR}/hicolor/scalable/apps)
|
||||||
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/cockatrice.desktop DESTINATION ${DESKTOPDIR})
|
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/cockatrice.desktop DESTINATION ${DESKTOPDIR})
|
||||||
|
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/cockatrice-cod.xml DESTINATION ${MIMEDIR})
|
||||||
|
|
||||||
|
# Refresh the freedesktop databases so the file associations and scheme
|
||||||
|
# handler register without requiring the user to run them manually. The
|
||||||
|
# tools may be missing on minimal systems; that is fine, packaging systems
|
||||||
|
# usually refresh these databases through their own triggers.
|
||||||
|
find_program(UPDATE_MIME_DATABASE update-mime-database)
|
||||||
|
if(UPDATE_MIME_DATABASE)
|
||||||
|
install(CODE "execute_process(COMMAND \"${UPDATE_MIME_DATABASE}\" \"${CMAKE_INSTALL_PREFIX}/share/mime\")")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
find_program(UPDATE_DESKTOP_DATABASE update-desktop-database)
|
||||||
|
if(UPDATE_DESKTOP_DATABASE)
|
||||||
|
install(
|
||||||
|
CODE "execute_process(COMMAND \"${UPDATE_DESKTOP_DATABASE}\" \"${CMAKE_INSTALL_PREFIX}/share/applications\")"
|
||||||
|
)
|
||||||
|
endif()
|
||||||
endif()
|
endif()
|
||||||
elseif(WIN32)
|
elseif(WIN32)
|
||||||
install(TARGETS cockatrice RUNTIME DESTINATION ./)
|
install(TARGETS cockatrice RUNTIME DESTINATION ./)
|
||||||
|
|
@ -508,7 +548,7 @@ if(APPLE)
|
||||||
set(plugin_dest_dir cockatrice.app/Contents/Plugins)
|
set(plugin_dest_dir cockatrice.app/Contents/Plugins)
|
||||||
set(qtconf_dest_dir cockatrice.app/Contents/Resources)
|
set(qtconf_dest_dir cockatrice.app/Contents/Resources)
|
||||||
|
|
||||||
# Qt plugins: audio (Qt5), iconengines, imageformats, multimedia (Qt6), platforms, printsupport (Qt5), styles, tls (Qt6)
|
# Qt plugins: audio, iconengines, imageformats, multimedia, platforms, printsupport, styles, tls
|
||||||
install(
|
install(
|
||||||
DIRECTORY "${QT_PLUGINS_DIR}/"
|
DIRECTORY "${QT_PLUGINS_DIR}/"
|
||||||
DESTINATION ${plugin_dest_dir}
|
DESTINATION ${plugin_dest_dir}
|
||||||
|
|
@ -575,7 +615,7 @@ if(WIN32)
|
||||||
PATTERN "*.ini"
|
PATTERN "*.ini"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Qt plugins: audio (Qt5), iconengines, imageformats, multimedia (Qt6) platforms, printsupport (Qt5), styles, tls (Qt6)
|
# Qt plugins: audio, iconengines, imageformats, multimedia, platforms, printsupport, styles, tls
|
||||||
install(
|
install(
|
||||||
DIRECTORY "${QT_PLUGINS_DIR}/"
|
DIRECTORY "${QT_PLUGINS_DIR}/"
|
||||||
DESTINATION ${plugin_dest_dir}
|
DESTINATION ${plugin_dest_dir}
|
||||||
|
|
@ -631,7 +671,7 @@ Data = Resources\")
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(Qt6_FOUND AND Qt6LinguistTools_FOUND)
|
if(Qt6LinguistTools_FOUND)
|
||||||
#Qt6 Translations happen after the executable is built up
|
#Qt6 Translations happen after the executable is built up
|
||||||
if(UPDATE_TRANSLATIONS)
|
if(UPDATE_TRANSLATIONS)
|
||||||
qt6_add_translations(
|
qt6_add_translations(
|
||||||
|
|
@ -658,6 +698,4 @@ if(Qt6_FOUND AND Qt6LinguistTools_FOUND)
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(Qt6_FOUND)
|
qt6_finalize_target(cockatrice)
|
||||||
qt6_finalize_target(cockatrice)
|
|
||||||
endif()
|
|
||||||
|
|
|
||||||
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
|
Version=1.0
|
||||||
Type=Application
|
Type=Application
|
||||||
Name=Cockatrice
|
Name=Cockatrice
|
||||||
Exec=cockatrice
|
Exec=cockatrice %U
|
||||||
Icon=cockatrice
|
Icon=cockatrice
|
||||||
Categories=Game;CardGame;
|
Categories=Game;CardGame;
|
||||||
|
MimeType=application/x-cockatrice;
|
||||||
|
X-Scheme-Handler/cockatrice=true
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -14,6 +14,7 @@
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
#include <libcockatrice/network/client/remote/remote_client.h>
|
#include <libcockatrice/network/client/remote/remote_client.h>
|
||||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||||
|
#include <libcockatrice/settings/servers_settings.h>
|
||||||
|
|
||||||
ConnectionController::ConnectionController(QWidget *dialogParent, QObject *parent)
|
ConnectionController::ConnectionController(QWidget *dialogParent, QObject *parent)
|
||||||
: QObject(parent), dialogParent(dialogParent)
|
: QObject(parent), dialogParent(dialogParent)
|
||||||
|
|
@ -295,6 +296,15 @@ void ConnectionController::onLoginError(int r,
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case Response::RespPasswordChangeRequired: {
|
||||||
|
QMessageBox::information(
|
||||||
|
dialogParent, tr("Password Change Required"),
|
||||||
|
tr("An administrator has reset your password. Please contact your server administrator to obtain "
|
||||||
|
"your temporary password, then log in and change it via Account -> Change Password."));
|
||||||
|
remoteClient->disconnectFromServer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
case Response::RespServerFull: {
|
case Response::RespServerFull: {
|
||||||
QMessageBox::critical(dialogParent, tr("Server Full"),
|
QMessageBox::critical(dialogParent, tr("Server Full"),
|
||||||
tr("The server has reached its maximum user capacity, please check back later."));
|
tr("The server has reached its maximum user capacity, please check back later."));
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@
|
||||||
#include <QtConcurrent>
|
#include <QtConcurrent>
|
||||||
#include <libcockatrice/card/database/card_database.h>
|
#include <libcockatrice/card/database/card_database.h>
|
||||||
#include <libcockatrice/card/database/card_database_manager.h>
|
#include <libcockatrice/card/database/card_database_manager.h>
|
||||||
|
#include <libcockatrice/settings/download_settings.h>
|
||||||
|
#include <libcockatrice/settings/paths_settings.h>
|
||||||
#include <version_string.h>
|
#include <version_string.h>
|
||||||
|
|
||||||
#define SPOILERS_STATUS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/SpoilerSeasonEnabled"
|
#define SPOILERS_STATUS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/SpoilerSeasonEnabled"
|
||||||
|
|
@ -21,7 +23,7 @@
|
||||||
|
|
||||||
SpoilerBackgroundUpdater::SpoilerBackgroundUpdater(QObject *apParent) : QObject(apParent), cardUpdateProcess(nullptr)
|
SpoilerBackgroundUpdater::SpoilerBackgroundUpdater(QObject *apParent) : QObject(apParent), cardUpdateProcess(nullptr)
|
||||||
{
|
{
|
||||||
isSpoilerDownloadEnabled = SettingsCache::instance().getDownloadSpoilersStatus();
|
isSpoilerDownloadEnabled = SettingsCache::instance().downloads().getDownloadSpoilersStatus();
|
||||||
if (isSpoilerDownloadEnabled) {
|
if (isSpoilerDownloadEnabled) {
|
||||||
// Start the process of checking if we're in spoiler season
|
// Start the process of checking if we're in spoiler season
|
||||||
// File exists means we're in spoiler season
|
// File exists means we're in spoiler season
|
||||||
|
|
@ -75,7 +77,7 @@ void SpoilerBackgroundUpdater::actDownloadFinishedSpoilersFile()
|
||||||
|
|
||||||
bool SpoilerBackgroundUpdater::deleteSpoilerFile()
|
bool SpoilerBackgroundUpdater::deleteSpoilerFile()
|
||||||
{
|
{
|
||||||
QString fileName = SettingsCache::instance().getSpoilerCardDatabasePath();
|
QString fileName = SettingsCache::instance().paths().getSpoilerCardDatabasePath();
|
||||||
QFileInfo fi(fileName);
|
QFileInfo fi(fileName);
|
||||||
QDir fileDir(fi.path());
|
QDir fileDir(fi.path());
|
||||||
QFile file(fileName);
|
QFile file(fileName);
|
||||||
|
|
@ -126,7 +128,7 @@ void SpoilerBackgroundUpdater::actCheckIfSpoilerSeasonEnabled()
|
||||||
|
|
||||||
bool SpoilerBackgroundUpdater::saveDownloadedFile(QByteArray data)
|
bool SpoilerBackgroundUpdater::saveDownloadedFile(QByteArray data)
|
||||||
{
|
{
|
||||||
QString fileName = SettingsCache::instance().getSpoilerCardDatabasePath();
|
QString fileName = SettingsCache::instance().paths().getSpoilerCardDatabasePath();
|
||||||
QFileInfo fi(fileName);
|
QFileInfo fi(fileName);
|
||||||
QDir fileDir(fi.path());
|
QDir fileDir(fi.path());
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -5,7 +5,7 @@
|
||||||
#include <QtMath>
|
#include <QtMath>
|
||||||
|
|
||||||
CardCounterSettings::CardCounterSettings(const QString &settingsPath, QObject *parent)
|
CardCounterSettings::CardCounterSettings(const QString &settingsPath, QObject *parent)
|
||||||
: SettingsManager(settingsPath + "global.ini", "cards", "counters", parent)
|
: SettingsManager(settingsPath + "card_counters.ini", "cards", "counters", parent)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -786,6 +786,10 @@ private:
|
||||||
ShortcutGroup::Tabs)},
|
ShortcutGroup::Tabs)},
|
||||||
{"Tabs/aTabLogs",
|
{"Tabs/aTabLogs",
|
||||||
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Logs"), parseSequenceString(""), ShortcutGroup::Tabs)},
|
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Logs"), parseSequenceString(""), ShortcutGroup::Tabs)},
|
||||||
|
{"Tabs/aTabReport",
|
||||||
|
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Report Queue"), parseSequenceString(""), ShortcutGroup::Tabs)},
|
||||||
|
{"Tabs/aTabModeration",
|
||||||
|
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Moderation"), parseSequenceString(""), ShortcutGroup::Tabs)},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,11 @@
|
||||||
|
|
||||||
#include "settings/cache_settings.h"
|
#include "settings/cache_settings.h"
|
||||||
|
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QAudioOutput>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QMediaPlayer>
|
#include <QMediaPlayer>
|
||||||
|
#include <libcockatrice/settings/sound_settings.h>
|
||||||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
|
|
||||||
#include <QAudioOutput>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#define DEFAULT_THEME_NAME "Default"
|
#define DEFAULT_THEME_NAME "Default"
|
||||||
#define TEST_SOUND_FILENAME "player_join"
|
#define TEST_SOUND_FILENAME "player_join"
|
||||||
|
|
@ -15,8 +14,10 @@
|
||||||
SoundEngine::SoundEngine(QObject *parent) : QObject(parent), audioOutput(nullptr), player(nullptr)
|
SoundEngine::SoundEngine(QObject *parent) : QObject(parent), audioOutput(nullptr), player(nullptr)
|
||||||
{
|
{
|
||||||
ensureThemeDirectoryExists();
|
ensureThemeDirectoryExists();
|
||||||
connect(&SettingsCache::instance(), &SettingsCache::soundThemeChanged, this, &SoundEngine::themeChangedSlot);
|
connect(&SettingsCache::instance().sound(), &SoundSettings::soundThemeChanged, this,
|
||||||
connect(&SettingsCache::instance(), &SettingsCache::soundEnabledChanged, this, &SoundEngine::soundEnabledChanged);
|
&SoundEngine::themeChangedSlot);
|
||||||
|
connect(&SettingsCache::instance().sound(), &SoundSettings::soundEnabledChanged, this,
|
||||||
|
&SoundEngine::soundEnabledChanged);
|
||||||
|
|
||||||
soundEnabledChanged();
|
soundEnabledChanged();
|
||||||
themeChangedSlot();
|
themeChangedSlot();
|
||||||
|
|
@ -36,14 +37,12 @@ SoundEngine::~SoundEngine()
|
||||||
|
|
||||||
void SoundEngine::soundEnabledChanged()
|
void SoundEngine::soundEnabledChanged()
|
||||||
{
|
{
|
||||||
if (SettingsCache::instance().getSoundEnabled()) {
|
if (SettingsCache::instance().sound().getSoundEnabled()) {
|
||||||
qCInfo(SoundEngineLog) << "SoundEngine: enabling sound with" << audioData.size() << "sounds";
|
qCInfo(SoundEngineLog) << "SoundEngine: enabling sound with" << audioData.size() << "sounds";
|
||||||
if (!player) {
|
if (!player) {
|
||||||
player = new QMediaPlayer;
|
player = new QMediaPlayer;
|
||||||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
|
|
||||||
audioOutput = new QAudioOutput(player);
|
audioOutput = new QAudioOutput(player);
|
||||||
player->setAudioOutput(audioOutput);
|
player->setAudioOutput(audioOutput);
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
qCInfo(SoundEngineLog) << "SoundEngine: disabling sound";
|
qCInfo(SoundEngineLog) << "SoundEngine: disabling sound";
|
||||||
|
|
@ -70,14 +69,9 @@ void SoundEngine::playSound(const QString &fileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
player->stop();
|
player->stop();
|
||||||
int volumeSliderValue = SettingsCache::instance().getMasterVolume();
|
int volumeSliderValue = SettingsCache::instance().sound().getMasterVolume();
|
||||||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
|
|
||||||
player->audioOutput()->setVolume(qreal(volumeSliderValue) / 100);
|
player->audioOutput()->setVolume(qreal(volumeSliderValue) / 100);
|
||||||
player->setSource(QUrl::fromLocalFile(audioData[fileName]));
|
player->setSource(QUrl::fromLocalFile(audioData[fileName]));
|
||||||
#else
|
|
||||||
player->setVolume(volumeSliderValue);
|
|
||||||
player->setMedia(QUrl::fromLocalFile(audioData[fileName]));
|
|
||||||
#endif
|
|
||||||
player->play();
|
player->play();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -88,10 +82,10 @@ void SoundEngine::testSound()
|
||||||
|
|
||||||
void SoundEngine::ensureThemeDirectoryExists()
|
void SoundEngine::ensureThemeDirectoryExists()
|
||||||
{
|
{
|
||||||
if (SettingsCache::instance().getSoundThemeName().isEmpty() ||
|
if (SettingsCache::instance().sound().getSoundThemeName().isEmpty() ||
|
||||||
!getAvailableThemes().contains(SettingsCache::instance().getSoundThemeName())) {
|
!getAvailableThemes().contains(SettingsCache::instance().sound().getSoundThemeName())) {
|
||||||
qCInfo(SoundEngineLog) << "Sounds theme name not set, setting default value";
|
qCInfo(SoundEngineLog) << "Sounds theme name not set, setting default value";
|
||||||
SettingsCache::instance().setSoundThemeName(DEFAULT_THEME_NAME);
|
SettingsCache::instance().sound().setSoundThemeName(DEFAULT_THEME_NAME);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -132,7 +126,7 @@ QStringMap &SoundEngine::getAvailableThemes()
|
||||||
|
|
||||||
void SoundEngine::themeChangedSlot()
|
void SoundEngine::themeChangedSlot()
|
||||||
{
|
{
|
||||||
QString themeName = SettingsCache::instance().getSoundThemeName();
|
QString themeName = SettingsCache::instance().sound().getSoundThemeName();
|
||||||
qCInfo(SoundEngineLog) << "Sound theme changed:" << themeName;
|
qCInfo(SoundEngineLog) << "Sound theme changed:" << themeName;
|
||||||
|
|
||||||
QDir dir = getAvailableThemes().value(themeName);
|
QDir dir = getAvailableThemes().value(themeName);
|
||||||
|
|
|
||||||
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 "../../client/settings/cache_settings.h"
|
||||||
|
|
||||||
#include <libcockatrice/interfaces/interface_card_preference_provider.h>
|
#include <libcockatrice/interfaces/interface_card_preference_provider.h>
|
||||||
|
#include <libcockatrice/settings/card_override_settings.h>
|
||||||
|
#include <libcockatrice/settings/cards_display_settings.h>
|
||||||
|
|
||||||
class SettingsCardPreferenceProvider : public ICardPreferenceProvider
|
class SettingsCardPreferenceProvider : public ICardPreferenceProvider
|
||||||
{
|
{
|
||||||
|
|
@ -14,7 +16,7 @@ public:
|
||||||
|
|
||||||
[[nodiscard]] bool getIncludeRebalancedCards() const override
|
[[nodiscard]] bool getIncludeRebalancedCards() const override
|
||||||
{
|
{
|
||||||
return SettingsCache::instance().getIncludeRebalancedCards();
|
return SettingsCache::instance().cardsDisplay().getIncludeRebalancedCards();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,18 +52,14 @@ static void setupParserRules()
|
||||||
|
|
||||||
search["Start"] = passthru;
|
search["Start"] = passthru;
|
||||||
search["QueryPartList"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
search["QueryPartList"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) {
|
return [=](const DeckSearchData &data) {
|
||||||
auto matchesFilter = [&deck, &info](const std::any &query) {
|
auto matchesFilter = [&data](const std::any &query) { return std::any_cast<DeckFilter>(query)(data); };
|
||||||
return std::any_cast<DeckFilter>(query)(deck, info);
|
|
||||||
};
|
|
||||||
return std::all_of(sv.begin(), sv.end(), matchesFilter);
|
return std::all_of(sv.begin(), sv.end(), matchesFilter);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
search["ComplexQueryPart"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
search["ComplexQueryPart"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) {
|
return [=](const DeckSearchData &data) {
|
||||||
auto matchesFilter = [&deck, &info](const std::any &query) {
|
auto matchesFilter = [&data](const std::any &query) { return std::any_cast<DeckFilter>(query)(data); };
|
||||||
return std::any_cast<DeckFilter>(query)(deck, info);
|
|
||||||
};
|
|
||||||
return std::any_of(sv.begin(), sv.end(), matchesFilter);
|
return std::any_of(sv.begin(), sv.end(), matchesFilter);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -71,9 +67,7 @@ static void setupParserRules()
|
||||||
search["QueryPart"] = passthru;
|
search["QueryPart"] = passthru;
|
||||||
search["NotQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
search["NotQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||||
const auto dependent = std::any_cast<DeckFilter>(sv[0]);
|
const auto dependent = std::any_cast<DeckFilter>(sv[0]);
|
||||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) -> bool {
|
return [=](const DeckSearchData &data) -> bool { return !dependent(data); };
|
||||||
return !dependent(deck, info);
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
search["String"] = [](const peg::SemanticValues &sv) -> QString {
|
search["String"] = [](const peg::SemanticValues &sv) -> QString {
|
||||||
|
|
@ -125,9 +119,9 @@ static void setupParserRules()
|
||||||
auto cardFilter = FilterString(std::any_cast<QString>(sv[0]));
|
auto cardFilter = FilterString(std::any_cast<QString>(sv[0]));
|
||||||
auto numberMatcher = sv.size() > 1 ? std::any_cast<NumberMatcher>(sv[1]) : [](int count) { return count > 0; };
|
auto numberMatcher = sv.size() > 1 ? std::any_cast<NumberMatcher>(sv[1]) : [](int count) { return count > 0; };
|
||||||
|
|
||||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) -> bool {
|
return [=](const DeckSearchData &data) -> bool {
|
||||||
int count = 0;
|
int count = 0;
|
||||||
auto cardNodes = deck->deckLoader->getDeck().deckList.getCardNodes();
|
auto cardNodes = data.deck->deckList.getCardNodes();
|
||||||
for (auto node : cardNodes) {
|
for (auto node : cardNodes) {
|
||||||
auto cardInfoPtr = CardDatabaseManager::query()->getCardInfo(node->getName());
|
auto cardInfoPtr = CardDatabaseManager::query()->getCardInfo(node->getName());
|
||||||
if (!cardInfoPtr.isNull() && cardFilter.check(cardInfoPtr)) {
|
if (!cardInfoPtr.isNull() && cardFilter.check(cardInfoPtr)) {
|
||||||
|
|
@ -146,53 +140,49 @@ static void setupParserRules()
|
||||||
|
|
||||||
search["DeckNameQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
search["DeckNameQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||||
auto name = std::any_cast<QString>(sv[0]);
|
auto name = std::any_cast<QString>(sv[0]);
|
||||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) {
|
return [=](const DeckSearchData &data) {
|
||||||
return deck->deckLoader->getDeck().deckList.getName().contains(name, Qt::CaseInsensitive);
|
return data.deck->deckList.getName().contains(name, Qt::CaseInsensitive);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
search["FileNameQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
search["FileNameQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||||
auto name = std::any_cast<QString>(sv[0]);
|
auto name = std::any_cast<QString>(sv[0]);
|
||||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) {
|
return [=](const DeckSearchData &data) {
|
||||||
auto filename = QFileInfo(deck->filePath).fileName();
|
auto filename = QFileInfo(data.filePath).fileName();
|
||||||
return filename.contains(name, Qt::CaseInsensitive);
|
return filename.contains(name, Qt::CaseInsensitive);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
search["PathQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
search["PathQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||||
auto name = std::any_cast<QString>(sv[0]);
|
auto name = std::any_cast<QString>(sv[0]);
|
||||||
return [=](const DeckPreviewWidget *, const ExtraDeckSearchInfo &info) {
|
return [=](const DeckSearchData &data) { return data.relativeFilePath.contains(name, Qt::CaseInsensitive); };
|
||||||
return info.relativeFilePath.contains(name, Qt::CaseInsensitive);
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
search["FormatQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
search["FormatQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||||
auto format = std::any_cast<QString>(sv[0]);
|
auto format = std::any_cast<QString>(sv[0]);
|
||||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) {
|
return [=](const DeckSearchData &data) {
|
||||||
auto gameFormat = deck->deckLoader->getDeck().deckList.getGameFormat();
|
auto gameFormat = data.deck->deckList.getGameFormat();
|
||||||
return QString::compare(format, gameFormat, Qt::CaseInsensitive) == 0;
|
return QString::compare(format, gameFormat, Qt::CaseInsensitive) == 0;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
search["CommentQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
search["CommentQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||||
auto value = std::any_cast<QString>(sv[0]);
|
auto value = std::any_cast<QString>(sv[0]);
|
||||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) {
|
return [=](const DeckSearchData &data) {
|
||||||
auto comments = deck->deckLoader->getDeck().deckList.getComments();
|
auto comments = data.deck->deckList.getComments();
|
||||||
return comments.contains(value, Qt::CaseInsensitive);
|
return comments.contains(value, Qt::CaseInsensitive);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
search["GenericQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
search["GenericQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||||
auto name = std::any_cast<QString>(sv[0]);
|
auto name = std::any_cast<QString>(sv[0]);
|
||||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) {
|
return [=](const DeckSearchData &data) { return data.displayName.contains(name, Qt::CaseInsensitive); };
|
||||||
return deck->getDisplayName().contains(name, Qt::CaseInsensitive);
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
DeckFilterString::DeckFilterString()
|
DeckFilterString::DeckFilterString()
|
||||||
{
|
{
|
||||||
filter = [](const DeckPreviewWidget *, const ExtraDeckSearchInfo &) { return false; };
|
filter = [](const DeckSearchData &) { return false; };
|
||||||
_error = "Not initialized";
|
_error = "Not initialized";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -205,7 +195,7 @@ DeckFilterString::DeckFilterString(const QString &expr)
|
||||||
_error = QString();
|
_error = QString();
|
||||||
|
|
||||||
if (ba.isEmpty()) {
|
if (ba.isEmpty()) {
|
||||||
filter = [](const DeckPreviewWidget *, const ExtraDeckSearchInfo &) { return true; };
|
filter = [](const DeckSearchData &) { return true; };
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -215,6 +205,6 @@ DeckFilterString::DeckFilterString(const QString &expr)
|
||||||
|
|
||||||
if (!search.parse(ba.data(), filter)) {
|
if (!search.parse(ba.data(), filter)) {
|
||||||
qCInfo(DeckFilterStringLog).nospace() << "DeckFilterString error for " << expr << "; " << qPrintable(_error);
|
qCInfo(DeckFilterStringLog).nospace() << "DeckFilterString error for " << expr << "; " << qPrintable(_error);
|
||||||
filter = [](const DeckPreviewWidget *, const ExtraDeckSearchInfo &) { return false; };
|
filter = [](const DeckSearchData &) { return false; };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
#ifndef DECK_FILTER_STRING_H
|
#ifndef DECK_FILTER_STRING_H
|
||||||
#define DECK_FILTER_STRING_H
|
#define DECK_FILTER_STRING_H
|
||||||
|
|
||||||
#include "../interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h"
|
#include "../interface/deck_loader/loaded_deck.h"
|
||||||
|
|
||||||
#include <QLoggingCategory>
|
#include <QLoggingCategory>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
@ -16,26 +16,29 @@
|
||||||
inline Q_LOGGING_CATEGORY(DeckFilterStringLog, "deck_filter_string");
|
inline Q_LOGGING_CATEGORY(DeckFilterStringLog, "deck_filter_string");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extra info relevant to filtering that isn't present in the DeckPreviewWidget
|
* The data a deck search expression is evaluated against.
|
||||||
|
*
|
||||||
|
* This is a data view rather than a widget pointer, so the same filter
|
||||||
|
* expression can be evaluated against a model or a live widget.
|
||||||
*/
|
*/
|
||||||
struct ExtraDeckSearchInfo
|
struct DeckSearchData
|
||||||
{
|
{
|
||||||
/**
|
const LoadedDeck *deck = nullptr; ///< The loaded deck. Must not be null.
|
||||||
* The relative filepath starting from the deck folder
|
QString filePath; ///< Absolute path of the deck file.
|
||||||
*/
|
QString displayName; ///< Deck name, or the file name if the deck has no name.
|
||||||
QString relativeFilePath;
|
QString relativeFilePath; ///< File path relative to the deck folder.
|
||||||
};
|
};
|
||||||
|
|
||||||
typedef std::function<bool(const DeckPreviewWidget *, const ExtraDeckSearchInfo &)> DeckFilter;
|
typedef std::function<bool(const DeckSearchData &data)> DeckFilter;
|
||||||
|
|
||||||
class DeckFilterString
|
class DeckFilterString
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
DeckFilterString();
|
DeckFilterString();
|
||||||
explicit DeckFilterString(const QString &expr);
|
explicit DeckFilterString(const QString &expr);
|
||||||
bool check(const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) const
|
bool check(const DeckSearchData &data) const
|
||||||
{
|
{
|
||||||
return filter(deck, info);
|
return filter(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] bool valid() const
|
[[nodiscard]] bool valid() const
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ AbstractClient *AbstractGame::getClientForPlayer(int playerId) const
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AbstractGame::loadReplay(GameReplay *replay)
|
void AbstractGame::loadReplay(const GameReplay *replay)
|
||||||
{
|
{
|
||||||
gameMetaInfo->setFromProto(replay->game_info());
|
gameMetaInfo->setFromProto(replay->game_info());
|
||||||
gameMetaInfo->setSpectatorsOmniscient(true);
|
gameMetaInfo->setSpectatorsOmniscient(true);
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ public:
|
||||||
|
|
||||||
AbstractClient *getClientForPlayer(int playerId) const;
|
AbstractClient *getClientForPlayer(int playerId) const;
|
||||||
|
|
||||||
void loadReplay(GameReplay *replay);
|
void loadReplay(const GameReplay *replay);
|
||||||
|
|
||||||
CardItem *getCard(int playerId, const QString &zoneName, int cardId) const;
|
CardItem *getCard(int playerId, const QString &zoneName, int cardId) const;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -229,7 +229,11 @@ void GameEventHandler::handleArrowDeletion(int creatorId, int arrowId)
|
||||||
|
|
||||||
void GameEventHandler::handleArrowDeletionFinished(const Response &response, int creatorId, int arrowId)
|
void GameEventHandler::handleArrowDeletionFinished(const Response &response, int creatorId, int arrowId)
|
||||||
{
|
{
|
||||||
if (response.response_code() == Response::RespNameNotFound) {
|
// The server confirms the arrow no longer exists whether it deleted it itself
|
||||||
|
// (RespOk, followed by an Event_DeleteArrow broadcast) or never had it
|
||||||
|
// (RespNameNotFound). In both cases the local copy has to go. deleteArrow is
|
||||||
|
// a no-op if the arrow was already removed by the event broadcast.
|
||||||
|
if (response.response_code() == Response::RespOk || response.response_code() == Response::RespNameNotFound) {
|
||||||
emit arrowDeleted(creatorId, arrowId);
|
emit arrowDeleted(creatorId, arrowId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -281,12 +285,19 @@ void GameEventHandler::eventGameStateChanged(const Event_GameStateChanged &event
|
||||||
emit playerJoined(prop);
|
emit playerJoined(prop);
|
||||||
}
|
}
|
||||||
player->processPlayerInfo(playerInfo);
|
player->processPlayerInfo(playerInfo);
|
||||||
|
// Extract playmat from player properties for opponent display
|
||||||
|
if (prop.has_playmat_params()) {
|
||||||
|
player->setPlaymatFromProperties(prop);
|
||||||
|
}
|
||||||
if (player->getPlayerInfo()->getLocal()) {
|
if (player->getPlayerInfo()->getLocal()) {
|
||||||
emit localPlayerDeckSelected(player, playerId, playerInfo);
|
emit localPlayerDeckSelected(player, playerId, playerInfo);
|
||||||
} else {
|
} else {
|
||||||
if (!game->getGameMetaInfo()->proto().share_decklists_on_load()) {
|
if (!game->getGameMetaInfo()->proto().share_decklists_on_load()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (!playerInfo.has_deck_list()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
opponentDecksToDisplay.append(
|
opponentDecksToDisplay.append(
|
||||||
qMakePair(playerId, qMakePair(playerName, QString::fromStdString(playerInfo.deck_list()))));
|
qMakePair(playerId, qMakePair(playerName, QString::fromStdString(playerInfo.deck_list()))));
|
||||||
|
|
@ -344,6 +355,11 @@ void GameEventHandler::eventPlayerPropertiesChanged(const Event_PlayerProperties
|
||||||
const ServerInfo_PlayerProperties &prop = event.player_properties();
|
const ServerInfo_PlayerProperties &prop = event.player_properties();
|
||||||
emit playerPropertiesChanged(prop, eventPlayerId);
|
emit playerPropertiesChanged(prop, eventPlayerId);
|
||||||
|
|
||||||
|
// Update playmat from player properties
|
||||||
|
if (prop.has_playmat_params()) {
|
||||||
|
player->setPlaymatFromProperties(prop);
|
||||||
|
}
|
||||||
|
|
||||||
const auto contextType = static_cast<GameEventContext::ContextType>(getPbExtension(context));
|
const auto contextType = static_cast<GameEventContext::ContextType>(getPbExtension(context));
|
||||||
switch (contextType) {
|
switch (contextType) {
|
||||||
case GameEventContext::READY_START: {
|
case GameEventContext::READY_START: {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,16 @@
|
||||||
/**
|
/**
|
||||||
* @file game_event_handler.h
|
* @file game_event_handler.h
|
||||||
* @ingroup GameLogic
|
* @ingroup GameLogic
|
||||||
|
* @brief Game-level command sender and event dispatcher.
|
||||||
|
*
|
||||||
|
* GameEventHandler sends commands initiated by the local client to the server
|
||||||
|
* and processes incoming game-wide events. It bridges the networking layer
|
||||||
|
* (protobuf events received via AbstractClient) with the game model and UI
|
||||||
|
* (GameState, PlayerManager, logging, widgets).
|
||||||
|
*
|
||||||
|
* Player-scoped events are forwarded to PlayerEventHandler instances, while
|
||||||
|
* spectator and global game events are handled directly here.
|
||||||
*/
|
*/
|
||||||
//! \todo Document this file.
|
|
||||||
|
|
||||||
#ifndef COCKATRICE_GAME_EVENT_HANDLER_H
|
#ifndef COCKATRICE_GAME_EVENT_HANDLER_H
|
||||||
#define COCKATRICE_GAME_EVENT_HANDLER_H
|
#define COCKATRICE_GAME_EVENT_HANDLER_H
|
||||||
|
|
@ -15,92 +23,310 @@
|
||||||
#include <libcockatrice/protocol/pb/serverinfo_player.pb.h>
|
#include <libcockatrice/protocol/pb/serverinfo_player.pb.h>
|
||||||
|
|
||||||
class AbstractClient;
|
class AbstractClient;
|
||||||
class Response;
|
class AbstractGame;
|
||||||
|
class CommandContainer;
|
||||||
|
class GameCommand;
|
||||||
class GameEventContainer;
|
class GameEventContainer;
|
||||||
class GameEventContext;
|
class GameEventContext;
|
||||||
class GameCommand;
|
class PendingCommand;
|
||||||
class GameState;
|
class PlayerLogic;
|
||||||
class MessageLogWidget;
|
class Response;
|
||||||
class CommandContainer;
|
|
||||||
class Event_GameJoined;
|
|
||||||
class Event_GameStateChanged;
|
class Event_GameStateChanged;
|
||||||
class Event_PlayerPropertiesChanged;
|
class Event_PlayerPropertiesChanged;
|
||||||
class Event_Join;
|
class Event_Join;
|
||||||
class Event_Leave;
|
class Event_Leave;
|
||||||
class Event_GameHostChanged;
|
class Event_GameHostChanged;
|
||||||
class Event_GameClosed;
|
class Event_GameClosed;
|
||||||
class Event_GameStart;
|
|
||||||
class Event_SetActivePlayer;
|
class Event_SetActivePlayer;
|
||||||
class Event_SetActivePhase;
|
class Event_SetActivePhase;
|
||||||
class Event_Ping;
|
|
||||||
class Event_GameSay;
|
class Event_GameSay;
|
||||||
class Event_Kicked;
|
class Event_Kicked;
|
||||||
class Event_ReverseTurn;
|
class Event_ReverseTurn;
|
||||||
class AbstractGame;
|
class Event_Ping;
|
||||||
class PendingCommand;
|
|
||||||
class PlayerLogic;
|
|
||||||
|
|
||||||
inline Q_LOGGING_CATEGORY(GameEventHandlerLog, "game_event_handler");
|
inline Q_LOGGING_CATEGORY(GameEventHandlerLog, "game_event_handler");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class GameEventHandler
|
||||||
|
* @brief Central dispatcher for game-wide commands and events.
|
||||||
|
*
|
||||||
|
* This class owns no game state itself. Instead, it:
|
||||||
|
* - Sends commands to the server on behalf of local players
|
||||||
|
* - Receives and dispatches server-side game events
|
||||||
|
* - Updates the game model indirectly via Player, GameState, and PlayerManager
|
||||||
|
* - Emits high-level signals for UI updates and logging
|
||||||
|
*/
|
||||||
class GameEventHandler : public QObject
|
class GameEventHandler : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
/** Pointer to the owning game instance. */
|
||||||
AbstractGame *game;
|
AbstractGame *game;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
/** @name Construction
|
||||||
|
* Lifecycle and ownership.
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Construct a GameEventHandler.
|
||||||
|
*
|
||||||
|
* The handler is owned by the AbstractGame instance and uses it to
|
||||||
|
* access the game state, players, and network clients.
|
||||||
|
*
|
||||||
|
* @param _game Owning game instance (also used as QObject parent).
|
||||||
|
*/
|
||||||
explicit GameEventHandler(AbstractGame *_game);
|
explicit GameEventHandler(AbstractGame *_game);
|
||||||
|
|
||||||
|
/** @} */
|
||||||
|
|
||||||
|
/** @name Outgoing game commands
|
||||||
|
* Commands initiated locally and sent to the server.
|
||||||
|
*
|
||||||
|
* These methods construct and send protobuf commands corresponding
|
||||||
|
* to user actions in the UI.
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @brief Request advancing the game to the next turn. */
|
||||||
void handleNextTurn();
|
void handleNextTurn();
|
||||||
|
|
||||||
|
/** @brief Request reversing the current turn order. */
|
||||||
void handleReverseTurn();
|
void handleReverseTurn();
|
||||||
|
|
||||||
|
/** @brief Concede the game for the currently active local player. */
|
||||||
void handleActiveLocalPlayerConceded();
|
void handleActiveLocalPlayerConceded();
|
||||||
|
|
||||||
|
/** @brief Undo a previous concede for the active local player. */
|
||||||
void handleActiveLocalPlayerUnconceded();
|
void handleActiveLocalPlayerUnconceded();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set the active phase of the game.
|
||||||
|
*
|
||||||
|
* Typically triggered by the active player selecting a new phase.
|
||||||
|
*
|
||||||
|
* @param phase Phase identifier.
|
||||||
|
*/
|
||||||
void handleActivePhaseChanged(int phase);
|
void handleActivePhaseChanged(int phase);
|
||||||
|
|
||||||
|
/** @brief Leave the current game session. */
|
||||||
void handleGameLeft();
|
void handleGameLeft();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Send a chat message to all players and spectators.
|
||||||
|
*
|
||||||
|
* @param chatMessage Message text.
|
||||||
|
*/
|
||||||
void handleChatMessageSent(const QString &chatMessage);
|
void handleChatMessageSent(const QString &chatMessage);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Delete an existing arrow.
|
||||||
|
*
|
||||||
|
* @param arrowId Unique identifier of the arrow to delete.
|
||||||
|
*/
|
||||||
void handleArrowDeletion(int creatorId, int arrowId);
|
void handleArrowDeletion(int creatorId, int arrowId);
|
||||||
void handleArrowDeletionFinished(const Response &response, int creatorId, int arrowId);
|
void handleArrowDeletionFinished(const Response &response, int creatorId, int arrowId);
|
||||||
|
|
||||||
|
/** @} */
|
||||||
|
|
||||||
|
/** @name Incoming event processing
|
||||||
|
* Entry points for server-sent events.
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Process a container of game events received from the server.
|
||||||
|
*
|
||||||
|
* This is the main dispatch function for incoming game events.
|
||||||
|
* Events are routed to spectator handlers, game-level handlers,
|
||||||
|
* or forwarded to PlayerEventHandler instances as appropriate.
|
||||||
|
*
|
||||||
|
* @param cont Game event container from the server.
|
||||||
|
* @param client Client that received the container.
|
||||||
|
* @param options Processing flags (e.g. silent, replay).
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
processGameEventContainer(const GameEventContainer &cont, AbstractClient *client, EventProcessingOptions options);
|
||||||
|
|
||||||
|
/** @} */
|
||||||
|
|
||||||
|
/** @name Command preparation helpers
|
||||||
|
* Internal helpers for building command containers.
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Wrap a single protobuf command in a PendingCommand.
|
||||||
|
*
|
||||||
|
* @param cmd Protobuf command message.
|
||||||
|
* @return Newly allocated PendingCommand (caller takes ownership).
|
||||||
|
*/
|
||||||
|
PendingCommand *prepareGameCommand(const ::google::protobuf::Message &cmd);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Wrap multiple protobuf commands in a single PendingCommand.
|
||||||
|
*
|
||||||
|
* Ownership of the messages in cmdList is transferred to the handler.
|
||||||
|
*
|
||||||
|
* @param cmdList List of protobuf command messages.
|
||||||
|
* @return Newly allocated PendingCommand.
|
||||||
|
*/
|
||||||
|
PendingCommand *prepareGameCommand(const QList<const ::google::protobuf::Message *> &cmdList);
|
||||||
|
|
||||||
|
/** @} */
|
||||||
|
|
||||||
|
/** @name Spectator event handlers
|
||||||
|
* Events originating from spectators.
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Handle a spectator chat message.
|
||||||
|
*/
|
||||||
void eventSpectatorSay(const Event_GameSay &event, int eventPlayerId, const GameEventContext &context);
|
void eventSpectatorSay(const Event_GameSay &event, int eventPlayerId, const GameEventContext &context);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Handle a spectator leaving the game.
|
||||||
|
*/
|
||||||
void eventSpectatorLeave(const Event_Leave &event, int eventPlayerId, const GameEventContext &context);
|
void eventSpectatorLeave(const Event_Leave &event, int eventPlayerId, const GameEventContext &context);
|
||||||
|
|
||||||
|
/** @} */
|
||||||
|
|
||||||
|
/** @name Game state event handlers
|
||||||
|
* Events that affect global game state.
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Handle a full game state update from the server.
|
||||||
|
*
|
||||||
|
* Used during game startup, reconnection, and resynchronization.
|
||||||
|
*/
|
||||||
void eventGameStateChanged(const Event_GameStateChanged &event, int eventPlayerId, const GameEventContext &context);
|
void eventGameStateChanged(const Event_GameStateChanged &event, int eventPlayerId, const GameEventContext &context);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Update card attachment relationships for all players.
|
||||||
|
*
|
||||||
|
* Called after a game state update to ensure attachments are resolved
|
||||||
|
* consistently across all zones.
|
||||||
|
*/
|
||||||
void processCardAttachmentsForPlayers(const Event_GameStateChanged &event);
|
void processCardAttachmentsForPlayers(const Event_GameStateChanged &event);
|
||||||
|
|
||||||
|
/** @brief Handle a change in game host. */
|
||||||
|
void eventGameHostChanged(const Event_GameHostChanged &event, int eventPlayerId, const GameEventContext &context);
|
||||||
|
|
||||||
|
/** @brief Handle the game being closed by the server. */
|
||||||
|
void eventGameClosed(const Event_GameClosed &event, int eventPlayerId, const GameEventContext &context);
|
||||||
|
|
||||||
|
/** @brief Handle a change of the active player. */
|
||||||
|
void eventSetActivePlayer(const Event_SetActivePlayer &event, int eventPlayerId, const GameEventContext &context);
|
||||||
|
|
||||||
|
/** @brief Handle a change of the active phase. */
|
||||||
|
void eventSetActivePhase(const Event_SetActivePhase &event, int eventPlayerId, const GameEventContext &context);
|
||||||
|
|
||||||
|
/** @brief Handle a turn reversal event. */
|
||||||
|
void eventReverseTurn(const Event_ReverseTurn &event, int eventPlayerId, const GameEventContext &context);
|
||||||
|
|
||||||
|
/** @brief Handle ping / latency updates. */
|
||||||
|
void eventPing(const Event_Ping &event, int eventPlayerId, const GameEventContext &context);
|
||||||
|
|
||||||
|
/** @} */
|
||||||
|
|
||||||
|
/** @name Player lifecycle and property handlers
|
||||||
|
* Events related to players joining, leaving, or changing state.
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Handle updates to a player's properties.
|
||||||
|
*
|
||||||
|
* Includes readiness, concede state, deck selection, sideboard lock,
|
||||||
|
* and connection state changes.
|
||||||
|
*/
|
||||||
void eventPlayerPropertiesChanged(const Event_PlayerPropertiesChanged &event,
|
void eventPlayerPropertiesChanged(const Event_PlayerPropertiesChanged &event,
|
||||||
int eventPlayerId,
|
int eventPlayerId,
|
||||||
const GameEventContext &context);
|
const GameEventContext &context);
|
||||||
|
|
||||||
|
/** @brief Handle a player or spectator joining the game. */
|
||||||
void eventJoin(const Event_Join &event, int eventPlayerId, const GameEventContext &context);
|
void eventJoin(const Event_Join &event, int eventPlayerId, const GameEventContext &context);
|
||||||
|
|
||||||
|
/** @brief Handle a player leaving the game. */
|
||||||
void eventLeave(const Event_Leave &event, int eventPlayerId, const GameEventContext &context);
|
void eventLeave(const Event_Leave &event, int eventPlayerId, const GameEventContext &context);
|
||||||
QString getLeaveReason(Event_Leave::LeaveReason reason);
|
|
||||||
|
/** @brief Handle the local player being kicked from the game. */
|
||||||
void eventKicked(const Event_Kicked &event, int eventPlayerId, const GameEventContext &context);
|
void eventKicked(const Event_Kicked &event, int eventPlayerId, const GameEventContext &context);
|
||||||
void eventGameHostChanged(const Event_GameHostChanged &event, int eventPlayerId, const GameEventContext &context);
|
|
||||||
void eventGameClosed(const Event_GameClosed &event, int eventPlayerId, const GameEventContext &context);
|
|
||||||
|
|
||||||
void eventSetActivePlayer(const Event_SetActivePlayer &event, int eventPlayerId, const GameEventContext &context);
|
/**
|
||||||
void eventSetActivePhase(const Event_SetActivePhase &event, int eventPlayerId, const GameEventContext &context);
|
* @brief Convert a leave reason enum to a human-readable string.
|
||||||
void eventPing(const Event_Ping &event, int eventPlayerId, const GameEventContext &context);
|
*/
|
||||||
void eventReverseTurn(const Event_ReverseTurn &event, int eventPlayerId, const GameEventContext & /*context*/);
|
QString getLeaveReason(Event_Leave::LeaveReason reason);
|
||||||
|
|
||||||
void commandFinished(const Response &response);
|
/** @} */
|
||||||
|
|
||||||
void
|
|
||||||
processGameEventContainer(const GameEventContainer &cont, AbstractClient *client, EventProcessingOptions options);
|
|
||||||
PendingCommand *prepareGameCommand(const ::google::protobuf::Message &cmd);
|
|
||||||
PendingCommand *prepareGameCommand(const QList<const ::google::protobuf::Message *> &cmdList);
|
|
||||||
public slots:
|
public slots:
|
||||||
|
/** @name Command dispatch slots
|
||||||
|
* Low-level command transmission.
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Send a prepared PendingCommand.
|
||||||
|
*
|
||||||
|
* @param pend Pending command to send.
|
||||||
|
* @param playerId Player whose client should send the command.
|
||||||
|
*/
|
||||||
void sendGameCommand(PendingCommand *pend, int playerId = -1);
|
void sendGameCommand(PendingCommand *pend, int playerId = -1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Send a single protobuf command.
|
||||||
|
*
|
||||||
|
* @param command Protobuf command message.
|
||||||
|
* @param playerId Player whose client should send the command.
|
||||||
|
*/
|
||||||
void sendGameCommand(const ::google::protobuf::Message &command, int playerId = -1);
|
void sendGameCommand(const ::google::protobuf::Message &command, int playerId = -1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Called when a PendingCommand finishes execution.
|
||||||
|
*
|
||||||
|
* Used to detect server-side errors such as chat flood protection.
|
||||||
|
*/
|
||||||
|
void commandFinished(const Response &response);
|
||||||
|
|
||||||
|
/** @} */
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
|
/** @name Core state signals
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
void emitUserEvent();
|
void emitUserEvent();
|
||||||
|
void containerProcessingStarted(GameEventContext context);
|
||||||
|
void containerProcessingDone();
|
||||||
|
void gameFlooded();
|
||||||
|
void setContextJudgeName(QString judgeName);
|
||||||
|
|
||||||
|
/** @} */
|
||||||
|
|
||||||
|
/** @name Player and spectator signals
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
void addPlayerToAutoCompleteList(QString playerName);
|
void addPlayerToAutoCompleteList(QString playerName);
|
||||||
void localPlayerDeckSelected(PlayerLogic *localPlayer, int playerId, ServerInfo_Player playerInfo);
|
void localPlayerDeckSelected(PlayerLogic *localPlayer, int playerId, ServerInfo_Player playerInfo);
|
||||||
void remotePlayerDeckSelected(QString deckList, int playerId, QString playerName);
|
void remotePlayerDeckSelected(QString deckList, int playerId, QString playerName);
|
||||||
void remotePlayersDecksSelected(QVector<QPair<int, QPair<QString, QString>>> opponentDecks);
|
void remotePlayersDecksSelected(QVector<QPair<int, QPair<QString, QString>>> opponentDecks);
|
||||||
void localPlayerSideboardLocked(int playerId, bool sideboardLocked);
|
void localPlayerSideboardLocked(int playerId, bool sideboardLocked);
|
||||||
void localPlayerReadyStateChanged(int playerId, bool ready);
|
void localPlayerReadyStateChanged(int playerId, bool ready);
|
||||||
|
|
||||||
|
/** @} */
|
||||||
|
|
||||||
|
/** @name Game flow signals
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
void gameStopped();
|
void gameStopped();
|
||||||
void gameClosed();
|
void gameClosed();
|
||||||
void playerPropertiesChanged(const ServerInfo_PlayerProperties &prop, int playerId);
|
void playerPropertiesChanged(const ServerInfo_PlayerProperties &prop, int playerId);
|
||||||
|
|
@ -109,11 +335,15 @@ signals:
|
||||||
void playerKicked();
|
void playerKicked();
|
||||||
void spectatorJoined(const ServerInfo_PlayerProperties &spectatorInfo);
|
void spectatorJoined(const ServerInfo_PlayerProperties &spectatorInfo);
|
||||||
void spectatorLeft(int leavingSpectatorId);
|
void spectatorLeft(int leavingSpectatorId);
|
||||||
void gameFlooded();
|
|
||||||
void containerProcessingStarted(GameEventContext context);
|
|
||||||
void setContextJudgeName(QString judgeName);
|
|
||||||
void containerProcessingDone();
|
|
||||||
void arrowDeleted(int creatorId, int arrowId);
|
void arrowDeleted(int creatorId, int arrowId);
|
||||||
|
|
||||||
|
/** @} */
|
||||||
|
|
||||||
|
/** @name Logging signals
|
||||||
|
* Signals consumed by MessageLogWidget.
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
void logSpectatorSay(ServerInfo_User userInfo, QString message);
|
void logSpectatorSay(ServerInfo_User userInfo, QString message);
|
||||||
void logSpectatorLeave(QString name, QString reason);
|
void logSpectatorLeave(QString name, QString reason);
|
||||||
void logGameStart();
|
void logGameStart();
|
||||||
|
|
@ -132,6 +362,8 @@ signals:
|
||||||
void logActivePhaseChanged(int activePhase);
|
void logActivePhaseChanged(int activePhase);
|
||||||
void logConcede(int playerId);
|
void logConcede(int playerId);
|
||||||
void logUnconcede(int playerId);
|
void logUnconcede(int playerId);
|
||||||
|
|
||||||
|
/** @} */
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // COCKATRICE_GAME_EVENT_HANDLER_H
|
#endif // COCKATRICE_GAME_EVENT_HANDLER_H
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,11 @@
|
||||||
#include <libcockatrice/protocol/pb/command_shuffle.pb.h>
|
#include <libcockatrice/protocol/pb/command_shuffle.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_undo_draw.pb.h>
|
#include <libcockatrice/protocol/pb/command_undo_draw.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/context_move_card.pb.h>
|
#include <libcockatrice/protocol/pb/context_move_card.pb.h>
|
||||||
|
#include <libcockatrice/settings/card_override_settings.h>
|
||||||
|
#include <libcockatrice/settings/interface_settings.h>
|
||||||
|
#include <libcockatrice/utility/clamped_arithmetic.h>
|
||||||
|
#include <libcockatrice/utility/counter_limits.h>
|
||||||
#include <libcockatrice/utility/expression.h>
|
#include <libcockatrice/utility/expression.h>
|
||||||
#include <libcockatrice/utility/trice_limits.h>
|
|
||||||
#include <libcockatrice/utility/zone_names.h>
|
#include <libcockatrice/utility/zone_names.h>
|
||||||
|
|
||||||
// milliseconds in between triggers of the move top cards until action
|
// milliseconds in between triggers of the move top cards until action
|
||||||
|
|
@ -66,7 +69,7 @@ void PlayerActions::playCard(CardItem *card, bool faceDown)
|
||||||
const CardInfo &info = exactCard.getInfo();
|
const CardInfo &info = exactCard.getInfo();
|
||||||
|
|
||||||
int tableRow = info.getUiAttributes().tableRow;
|
int tableRow = info.getUiAttributes().tableRow;
|
||||||
bool playToStack = SettingsCache::instance().getPlayToStack();
|
bool playToStack = SettingsCache::instance().userInterface().getPlayToStack();
|
||||||
QString currentZone = card->getZone()->getName();
|
QString currentZone = card->getZone()->getName();
|
||||||
if (!faceDown && currentZone == ZoneNames::STACK && tableRow == 3) {
|
if (!faceDown && currentZone == ZoneNames::STACK && tableRow == 3) {
|
||||||
cmd.set_target_zone(ZoneNames::GRAVE);
|
cmd.set_target_zone(ZoneNames::GRAVE);
|
||||||
|
|
@ -309,7 +312,7 @@ void PlayerActions::actDrawCard()
|
||||||
|
|
||||||
void PlayerActions::actRequestMulliganDialog()
|
void PlayerActions::actRequestMulliganDialog()
|
||||||
{
|
{
|
||||||
int startSize = SettingsCache::instance().getStartingHandSize();
|
int startSize = SettingsCache::instance().userInterface().getStartingHandSize();
|
||||||
int handSize = player->getHandZone()->getCards().size();
|
int handSize = player->getHandZone()->getCards().size();
|
||||||
int deckSize = player->getDeckZone()->getCards().size() + handSize;
|
int deckSize = player->getDeckZone()->getCards().size() + handSize;
|
||||||
|
|
||||||
|
|
@ -325,7 +328,7 @@ void PlayerActions::actMulligan(int number)
|
||||||
}
|
}
|
||||||
|
|
||||||
doMulligan(number);
|
doMulligan(number);
|
||||||
SettingsCache::instance().setStartingHandSize(number);
|
SettingsCache::instance().userInterface().setStartingHandSize(number);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlayerActions::actMulliganSameSize()
|
void PlayerActions::actMulliganSameSize()
|
||||||
|
|
@ -929,13 +932,13 @@ void PlayerActions::setLastTokenInfo(CardInfoPtr cardInfo)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
lastTokenInfo = {.name = cardInfo->getName(),
|
lastTokenInfo = {
|
||||||
.color = cardInfo->getColors().isEmpty() ? QString() : cardInfo->getColors().left(1).toLower(),
|
.name = cardInfo->getName(),
|
||||||
.pt = cardInfo->getPowTough(),
|
.color = cardInfo->getColors().isEmpty() ? QString() : cardInfo->getColors().left(1).toLower(),
|
||||||
.annotation = SettingsCache::instance().getAnnotateTokens() ? cardInfo->getText() : "",
|
.pt = cardInfo->getPowTough(),
|
||||||
.destroy = true,
|
.annotation = SettingsCache::instance().userInterface().getAnnotateTokens() ? cardInfo->getText() : "",
|
||||||
.providerId =
|
.destroy = true,
|
||||||
SettingsCache::instance().cardOverrides().getCardPreferenceOverride(cardInfo->getName())};
|
.providerId = SettingsCache::instance().cardOverrides().getCardPreferenceOverride(cardInfo->getName())};
|
||||||
|
|
||||||
lastTokenTableRow = TableZone::tableRowToGridY(cardInfo->getUiAttributes().tableRow);
|
lastTokenTableRow = TableZone::tableRowToGridY(cardInfo->getUiAttributes().tableRow);
|
||||||
|
|
||||||
|
|
@ -1168,7 +1171,7 @@ void PlayerActions::createCard(const CardItem *sourceCard,
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.set_pt(cardInfo->getPowTough().toStdString());
|
cmd.set_pt(cardInfo->getPowTough().toStdString());
|
||||||
if (SettingsCache::instance().getAnnotateTokens()) {
|
if (SettingsCache::instance().userInterface().getAnnotateTokens()) {
|
||||||
cmd.set_annotation(cardInfo->getText().toStdString());
|
cmd.set_annotation(cardInfo->getText().toStdString());
|
||||||
} else {
|
} else {
|
||||||
cmd.set_annotation("");
|
cmd.set_annotation("");
|
||||||
|
|
@ -1351,11 +1354,7 @@ void PlayerActions::actSetPT(QList<CardItem *> selectedCards, const QString &pt)
|
||||||
const auto oldpt = CardItem::parsePT(card->getPT());
|
const auto oldpt = CardItem::parsePT(card->getPT());
|
||||||
int ptIter = 0;
|
int ptIter = 0;
|
||||||
for (const auto &_item : ptList) {
|
for (const auto &_item : ptList) {
|
||||||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
|
|
||||||
if (_item.typeId() == QMetaType::Type::Int) {
|
if (_item.typeId() == QMetaType::Type::Int) {
|
||||||
#else
|
|
||||||
if (_item.type() == QVariant::Int) {
|
|
||||||
#endif
|
|
||||||
int oldItem = ptIter < oldpt.size() ? oldpt.at(ptIter).toInt() : 0;
|
int oldItem = ptIter < oldpt.size() ? oldpt.at(ptIter).toInt() : 0;
|
||||||
newpt += '/' + QString::number(oldItem + _item.toInt());
|
newpt += '/' + QString::number(oldItem + _item.toInt());
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1530,12 +1529,15 @@ void PlayerActions::offsetCardCounter(QList<CardItem *> selectedCards, int count
|
||||||
QList<const ::google::protobuf::Message *> commandList;
|
QList<const ::google::protobuf::Message *> commandList;
|
||||||
for (auto card : selectedCards) {
|
for (auto card : selectedCards) {
|
||||||
int oldValue = card->getCounters().value(counterId, 0);
|
int oldValue = card->getCounters().value(counterId, 0);
|
||||||
int newValue = oldValue + offset;
|
|
||||||
|
|
||||||
// Early exit optimization: server enforces [0, MAX_COUNTERS_ON_CARD].
|
// Overflow-safe clamp to the server-enforced range [0, MAX_COUNTER_VALUE];
|
||||||
// Compare clamped value to allow recovery from invalid states.
|
// a result differing from oldValue also corrects an out-of-range cached value.
|
||||||
int clampedValue = qBound(0, newValue, MAX_COUNTERS_ON_CARD);
|
// Callers only ever pass offset == ±1 (actAddCardCounter / actRemoveCardCounter).
|
||||||
if (clampedValue != oldValue) {
|
// This client-side clamp is a defense-in-depth UX check, consistent with
|
||||||
|
// actSetCardCounter and actIncrementAllCardCounters; the server remains the
|
||||||
|
// authoritative enforcer of the bounds.
|
||||||
|
int newValue = addClamped(oldValue, offset, 0, MAX_COUNTER_VALUE);
|
||||||
|
if (newValue != oldValue) {
|
||||||
auto *cmd = new Command_SetCardCounter;
|
auto *cmd = new Command_SetCardCounter;
|
||||||
cmd->set_zone(card->getZone()->getName().toStdString());
|
cmd->set_zone(card->getZone()->getName().toStdString());
|
||||||
cmd->set_card_id(card->getId());
|
cmd->set_card_id(card->getId());
|
||||||
|
|
@ -1568,7 +1570,7 @@ void PlayerActions::actSetCardCounter(QList<CardItem *> selectedCards, int count
|
||||||
Expression exp(oldValue);
|
Expression exp(oldValue);
|
||||||
double parsed = exp.parse(counterValue);
|
double parsed = exp.parse(counterValue);
|
||||||
// Clamp in double precision first to avoid UB, then cast
|
// Clamp in double precision first to avoid UB, then cast
|
||||||
int number = static_cast<int>(qBound(0.0, parsed, static_cast<double>(MAX_COUNTERS_ON_CARD)));
|
int number = static_cast<int>(qBound(0.0, parsed, static_cast<double>(MAX_COUNTER_VALUE)));
|
||||||
|
|
||||||
auto *cmd = new Command_SetCardCounter;
|
auto *cmd = new Command_SetCardCounter;
|
||||||
cmd->set_zone(card->getZone()->getName().toStdString());
|
cmd->set_zone(card->getZone()->getName().toStdString());
|
||||||
|
|
@ -1598,7 +1600,7 @@ void PlayerActions::actIncrementAllCardCounters(QList<CardItem *> cardsToUpdate)
|
||||||
counterIterator.next();
|
counterIterator.next();
|
||||||
int counterId = counterIterator.key();
|
int counterId = counterIterator.key();
|
||||||
int currentValue = counterIterator.value();
|
int currentValue = counterIterator.value();
|
||||||
if (currentValue >= MAX_COUNTERS_ON_CARD) {
|
if (currentValue >= MAX_COUNTER_VALUE) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,24 @@
|
||||||
/**
|
/**
|
||||||
* @file player_event_handler.h
|
* @file player_event_handler.h
|
||||||
* @ingroup GameLogicPlayers
|
* @ingroup GameLogicPlayers
|
||||||
|
* @brief Player-scoped game event handler.
|
||||||
|
*
|
||||||
|
* PlayerEventHandler applies game events that affect a single 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
|
#ifndef COCKATRICE_PLAYER_EVENT_HANDLER_H
|
||||||
#define COCKATRICE_PLAYER_EVENT_HANDLER_H
|
#define COCKATRICE_PLAYER_EVENT_HANDLER_H
|
||||||
|
|
||||||
#include "event_processing_options.h"
|
#include "event_processing_options.h"
|
||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
|
@ -16,6 +29,7 @@
|
||||||
class CardItem;
|
class CardItem;
|
||||||
class CardZoneLogic;
|
class CardZoneLogic;
|
||||||
class PlayerLogic;
|
class PlayerLogic;
|
||||||
|
|
||||||
class Event_AttachCard;
|
class Event_AttachCard;
|
||||||
class Event_ChangeZoneProperties;
|
class Event_ChangeZoneProperties;
|
||||||
class Event_CreateArrow;
|
class Event_CreateArrow;
|
||||||
|
|
@ -37,11 +51,176 @@ class Event_SetCounter;
|
||||||
class Event_Shuffle;
|
class Event_Shuffle;
|
||||||
class Event_GameLogNotice;
|
class Event_GameLogNotice;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class PlayerEventHandler
|
||||||
|
* @brief Applies player-specific game events and emits corresponding log signals.
|
||||||
|
*
|
||||||
|
* Design notes:
|
||||||
|
* - All event handlers assume events are authoritative and already validated
|
||||||
|
* by the server.
|
||||||
|
* - Most handlers mutate both logical state (CardItem, CardZoneLogic, counters)
|
||||||
|
* and visual/UI state (views, arrows, menus).
|
||||||
|
* - Logging signals are emitted *after* or *during* state mutation, depending
|
||||||
|
* on whether later mutations would invalidate log data.
|
||||||
|
*/
|
||||||
class PlayerEventHandler : public QObject
|
class PlayerEventHandler : public QObject
|
||||||
{
|
{
|
||||||
|
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Construct a PlayerEventHandler bound to a Player.
|
||||||
|
* @param player Owning player instance.
|
||||||
|
*/
|
||||||
|
explicit PlayerEventHandler(PlayerLogic *player);
|
||||||
|
|
||||||
|
/** @name Event dispatch
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Dispatch a generic GameEvent to the appropriate handler.
|
||||||
|
*
|
||||||
|
* This is the single entry point used by GameEventHandler. It extracts
|
||||||
|
* the correct protobuf extension and forwards the event to a typed
|
||||||
|
* handler method.
|
||||||
|
*
|
||||||
|
* @param type Game event type enum.
|
||||||
|
* @param event Generic protobuf container.
|
||||||
|
* @param context Additional context (undo, judge, etc.).
|
||||||
|
* @param options Processing options (UI suppression, reveal behavior).
|
||||||
|
*/
|
||||||
|
void processGameEvent(GameEvent::GameEventType type,
|
||||||
|
const GameEvent &event,
|
||||||
|
const GameEventContext &context,
|
||||||
|
EventProcessingOptions options);
|
||||||
|
|
||||||
|
/** @} */
|
||||||
|
|
||||||
|
/** @name Chat and randomization events
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// Handle in-game chat messages from this player.
|
||||||
|
void eventGameSay(const Event_GameSay &event);
|
||||||
|
|
||||||
|
/// Handle zone shuffle events (typically libraries).
|
||||||
|
void eventShuffle(const Event_Shuffle &event);
|
||||||
|
|
||||||
|
/// Handle die roll events.
|
||||||
|
void eventRollDie(const Event_RollDie &event);
|
||||||
|
|
||||||
|
/** @} */
|
||||||
|
|
||||||
|
/** @name Arrow and targeting events
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// Create a visual arrow between cards or players.
|
||||||
|
void eventCreateArrow(const Event_CreateArrow &event);
|
||||||
|
|
||||||
|
/// Delete an existing arrow.
|
||||||
|
void eventDeleteArrow(const Event_DeleteArrow &event);
|
||||||
|
|
||||||
|
/** @} */
|
||||||
|
|
||||||
|
/** @name Token and card creation
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// Create a token card in a target zone.
|
||||||
|
void eventCreateToken(const Event_CreateToken &event);
|
||||||
|
|
||||||
|
/** @} */
|
||||||
|
|
||||||
|
/** @name Card attribute and counter updates
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set a card attribute (tapped, PT, annotation, etc.).
|
||||||
|
*
|
||||||
|
* May apply to a single card or all cards in a zone if no card ID
|
||||||
|
* is provided by the event.
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
eventSetCardAttr(const Event_SetCardAttr &event, const GameEventContext &context, EventProcessingOptions options);
|
||||||
|
|
||||||
|
/// Update a counter attached to a card.
|
||||||
|
void eventSetCardCounter(const Event_SetCardCounter &event);
|
||||||
|
|
||||||
|
/// Create a player-level counter.
|
||||||
|
void eventCreateCounter(const Event_CreateCounter &event);
|
||||||
|
|
||||||
|
/// Set a player-level counter value.
|
||||||
|
void eventSetCounter(const Event_SetCounter &event);
|
||||||
|
|
||||||
|
/// Delete a player-level counter.
|
||||||
|
void eventDelCounter(const Event_DelCounter &event);
|
||||||
|
|
||||||
|
/** @} */
|
||||||
|
|
||||||
|
/** @name Zone-level operations
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// Log a zone dump (e.g. reveal graveyard/library contents).
|
||||||
|
void eventDumpZone(const Event_DumpZone &event);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Move a card between zones and/or players.
|
||||||
|
*
|
||||||
|
* This is one of the most complex handlers:
|
||||||
|
* - Removes the card from the start zone
|
||||||
|
* - Updates card identity and ownership if needed
|
||||||
|
* - Handles attachments and arrows
|
||||||
|
* - Emits appropriate move or undo-draw logs
|
||||||
|
* - Inserts the card into the target zone
|
||||||
|
*/
|
||||||
|
void eventMoveCard(const Event_MoveCard &event, const GameEventContext &context);
|
||||||
|
|
||||||
|
/// Flip a card face up or face down.
|
||||||
|
void eventFlipCard(const Event_FlipCard &event);
|
||||||
|
|
||||||
|
/// Destroy a card and clean up attachments.
|
||||||
|
void eventDestroyCard(const Event_DestroyCard &event);
|
||||||
|
|
||||||
|
/// Attach or detach a card to/from another card.
|
||||||
|
void eventAttachCard(const Event_AttachCard &event);
|
||||||
|
|
||||||
|
/** @} */
|
||||||
|
|
||||||
|
/** @name Draw and reveal operations
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// Draw one or more cards from the deck.
|
||||||
|
void eventDrawCards(const Event_DrawCards &event);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Reveal cards from a zone.
|
||||||
|
*
|
||||||
|
* Handles peeking, in-place top-card reveals, full reveal windows,
|
||||||
|
* and write-access granting.
|
||||||
|
*/
|
||||||
|
void eventRevealCards(const Event_RevealCards &event, EventProcessingOptions options);
|
||||||
|
|
||||||
|
/** @} */
|
||||||
|
|
||||||
|
/** @name Zone configuration
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// Update zone visibility and reveal behavior.
|
||||||
|
void eventChangeZoneProperties(const Event_ChangeZoneProperties &event);
|
||||||
|
|
||||||
|
/** @} */
|
||||||
|
|
||||||
|
void eventGameLogNotice(const Event_GameLogNotice &event);
|
||||||
signals:
|
signals:
|
||||||
|
/** @name Logging signals
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
void logSay(PlayerLogic *player, QString message);
|
void logSay(PlayerLogic *player, QString message);
|
||||||
void logShuffle(PlayerLogic *player, CardZoneLogic *zone, int start, int end);
|
void logShuffle(PlayerLogic *player, CardZoneLogic *zone, int start, int end);
|
||||||
void logRollDie(PlayerLogic *player, int sides, const QList<uint> &rolls);
|
void logRollDie(PlayerLogic *player, int sides, const QList<uint> &rolls);
|
||||||
|
|
@ -82,40 +261,13 @@ signals:
|
||||||
bool isLentToAnotherPlayer = false);
|
bool isLentToAnotherPlayer = false);
|
||||||
void logAlwaysRevealTopCard(PlayerLogic *player, CardZoneLogic *zone, bool reveal);
|
void logAlwaysRevealTopCard(PlayerLogic *player, CardZoneLogic *zone, bool reveal);
|
||||||
void logAlwaysLookAtTopCard(PlayerLogic *player, CardZoneLogic *zone, bool reveal);
|
void logAlwaysLookAtTopCard(PlayerLogic *player, CardZoneLogic *zone, bool reveal);
|
||||||
|
/** @} */
|
||||||
|
|
||||||
void cardZoneChanged(CardItem *card, bool sameZone);
|
void cardZoneChanged(CardItem *card, bool sameZone);
|
||||||
void requestCardMenuUpdate(const CardItem *card);
|
void requestCardMenuUpdate(const CardItem *card);
|
||||||
|
|
||||||
public:
|
|
||||||
PlayerEventHandler(PlayerLogic *player);
|
|
||||||
|
|
||||||
void processGameEvent(GameEvent::GameEventType type,
|
|
||||||
const GameEvent &event,
|
|
||||||
const GameEventContext &context,
|
|
||||||
EventProcessingOptions options);
|
|
||||||
|
|
||||||
void eventGameSay(const Event_GameSay &event);
|
|
||||||
void eventShuffle(const Event_Shuffle &event);
|
|
||||||
void eventRollDie(const Event_RollDie &event);
|
|
||||||
void eventCreateArrow(const Event_CreateArrow &event);
|
|
||||||
void eventDeleteArrow(const Event_DeleteArrow &event);
|
|
||||||
void eventCreateToken(const Event_CreateToken &event);
|
|
||||||
void
|
|
||||||
eventSetCardAttr(const Event_SetCardAttr &event, const GameEventContext &context, EventProcessingOptions options);
|
|
||||||
void eventSetCardCounter(const Event_SetCardCounter &event);
|
|
||||||
void eventCreateCounter(const Event_CreateCounter &event);
|
|
||||||
void eventSetCounter(const Event_SetCounter &event);
|
|
||||||
void eventDelCounter(const Event_DelCounter &event);
|
|
||||||
void eventDumpZone(const Event_DumpZone &event);
|
|
||||||
void eventMoveCard(const Event_MoveCard &event, const GameEventContext &context);
|
|
||||||
void eventFlipCard(const Event_FlipCard &event);
|
|
||||||
void eventDestroyCard(const Event_DestroyCard &event);
|
|
||||||
void eventAttachCard(const Event_AttachCard &event);
|
|
||||||
void eventDrawCards(const Event_DrawCards &event);
|
|
||||||
void eventRevealCards(const Event_RevealCards &event, EventProcessingOptions options);
|
|
||||||
void eventChangeZoneProperties(const Event_ChangeZoneProperties &event);
|
|
||||||
void eventGameLogNotice(const Event_GameLogNotice &event);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
/** Owning player instance. */
|
||||||
PlayerLogic *player;
|
PlayerLogic *player;
|
||||||
|
|
||||||
void setCardAttrHelper(const GameEventContext &context,
|
void setCardAttrHelper(const GameEventContext &context,
|
||||||
|
|
|
||||||
|
|
@ -250,6 +250,22 @@ void PlayerLogic::setDeck(const DeckList &_deck)
|
||||||
emit deckChanged();
|
emit deckChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PlayerLogic::setPlaymatFromProperties(const ServerInfo_PlayerProperties &props)
|
||||||
|
{
|
||||||
|
if (props.has_playmat_params() && !props.playmat_params().card_name().empty()) {
|
||||||
|
const auto &pp = props.playmat_params();
|
||||||
|
remotePlaymatCard = {QString::fromStdString(pp.card_name()), QString::fromStdString(pp.card_provider_id())};
|
||||||
|
remotePlaymatParams = {qBound(0.0, pp.margin_pct_l(), 0.95), qBound(0.0, pp.margin_pct_r(), 0.95),
|
||||||
|
qBound(0.0, pp.vertical_offset(), 1.0), qBound(0.1, pp.zoom(), 4.0)};
|
||||||
|
hasRemotePlaymat = true;
|
||||||
|
} else {
|
||||||
|
remotePlaymatCard = CardRef{};
|
||||||
|
remotePlaymatParams = PlaymatParams{};
|
||||||
|
hasRemotePlaymat = false;
|
||||||
|
}
|
||||||
|
emit playmatChanged();
|
||||||
|
}
|
||||||
|
|
||||||
CounterState *PlayerLogic::addCounter(const ServerInfo_Counter &counter)
|
CounterState *PlayerLogic::addCounter(const ServerInfo_Counter &counter)
|
||||||
{
|
{
|
||||||
return addCounter(counter.id(), QString::fromStdString(counter.name()),
|
return addCounter(counter.id(), QString::fromStdString(counter.name()),
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
#include "../zones/table_zone_logic.h"
|
#include "../zones/table_zone_logic.h"
|
||||||
#include "player_event_handler.h"
|
#include "player_event_handler.h"
|
||||||
#include "player_info.h"
|
#include "player_info.h"
|
||||||
|
#include "player_manager.h"
|
||||||
|
|
||||||
#include <QInputDialog>
|
#include <QInputDialog>
|
||||||
#include <QLoggingCategory>
|
#include <QLoggingCategory>
|
||||||
|
|
@ -72,6 +73,8 @@ signals:
|
||||||
const QList<const ServerInfo_Card *> &cardList,
|
const QList<const ServerInfo_Card *> &cardList,
|
||||||
bool withWritePermission);
|
bool withWritePermission);
|
||||||
void deckChanged();
|
void deckChanged();
|
||||||
|
/** @brief Emitted when the remote playmat (card/params) is updated from player properties. */
|
||||||
|
void playmatChanged();
|
||||||
void newCardAdded(AbstractCardItem *card);
|
void newCardAdded(AbstractCardItem *card);
|
||||||
void requestCardMenuUpdate(const CardItem *card);
|
void requestCardMenuUpdate(const CardItem *card);
|
||||||
void counterAdded(CounterState *state);
|
void counterAdded(CounterState *state);
|
||||||
|
|
@ -226,6 +229,20 @@ public:
|
||||||
|
|
||||||
void setZoneId(int _zoneId);
|
void setZoneId(int _zoneId);
|
||||||
|
|
||||||
|
void setPlaymatFromProperties(const ServerInfo_PlayerProperties &props);
|
||||||
|
const CardRef &getRemotePlaymatCard() const
|
||||||
|
{
|
||||||
|
return remotePlaymatCard;
|
||||||
|
}
|
||||||
|
const PlaymatParams &getRemotePlaymatParams() const
|
||||||
|
{
|
||||||
|
return remotePlaymatParams;
|
||||||
|
}
|
||||||
|
bool getHasRemotePlaymat() const
|
||||||
|
{
|
||||||
|
return hasRemotePlaymat;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
AbstractGame *game;
|
AbstractGame *game;
|
||||||
PlayerInfo *playerInfo;
|
PlayerInfo *playerInfo;
|
||||||
|
|
@ -243,6 +260,11 @@ private:
|
||||||
|
|
||||||
bool dialogSemaphore;
|
bool dialogSemaphore;
|
||||||
QList<CardItem *> cardsToDelete;
|
QList<CardItem *> cardsToDelete;
|
||||||
|
|
||||||
|
// Playmat from player properties (for opponent display)
|
||||||
|
CardRef remotePlaymatCard;
|
||||||
|
PlaymatParams remotePlaymatParams;
|
||||||
|
bool hasRemotePlaymat = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
class AnnotationDialog : public QInputDialog
|
class AnnotationDialog : public QInputDialog
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
#include "../interface/widgets/tabs/tab_game.h"
|
#include "../interface/widgets/tabs/tab_game.h"
|
||||||
|
|
||||||
Replay::Replay(QObject *_parent, GameReplay *_replay, bool isLocalGame) : AbstractGame(_parent)
|
Replay::Replay(QObject *_parent, const GameReplay *_replay, bool isLocalGame) : AbstractGame(_parent)
|
||||||
{
|
{
|
||||||
gameState = new GameState(this, 0, -1, isLocalGame, {}, false, false, -1, false);
|
gameState = new GameState(this, 0, -1, isLocalGame, {}, false, false, -1, false);
|
||||||
connect(gameMetaInfo, &GameMetaInfo::startedChanged, gameState, &GameState::onStartedChanged);
|
connect(gameMetaInfo, &GameMetaInfo::startedChanged, gameState, &GameState::onStartedChanged);
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ class Replay : public AbstractGame
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit Replay(QObject *_parent, GameReplay *_replay, bool isLocalGame);
|
explicit Replay(QObject *_parent, const GameReplay *_replay, bool isLocalGame);
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // COCKATRICE_REPLAY_H
|
#endif // COCKATRICE_REPLAY_H
|
||||||
|
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
#ifndef SELECTION_SUBTYPE_TALLY_H
|
|
||||||
#define SELECTION_SUBTYPE_TALLY_H
|
|
||||||
|
|
||||||
#include <QList>
|
|
||||||
#include <QString>
|
|
||||||
|
|
||||||
class CardItem;
|
|
||||||
|
|
||||||
/** @brief A single subtype (e.g., "Goblin", "Warrior") with its occurrence count. */
|
|
||||||
struct SubtypeEntry
|
|
||||||
{
|
|
||||||
QString name; ///< The subtype name
|
|
||||||
int count; ///< Number of selected cards with this subtype
|
|
||||||
|
|
||||||
bool operator==(const SubtypeEntry &other) const
|
|
||||||
{
|
|
||||||
return name == other.name && count == other.count;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Extracts and tallies subtypes from selected cards.
|
|
||||||
*/
|
|
||||||
namespace SelectionSubtypeTally
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @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<SubtypeEntry> countSubtypes(const QList<CardItem *> &cards);
|
|
||||||
} // namespace SelectionSubtypeTally
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
#include "../../client/settings/cache_settings.h"
|
#include "../../client/settings/cache_settings.h"
|
||||||
#include "../../game_graphics/board/card_item.h"
|
#include "../../game_graphics/board/card_item.h"
|
||||||
|
|
||||||
|
#include <libcockatrice/settings/interface_settings.h>
|
||||||
/**
|
/**
|
||||||
* @param _player the player that the cards are revealed to.
|
* @param _player the player that the cards are revealed to.
|
||||||
* @param _origZone the zone the cards were revealed from.
|
* @param _origZone the zone the cards were revealed from.
|
||||||
|
|
@ -57,7 +58,7 @@ bool ZoneViewZoneLogic::prepareAddCard(int x)
|
||||||
|
|
||||||
// autoclose check is done both here and in removeCard
|
// autoclose check is done both here and in removeCard
|
||||||
|
|
||||||
if (cards.isEmpty() && !doInsert && SettingsCache::instance().getCloseEmptyCardView()) {
|
if (cards.isEmpty() && !doInsert && SettingsCache::instance().userInterface().getCloseEmptyCardView()) {
|
||||||
emit closeView();
|
emit closeView();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -144,7 +145,7 @@ void ZoneViewZoneLogic::removeCard(int position, bool toNewZone)
|
||||||
// card gets dragged within the view.
|
// card gets dragged within the view.
|
||||||
// Another autoclose check is done in prepareAddCard so that the view autocloses if the last card was moved to an
|
// Another autoclose check is done in prepareAddCard so that the view autocloses if the last card was moved to an
|
||||||
// unrevealed portion of the same zone.
|
// unrevealed portion of the same zone.
|
||||||
if (cards.isEmpty() && SettingsCache::instance().getCloseEmptyCardView() && toNewZone) {
|
if (cards.isEmpty() && SettingsCache::instance().userInterface().getCloseEmptyCardView() && toNewZone) {
|
||||||
emit closeView();
|
emit closeView();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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 <QDebug>
|
||||||
#include <QGraphicsSceneMouseEvent>
|
#include <QGraphicsSceneMouseEvent>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
|
#include <libcockatrice/settings/cards_display_settings.h>
|
||||||
|
|
||||||
const QColor GHOST_MASK = QColor(255, 255, 255, 50);
|
const QColor GHOST_MASK = QColor(255, 255, 255, 50);
|
||||||
|
|
||||||
|
|
@ -34,12 +35,13 @@ AbstractCardDragItem::AbstractCardDragItem(AbstractCardItem *_item,
|
||||||
|
|
||||||
setCacheMode(DeviceCoordinateCache);
|
setCacheMode(DeviceCoordinateCache);
|
||||||
|
|
||||||
connect(&SettingsCache::instance(), &SettingsCache::roundCardCornersChanged, this, [this](bool _roundCardCorners) {
|
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::roundCardCornersChanged, this,
|
||||||
Q_UNUSED(_roundCardCorners);
|
[this](bool _roundCardCorners) {
|
||||||
|
Q_UNUSED(_roundCardCorners);
|
||||||
|
|
||||||
prepareGeometryChange();
|
prepareGeometryChange();
|
||||||
update();
|
update();
|
||||||
});
|
});
|
||||||
|
|
||||||
connect(item, &QObject::destroyed, this, &AbstractCardDragItem::deleteLater);
|
connect(item, &QObject::destroyed, this, &AbstractCardDragItem::deleteLater);
|
||||||
}
|
}
|
||||||
|
|
@ -47,7 +49,8 @@ AbstractCardDragItem::AbstractCardDragItem(AbstractCardItem *_item,
|
||||||
QPainterPath AbstractCardDragItem::shape() const
|
QPainterPath AbstractCardDragItem::shape() const
|
||||||
{
|
{
|
||||||
QPainterPath shape;
|
QPainterPath shape;
|
||||||
qreal cardCornerRadius = SettingsCache::instance().getRoundCardCorners() ? 0.05 * CardDimensions::WIDTH_F : 0.0;
|
qreal cardCornerRadius =
|
||||||
|
SettingsCache::instance().cardsDisplay().getRoundCardCorners() ? 0.05 * CardDimensions::WIDTH_F : 0.0;
|
||||||
shape.addRoundedRect(boundingRect(), cardCornerRadius, cardCornerRadius);
|
shape.addRoundedRect(boundingRect(), cardCornerRadius, cardCornerRadius);
|
||||||
return shape;
|
return shape;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,9 @@
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <libcockatrice/card/database/card_database.h>
|
#include <libcockatrice/card/database/card_database.h>
|
||||||
#include <libcockatrice/card/database/card_database_manager.h>
|
#include <libcockatrice/card/database/card_database_manager.h>
|
||||||
|
#include <libcockatrice/settings/appearance_settings.h>
|
||||||
|
#include <libcockatrice/settings/cards_display_settings.h>
|
||||||
|
#include <libcockatrice/settings/debug_settings.h>
|
||||||
|
|
||||||
AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef, PlayerLogic *_owner, int _id)
|
AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef, PlayerLogic *_owner, int _id)
|
||||||
: ArrowTarget(_owner, parent), id(_id), cardRef(cardRef), tapped(false), facedown(false), tapAngle(0),
|
: ArrowTarget(_owner, parent), id(_id), cardRef(cardRef), tapped(false), facedown(false), tapAngle(0),
|
||||||
|
|
@ -21,15 +24,17 @@ AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef
|
||||||
setFlag(ItemIsSelectable);
|
setFlag(ItemIsSelectable);
|
||||||
setCacheMode(DeviceCoordinateCache);
|
setCacheMode(DeviceCoordinateCache);
|
||||||
|
|
||||||
connect(&SettingsCache::instance(), &SettingsCache::displayCardNamesChanged, this, [this] { update(); });
|
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::displayCardNamesChanged, this,
|
||||||
|
[this] { update(); });
|
||||||
refreshCardInfo();
|
refreshCardInfo();
|
||||||
|
|
||||||
connect(&SettingsCache::instance(), &SettingsCache::roundCardCornersChanged, this, [this](bool _roundCardCorners) {
|
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::roundCardCornersChanged, this,
|
||||||
Q_UNUSED(_roundCardCorners);
|
[this](bool _roundCardCorners) {
|
||||||
|
Q_UNUSED(_roundCardCorners);
|
||||||
|
|
||||||
prepareGeometryChange();
|
prepareGeometryChange();
|
||||||
update();
|
update();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
AbstractCardItem::~AbstractCardItem()
|
AbstractCardItem::~AbstractCardItem()
|
||||||
|
|
@ -45,7 +50,8 @@ QRectF AbstractCardItem::boundingRect() const
|
||||||
QPainterPath AbstractCardItem::shape() const
|
QPainterPath AbstractCardItem::shape() const
|
||||||
{
|
{
|
||||||
QPainterPath shape;
|
QPainterPath shape;
|
||||||
qreal cardCornerRadius = SettingsCache::instance().getRoundCardCorners() ? 0.05 * CardDimensions::WIDTH_F : 0.0;
|
qreal cardCornerRadius =
|
||||||
|
SettingsCache::instance().cardsDisplay().getRoundCardCorners() ? 0.05 * CardDimensions::WIDTH_F : 0.0;
|
||||||
shape.addRoundedRect(boundingRect(), cardCornerRadius, cardCornerRadius);
|
shape.addRoundedRect(boundingRect(), cardCornerRadius, cardCornerRadius);
|
||||||
return shape;
|
return shape;
|
||||||
}
|
}
|
||||||
|
|
@ -101,7 +107,7 @@ QSizeF AbstractCardItem::getTranslatedSize(QPainter *painter) const
|
||||||
|
|
||||||
void AbstractCardItem::transformPainter(QPainter *painter, const QSizeF &translatedSize, int angle)
|
void AbstractCardItem::transformPainter(QPainter *painter, const QSizeF &translatedSize, int angle)
|
||||||
{
|
{
|
||||||
const int MAX_FONT_SIZE = SettingsCache::instance().getMaxFontSize();
|
const int MAX_FONT_SIZE = SettingsCache::instance().appearance().getMaxFontSize();
|
||||||
const int fontSize = std::max(9, MAX_FONT_SIZE);
|
const int fontSize = std::max(9, MAX_FONT_SIZE);
|
||||||
|
|
||||||
QRectF totalBoundingRect = painter->combinedTransform().mapRect(boundingRect());
|
QRectF totalBoundingRect = painter->combinedTransform().mapRect(boundingRect());
|
||||||
|
|
@ -151,7 +157,7 @@ void AbstractCardItem::paintPicture(QPainter *painter, const QSizeF &translatedS
|
||||||
painter->drawPath(shape());
|
painter->drawPath(shape());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (translatedPixmap.isNull() || SettingsCache::instance().getDisplayCardNames() || facedown) {
|
if (translatedPixmap.isNull() || SettingsCache::instance().cardsDisplay().getDisplayCardNames() || facedown) {
|
||||||
painter->save();
|
painter->save();
|
||||||
transformPainter(painter, translatedSize, angle);
|
transformPainter(painter, translatedSize, angle);
|
||||||
painter->setPen(Qt::white);
|
painter->setPen(Qt::white);
|
||||||
|
|
@ -234,7 +240,7 @@ void AbstractCardItem::setHovered(bool _hovered)
|
||||||
|
|
||||||
isHovered = _hovered;
|
isHovered = _hovered;
|
||||||
setZValue(_hovered ? ZValues::HOVERED_CARD : realZValue);
|
setZValue(_hovered ? ZValues::HOVERED_CARD : realZValue);
|
||||||
setScale(_hovered && SettingsCache::instance().getScaleCards() ? 1.1 : 1);
|
setScale(_hovered && SettingsCache::instance().cardsDisplay().getScaleCards() ? 1.1 : 1);
|
||||||
setTransformOriginPoint(_hovered ? CardDimensions::WIDTH_HALF_F : 0, _hovered ? CardDimensions::HEIGHT_HALF_F : 0);
|
setTransformOriginPoint(_hovered ? CardDimensions::WIDTH_HALF_F : 0, _hovered ? CardDimensions::HEIGHT_HALF_F : 0);
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
@ -287,7 +293,7 @@ void AbstractCardItem::setTapped(bool _tapped, bool canAnimate)
|
||||||
}
|
}
|
||||||
|
|
||||||
tapped = _tapped;
|
tapped = _tapped;
|
||||||
if (SettingsCache::instance().getTapAnimation() && canAnimate) {
|
if (SettingsCache::instance().cardsDisplay().getTapAnimation() && canAnimate) {
|
||||||
static_cast<GameScene *>(scene())->registerAnimationItem(this);
|
static_cast<GameScene *>(scene())->registerAnimationItem(this);
|
||||||
} else {
|
} else {
|
||||||
tapAngle = tapped ? 90 : 0;
|
tapAngle = tapped ? 90 : 0;
|
||||||
|
|
@ -299,6 +305,11 @@ void AbstractCardItem::setTapped(bool _tapped, bool canAnimate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool AbstractCardItem::animationEvent()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
void AbstractCardItem::setFaceDown(bool _facedown)
|
void AbstractCardItem::setFaceDown(bool _facedown)
|
||||||
{
|
{
|
||||||
facedown = _facedown;
|
facedown = _facedown;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
#ifndef ABSTRACTCARDITEM_H
|
#ifndef ABSTRACTCARDITEM_H
|
||||||
#define ABSTRACTCARDITEM_H
|
#define ABSTRACTCARDITEM_H
|
||||||
|
|
||||||
|
#include "../animated_item.h"
|
||||||
#include "../card_dimensions.h"
|
#include "../card_dimensions.h"
|
||||||
#include "arrow_target.h"
|
#include "arrow_target.h"
|
||||||
#include "graphics_item_type.h"
|
#include "graphics_item_type.h"
|
||||||
|
|
@ -16,7 +17,7 @@
|
||||||
|
|
||||||
class PlayerLogic;
|
class PlayerLogic;
|
||||||
|
|
||||||
class AbstractCardItem : public ArrowTarget
|
class AbstractCardItem : public ArrowTarget, public IAnimatedItem
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
protected:
|
protected:
|
||||||
|
|
@ -126,6 +127,9 @@ public:
|
||||||
emit deleteCardInfoPopup(cardRef.name);
|
emit deleteCardInfoPopup(cardRef.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @brief Default: no per-tick animation. Subclasses override to animate. */
|
||||||
|
bool animationEvent() override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void transformPainter(QPainter *painter, const QSizeF &translatedSize, int angle);
|
void transformPainter(QPainter *painter, const QSizeF &translatedSize, int angle);
|
||||||
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
|
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#include "abstract_counter.h"
|
#include "abstract_counter.h"
|
||||||
|
|
||||||
#include "../../client/settings/cache_settings.h"
|
#include "../../client/settings/cache_settings.h"
|
||||||
|
#include "../../client/settings/shortcuts_settings.h"
|
||||||
#include "../../game/player/player_actions.h"
|
#include "../../game/player/player_actions.h"
|
||||||
#include "../../game/player/player_logic.h"
|
#include "../../game/player/player_logic.h"
|
||||||
#include "../../game_graphics/board/translate_counter_name.h"
|
#include "../../game_graphics/board/translate_counter_name.h"
|
||||||
|
|
@ -28,8 +29,9 @@ AbstractCounter::AbstractCounter(CounterState *state,
|
||||||
{
|
{
|
||||||
setAcceptHoverEvents(true);
|
setAcceptHoverEvents(true);
|
||||||
|
|
||||||
connect(state, &CounterState::valueChanged, this, [this](int, int newValue) {
|
connect(state, &CounterState::valueChanged, this, [this](int oldValue, int newValue) {
|
||||||
value = newValue;
|
value = newValue;
|
||||||
|
onValueChanged(oldValue, newValue);
|
||||||
update();
|
update();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -227,3 +229,9 @@ void AbstractCounterDialog::changeValue(int diff)
|
||||||
curValue += diff;
|
curValue += diff;
|
||||||
setTextValue(QString::number(curValue));
|
setTextValue(QString::number(curValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AbstractCounter::onValueChanged(int /*oldValue*/, int /*newValue*/)
|
||||||
|
{
|
||||||
|
// Default: no feedback. Subclasses such as PlayerCounter override this to
|
||||||
|
// flash the counter on meaningful changes (life gain/loss).
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,13 @@ protected:
|
||||||
bool hovered = false;
|
bool hovered = false;
|
||||||
bool useNameForShortcut;
|
bool useNameForShortcut;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Hook for subclasses that need per-value-change feedback (e.g. life-total flash).
|
||||||
|
*
|
||||||
|
* Called whenever the counter's value changes, before the item repaints.
|
||||||
|
*/
|
||||||
|
virtual void onValueChanged(int oldValue, int newValue);
|
||||||
|
|
||||||
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
|
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
|
||||||
void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override;
|
void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override;
|
||||||
void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override;
|
void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override;
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,14 @@
|
||||||
#include "../../client/settings/cache_settings.h"
|
#include "../../client/settings/cache_settings.h"
|
||||||
#include "../../game/player/player_actions.h"
|
#include "../../game/player/player_actions.h"
|
||||||
#include "../../game/player/player_logic.h"
|
#include "../../game/player/player_logic.h"
|
||||||
|
#include "../game_scene.h"
|
||||||
#include "../player/player_target.h"
|
#include "../player/player_target.h"
|
||||||
#include "../z_values.h"
|
#include "../z_values.h"
|
||||||
#include "../zones/card_zone.h"
|
#include "../zones/card_zone.h"
|
||||||
#include "card_item.h"
|
#include "card_item.h"
|
||||||
|
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
#include <QElapsedTimer>
|
||||||
#include <QGraphicsScene>
|
#include <QGraphicsScene>
|
||||||
#include <QGraphicsSceneMouseEvent>
|
#include <QGraphicsSceneMouseEvent>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
|
|
@ -18,9 +20,27 @@
|
||||||
#include <libcockatrice/protocol/pb/command_attach_card.pb.h>
|
#include <libcockatrice/protocol/pb/command_attach_card.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_create_arrow.pb.h>
|
#include <libcockatrice/protocol/pb/command_create_arrow.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_delete_arrow.pb.h>
|
#include <libcockatrice/protocol/pb/command_delete_arrow.pb.h>
|
||||||
|
#include <libcockatrice/settings/cards_display_settings.h>
|
||||||
|
#include <libcockatrice/settings/interface_settings.h>
|
||||||
#include <libcockatrice/utility/color.h>
|
#include <libcockatrice/utility/color.h>
|
||||||
#include <libcockatrice/utility/zone_names.h>
|
#include <libcockatrice/utility/zone_names.h>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr qreal kMinStrokeDurationMs = 200.0;
|
||||||
|
constexpr qreal kMaxStrokeDurationMs = 450.0;
|
||||||
|
constexpr qreal kMsPerPixel = 0.8;
|
||||||
|
constexpr qreal kGlowFadeDurationMs = 120.0;
|
||||||
|
constexpr qreal kSheenHalfWidth = 14.0;
|
||||||
|
|
||||||
|
/// @brief Ease-out cubic, for a natural "slow in / slow out" reveal.
|
||||||
|
qreal easeOutCubic(qreal t)
|
||||||
|
{
|
||||||
|
const qreal inverse = 1.0 - t;
|
||||||
|
return 1.0 - inverse * inverse * inverse;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
ArrowItem::ArrowItem(QSharedPointer<const ArrowData> _data, ArrowTarget *_startItem, ArrowTarget *_targetItem)
|
ArrowItem::ArrowItem(QSharedPointer<const ArrowData> _data, ArrowTarget *_startItem, ArrowTarget *_targetItem)
|
||||||
: data(std::move(_data)), startItem(_startItem), targetItem(_targetItem)
|
: data(std::move(_data)), startItem(_startItem), targetItem(_targetItem)
|
||||||
{
|
{
|
||||||
|
|
@ -46,8 +66,23 @@ ArrowItem::ArrowItem(QSharedPointer<const ArrowData> _data, ArrowTarget *_startI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ArrowItem::~ArrowItem()
|
||||||
|
{
|
||||||
|
if (auto *scene = qobject_cast<GameScene *>(this->scene())) {
|
||||||
|
scene->unregisterAnimationItem(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void ArrowItem::onTargetDestroyed()
|
void ArrowItem::onTargetDestroyed()
|
||||||
{
|
{
|
||||||
|
if (data->id == -1) {
|
||||||
|
// Drag and attach arrows are never inserted into the arrow registry and
|
||||||
|
// have no server-side counterpart, so no deletion event can clean them
|
||||||
|
// up. Delete them locally when either endpoint is destroyed.
|
||||||
|
delArrow();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
emit requestDeletion(data->creatorId, data->id);
|
emit requestDeletion(data->creatorId, data->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,16 +125,21 @@ void ArrowItem::updatePath(const QPointF &endPoint)
|
||||||
prepareGeometryChange();
|
prepareGeometryChange();
|
||||||
if (lineLength < 30) {
|
if (lineLength < 30) {
|
||||||
path = QPainterPath();
|
path = QPainterPath();
|
||||||
|
bodyPath = QPainterPath();
|
||||||
|
headPath = QPainterPath();
|
||||||
|
shaftOutlinePath = QPainterPath();
|
||||||
|
centerLine = QPainterPath();
|
||||||
|
headBaseFraction = 1.0;
|
||||||
} else {
|
} else {
|
||||||
QPointF c(lineLength / 2, qTan(phi * M_PI / 180) * lineLength);
|
QPointF c(lineLength / 2, qTan(phi * M_PI / 180) * lineLength);
|
||||||
|
|
||||||
QPainterPath centerLine;
|
centerLine = QPainterPath();
|
||||||
centerLine.moveTo(0, 0);
|
centerLine.moveTo(0, 0);
|
||||||
centerLine.quadTo(c, QPointF(lineLength, 0));
|
centerLine.quadTo(c, QPointF(lineLength, 0));
|
||||||
|
|
||||||
double percentage = 1 - headLength / lineLength;
|
headBaseFraction = 1 - headLength / lineLength;
|
||||||
QPointF arrowBodyEndPoint = centerLine.pointAtPercent(percentage);
|
QPointF arrowBodyEndPoint = centerLine.pointAtPercent(headBaseFraction);
|
||||||
QLineF testLine(arrowBodyEndPoint, centerLine.pointAtPercent(percentage + 0.001));
|
QLineF testLine(arrowBodyEndPoint, centerLine.pointAtPercent(headBaseFraction + 0.001));
|
||||||
qreal alpha = testLine.angle() - 90;
|
qreal alpha = testLine.angle() - 90;
|
||||||
QPointF endPoint1 =
|
QPointF endPoint1 =
|
||||||
arrowBodyEndPoint + arrowWidth / 2 * QPointF(qCos(alpha * M_PI / 180), -qSin(alpha * M_PI / 180));
|
arrowBodyEndPoint + arrowWidth / 2 * QPointF(qCos(alpha * M_PI / 180), -qSin(alpha * M_PI / 180));
|
||||||
|
|
@ -110,20 +150,89 @@ void ArrowItem::updatePath(const QPointF &endPoint)
|
||||||
QPointF point2 =
|
QPointF point2 =
|
||||||
endPoint2 + (headWidth - arrowWidth) / 2 * QPointF(-qCos(alpha * M_PI / 180), qSin(alpha * M_PI / 180));
|
endPoint2 + (headWidth - arrowWidth) / 2 * QPointF(-qCos(alpha * M_PI / 180), qSin(alpha * M_PI / 180));
|
||||||
|
|
||||||
path = QPainterPath(-arrowWidth / 2 * QPointF(qCos((phi - 90) * M_PI / 180), qSin((phi - 90) * M_PI / 180)));
|
QPointF start1 = -arrowWidth / 2 * QPointF(qCos((phi - 90) * M_PI / 180), qSin((phi - 90) * M_PI / 180));
|
||||||
|
QPointF start2 = arrowWidth / 2 * QPointF(qCos((phi - 90) * M_PI / 180), qSin((phi - 90) * M_PI / 180));
|
||||||
|
|
||||||
|
path = QPainterPath(start1);
|
||||||
path.quadTo(c, endPoint1);
|
path.quadTo(c, endPoint1);
|
||||||
path.lineTo(point1);
|
path.lineTo(point1);
|
||||||
path.lineTo(QPointF(lineLength, 0));
|
path.lineTo(QPointF(lineLength, 0));
|
||||||
path.lineTo(point2);
|
path.lineTo(point2);
|
||||||
path.lineTo(endPoint2);
|
path.lineTo(endPoint2);
|
||||||
path.quadTo(c, arrowWidth / 2 * QPointF(qCos((phi - 90) * M_PI / 180), qSin((phi - 90) * M_PI / 180)));
|
path.quadTo(c, start2);
|
||||||
path.lineTo(-arrowWidth / 2 * QPointF(qCos((phi - 90) * M_PI / 180), qSin((phi - 90) * M_PI / 180)));
|
path.lineTo(start1);
|
||||||
|
|
||||||
|
bodyPath = QPainterPath(start1);
|
||||||
|
bodyPath.quadTo(c, endPoint1);
|
||||||
|
bodyPath.lineTo(endPoint2);
|
||||||
|
bodyPath.quadTo(c, start2);
|
||||||
|
bodyPath.lineTo(start1);
|
||||||
|
|
||||||
|
headPath = QPainterPath(endPoint1);
|
||||||
|
headPath.lineTo(point1);
|
||||||
|
headPath.lineTo(QPointF(lineLength, 0));
|
||||||
|
headPath.lineTo(point2);
|
||||||
|
headPath.lineTo(endPoint2);
|
||||||
|
|
||||||
|
shaftOutlinePath = QPainterPath(start1);
|
||||||
|
shaftOutlinePath.quadTo(c, endPoint1);
|
||||||
|
shaftOutlinePath.moveTo(endPoint2);
|
||||||
|
shaftOutlinePath.quadTo(c, start2);
|
||||||
|
shaftOutlinePath.lineTo(start1);
|
||||||
}
|
}
|
||||||
|
|
||||||
setPos(startPoint);
|
setPos(startPoint);
|
||||||
setTransform(QTransform().rotate(-line.angle()));
|
setTransform(QTransform().rotate(-line.angle()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ArrowItem::startDrawAnimation()
|
||||||
|
{
|
||||||
|
if (!SettingsCache::instance().cardsDisplay().getArrowDrawAnimation() || centerLine.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
strokeDurationMs = qBound(kMinStrokeDurationMs, centerLine.length() * kMsPerPixel, kMaxStrokeDurationMs);
|
||||||
|
glowFadeDurationMs = kGlowFadeDurationMs;
|
||||||
|
// The clock is started on the first animationEvent() tick so that t=0
|
||||||
|
// corresponds to the first rendered frame. Starting it here would count
|
||||||
|
// the time spent before the item's first paint (event-loop delays, bursts
|
||||||
|
// of arrows created together), making the arrow appear already partway
|
||||||
|
// drawn when it first shows up.
|
||||||
|
animationStarted = false;
|
||||||
|
drawProgress = 0.0;
|
||||||
|
glowAlpha = 1.0;
|
||||||
|
update();
|
||||||
|
if (auto *scene = qobject_cast<GameScene *>(this->scene())) {
|
||||||
|
scene->registerAnimationItem(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ArrowItem::animationEvent()
|
||||||
|
{
|
||||||
|
if (!animationStarted) {
|
||||||
|
animationClock.start();
|
||||||
|
animationStarted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const qint64 elapsed = animationClock.elapsed();
|
||||||
|
if (elapsed >= strokeDurationMs + glowFadeDurationMs) {
|
||||||
|
drawProgress = 1.0;
|
||||||
|
glowAlpha = 0.0;
|
||||||
|
update();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elapsed < strokeDurationMs) {
|
||||||
|
drawProgress = easeOutCubic(qBound<qreal>(0.0, elapsed / strokeDurationMs, 1.0));
|
||||||
|
glowAlpha = 1.0;
|
||||||
|
} else {
|
||||||
|
drawProgress = 1.0;
|
||||||
|
glowAlpha = 1.0 - (elapsed - strokeDurationMs) / glowFadeDurationMs;
|
||||||
|
}
|
||||||
|
update();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void ArrowItem::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
|
void ArrowItem::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
|
||||||
{
|
{
|
||||||
QColor paintColor(data->color);
|
QColor paintColor(data->color);
|
||||||
|
|
@ -132,8 +241,66 @@ void ArrowItem::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*opti
|
||||||
} else {
|
} else {
|
||||||
paintColor.setAlpha(150);
|
paintColor.setAlpha(150);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
painter->save();
|
||||||
|
const QPen outlinePen = painter->pen();
|
||||||
painter->setBrush(paintColor);
|
painter->setBrush(paintColor);
|
||||||
painter->drawPath(path);
|
|
||||||
|
const auto drawShaft = [this, painter, &outlinePen, paintColor]() {
|
||||||
|
painter->setPen(Qt::NoPen);
|
||||||
|
painter->drawPath(bodyPath);
|
||||||
|
painter->setPen(outlinePen);
|
||||||
|
painter->setBrush(Qt::NoBrush);
|
||||||
|
painter->drawPath(shaftOutlinePath);
|
||||||
|
painter->setBrush(paintColor);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (drawProgress >= 1.0 || path.isEmpty()) {
|
||||||
|
painter->drawPath(path);
|
||||||
|
} else if (drawProgress < headBaseFraction) {
|
||||||
|
// The reveal edge and the sheen share the same arc-length parameterization,
|
||||||
|
// so the stroke stays exactly in sync with the trailing sheen.
|
||||||
|
const qreal revealX = centerLine.pointAtPercent(drawProgress).x();
|
||||||
|
QPainterPath clip;
|
||||||
|
clip.addRect(QRectF(-glowExtent, path.boundingRect().top() - glowExtent, revealX + glowExtent,
|
||||||
|
path.boundingRect().height() + 2 * glowExtent));
|
||||||
|
painter->setClipPath(clip);
|
||||||
|
drawShaft();
|
||||||
|
} else {
|
||||||
|
// Once the reveal reaches the head base, pop the whole head in with a fade
|
||||||
|
// instead of slicing the triangle into a growing stub.
|
||||||
|
drawShaft();
|
||||||
|
const qreal headFadeIn = (drawProgress - headBaseFraction) / (1.0 - headBaseFraction);
|
||||||
|
painter->setOpacity(headFadeIn);
|
||||||
|
painter->setPen(Qt::NoPen);
|
||||||
|
painter->drawPath(headPath);
|
||||||
|
painter->setPen(outlinePen);
|
||||||
|
painter->setBrush(Qt::NoBrush);
|
||||||
|
painter->drawPath(headPath);
|
||||||
|
painter->setOpacity(1.0);
|
||||||
|
painter->setBrush(paintColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (glowAlpha > 0.0 && !centerLine.isEmpty()) {
|
||||||
|
// Sweep a bright band across the arrow. Clipping to the
|
||||||
|
// silhouette keeps it flat against the shaft so it reads as a light reflection.
|
||||||
|
const qreal anticipation = qMin<qreal>(1.0, drawProgress / 0.08);
|
||||||
|
const QPointF sweep = centerLine.pointAtPercent(qMin<qreal>(drawProgress, 1.0));
|
||||||
|
QLinearGradient sheen(sweep.x() - kSheenHalfWidth, 0.0, sweep.x() + kSheenHalfWidth, 0.0);
|
||||||
|
sheen.setColorAt(0.0, QColor(paintColor.red(), paintColor.green(), paintColor.blue(), 0));
|
||||||
|
sheen.setColorAt(0.5, QColor(255, 255, 255, 200));
|
||||||
|
sheen.setColorAt(1.0, QColor(paintColor.red(), paintColor.green(), paintColor.blue(), 0));
|
||||||
|
painter->save();
|
||||||
|
painter->setPen(Qt::NoPen);
|
||||||
|
painter->setClipPath(path);
|
||||||
|
painter->setBrush(sheen);
|
||||||
|
painter->setOpacity(glowAlpha * anticipation);
|
||||||
|
painter->drawRect(QRectF(sweep.x() - kSheenHalfWidth - glowExtent, path.boundingRect().top() - glowExtent,
|
||||||
|
(kSheenHalfWidth + glowExtent) * 2.0,
|
||||||
|
path.boundingRect().height() + glowExtent * 2.0));
|
||||||
|
painter->restore();
|
||||||
|
}
|
||||||
|
painter->restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ArrowItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
|
void ArrowItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
|
||||||
|
|
@ -225,6 +392,12 @@ void ArrowDragItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
|
||||||
void ArrowDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
void ArrowDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
||||||
{
|
{
|
||||||
if (!startItem) {
|
if (!startItem) {
|
||||||
|
// The source card was destroyed while the arrow was being drawn.
|
||||||
|
// Clean up the arrow and its children instead of leaking them.
|
||||||
|
delArrow();
|
||||||
|
for (auto *child : childArrows) {
|
||||||
|
child->mouseReleaseEvent(event);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -261,7 +434,7 @@ void ArrowDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
||||||
if (startZone->getName() == ZoneNames::HAND) {
|
if (startZone->getName() == ZoneNames::HAND) {
|
||||||
startCard->playCard(false);
|
startCard->playCard(false);
|
||||||
CardInfoPtr ci = startCard->getCard().getCardPtr();
|
CardInfoPtr ci = startCard->getCard().getCardPtr();
|
||||||
bool playToStack = SettingsCache::instance().getPlayToStack();
|
bool playToStack = SettingsCache::instance().userInterface().getPlayToStack();
|
||||||
if (ci && ((!playToStack && ci->getUiAttributes().tableRow == 3) ||
|
if (ci && ((!playToStack && ci->getUiAttributes().tableRow == 3) ||
|
||||||
(playToStack && ci->getUiAttributes().tableRow != 0 &&
|
(playToStack && ci->getUiAttributes().tableRow != 0 &&
|
||||||
startCard->getZone()->getName() != ZoneNames::STACK))) {
|
startCard->getZone()->getName() != ZoneNames::STACK))) {
|
||||||
|
|
@ -348,6 +521,12 @@ void ArrowAttachItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
|
||||||
void ArrowAttachItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
void ArrowAttachItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
||||||
{
|
{
|
||||||
if (!startItem) {
|
if (!startItem) {
|
||||||
|
// The source card was destroyed while the arrow was being drawn.
|
||||||
|
// Clean up the arrow and its children instead of leaking them.
|
||||||
|
delArrow();
|
||||||
|
for (auto *child : childArrows) {
|
||||||
|
child->mouseReleaseEvent(event);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,13 @@
|
||||||
#define ARROWITEM_H
|
#define ARROWITEM_H
|
||||||
|
|
||||||
#include "../../game/board/arrow_data.h"
|
#include "../../game/board/arrow_data.h"
|
||||||
|
#include "../animated_item.h"
|
||||||
#include "arrow_target.h"
|
#include "arrow_target.h"
|
||||||
|
#include "graphics_item_type.h"
|
||||||
|
|
||||||
|
#include <QElapsedTimer>
|
||||||
#include <QGraphicsItem>
|
#include <QGraphicsItem>
|
||||||
|
#include <QPainterPath>
|
||||||
#include <QPointer>
|
#include <QPointer>
|
||||||
#include <QSharedPointer>
|
#include <QSharedPointer>
|
||||||
|
|
||||||
|
|
@ -12,7 +16,7 @@ class CardItem;
|
||||||
class QGraphicsSceneMouseEvent;
|
class QGraphicsSceneMouseEvent;
|
||||||
class PlayerLogic;
|
class PlayerLogic;
|
||||||
|
|
||||||
class ArrowItem : public QObject, public QGraphicsItem
|
class ArrowItem : public QObject, public QGraphicsItem, public IAnimatedItem
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
Q_INTERFACES(QGraphicsItem)
|
Q_INTERFACES(QGraphicsItem)
|
||||||
|
|
@ -21,6 +25,19 @@ signals:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QPainterPath path;
|
QPainterPath path;
|
||||||
|
QPainterPath bodyPath;
|
||||||
|
QPainterPath headPath;
|
||||||
|
QPainterPath shaftOutlinePath;
|
||||||
|
QPainterPath centerLine;
|
||||||
|
qreal headBaseFraction = 1.0;
|
||||||
|
QElapsedTimer animationClock;
|
||||||
|
qreal strokeDurationMs = 0;
|
||||||
|
qreal glowFadeDurationMs = 0;
|
||||||
|
qreal drawProgress = 1.0;
|
||||||
|
qreal glowAlpha = 0.0;
|
||||||
|
bool animationStarted = false;
|
||||||
|
|
||||||
|
static constexpr qreal glowExtent = 12.0;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
QSharedPointer<const ArrowData> data;
|
QSharedPointer<const ArrowData> data;
|
||||||
|
|
@ -32,17 +49,28 @@ protected:
|
||||||
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
|
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
enum
|
||||||
|
{
|
||||||
|
Type = typeArrow
|
||||||
|
};
|
||||||
|
[[nodiscard]] int type() const override
|
||||||
|
{
|
||||||
|
return Type;
|
||||||
|
}
|
||||||
ArrowItem(QSharedPointer<const ArrowData> _data, ArrowTarget *_startItem, ArrowTarget *_targetItem);
|
ArrowItem(QSharedPointer<const ArrowData> _data, ArrowTarget *_startItem, ArrowTarget *_targetItem);
|
||||||
|
~ArrowItem() override;
|
||||||
|
|
||||||
void onTargetDestroyed();
|
void onTargetDestroyed();
|
||||||
void delArrow();
|
void delArrow();
|
||||||
void updatePath();
|
void updatePath();
|
||||||
void updatePath(const QPointF &endPoint);
|
void updatePath(const QPointF &endPoint);
|
||||||
|
void startDrawAnimation();
|
||||||
|
bool animationEvent() override;
|
||||||
|
|
||||||
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
|
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
|
||||||
[[nodiscard]] QRectF boundingRect() const override
|
[[nodiscard]] QRectF boundingRect() const override
|
||||||
{
|
{
|
||||||
return path.boundingRect();
|
return path.boundingRect().adjusted(-glowExtent, -glowExtent, glowExtent, glowExtent);
|
||||||
}
|
}
|
||||||
[[nodiscard]] QPainterPath shape() const override
|
[[nodiscard]] QPainterPath shape() const override
|
||||||
{
|
{
|
||||||
|
|
@ -106,4 +134,4 @@ protected:
|
||||||
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;
|
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#include "card_item.h"
|
#include "card_item.h"
|
||||||
|
|
||||||
#include "../../client/settings/cache_settings.h"
|
#include "../../client/settings/cache_settings.h"
|
||||||
|
#include "../../client/settings/card_counter_settings.h"
|
||||||
#include "../../game/phase.h"
|
#include "../../game/phase.h"
|
||||||
#include "../../game/player/player_actions.h"
|
#include "../../game/player/player_actions.h"
|
||||||
#include "../../game/player/player_logic.h"
|
#include "../../game/player/player_logic.h"
|
||||||
|
|
@ -19,6 +20,7 @@
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <libcockatrice/card/card_info.h>
|
#include <libcockatrice/card/card_info.h>
|
||||||
#include <libcockatrice/protocol/pb/serverinfo_card.pb.h>
|
#include <libcockatrice/protocol/pb/serverinfo_card.pb.h>
|
||||||
|
#include <libcockatrice/settings/interface_settings.h>
|
||||||
|
|
||||||
CardItem::CardItem(PlayerLogic *_owner,
|
CardItem::CardItem(PlayerLogic *_owner,
|
||||||
QGraphicsItem *parent,
|
QGraphicsItem *parent,
|
||||||
|
|
@ -279,7 +281,7 @@ void CardItem::drawArrow(const QColor &arrowColor)
|
||||||
auto *game = owner->getGame();
|
auto *game = owner->getGame();
|
||||||
PlayerLogic *arrowOwner = game->getPlayerManager()->getActiveLocalPlayer(game->getGameState()->getActivePlayer());
|
PlayerLogic *arrowOwner = game->getPlayerManager()->getActiveLocalPlayer(game->getGameState()->getActivePlayer());
|
||||||
int phase = 0; // 0 means to not set the phase
|
int phase = 0; // 0 means to not set the phase
|
||||||
if (SettingsCache::instance().getDoNotDeleteArrowsInSubPhases()) {
|
if (SettingsCache::instance().userInterface().getDoNotDeleteArrowsInSubPhases()) {
|
||||||
int currentPhase = game->getGameState()->getCurrentPhase();
|
int currentPhase = game->getGameState()->getCurrentPhase();
|
||||||
phase = Phases::getLastSubphase(currentPhase) + 1;
|
phase = Phases::getLastSubphase(currentPhase) + 1;
|
||||||
}
|
}
|
||||||
|
|
@ -398,7 +400,7 @@ void CardItem::playCard(bool faceDown)
|
||||||
if (tz) {
|
if (tz) {
|
||||||
emit tz->toggleTapped();
|
emit tz->toggleTapped();
|
||||||
} else {
|
} else {
|
||||||
if (SettingsCache::instance().getClickPlaysAllSelected()) {
|
if (SettingsCache::instance().userInterface().getClickPlaysAllSelected()) {
|
||||||
if (faceDown) {
|
if (faceDown) {
|
||||||
emit playSelectedFaceDown(this);
|
emit playSelectedFaceDown(this);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -462,7 +464,7 @@ static bool isUnwritableRevealZone(CardZoneLogic *zone)
|
||||||
void CardItem::handleClickedToPlay(bool shiftHeld)
|
void CardItem::handleClickedToPlay(bool shiftHeld)
|
||||||
{
|
{
|
||||||
if (isUnwritableRevealZone(state->getZone())) {
|
if (isUnwritableRevealZone(state->getZone())) {
|
||||||
if (SettingsCache::instance().getClickPlaysAllSelected()) {
|
if (SettingsCache::instance().userInterface().getClickPlaysAllSelected()) {
|
||||||
emit hideSelected(this);
|
emit hideSelected(this);
|
||||||
} else {
|
} else {
|
||||||
state->getZone()->removeCard(this);
|
state->getZone()->removeCard(this);
|
||||||
|
|
@ -479,7 +481,7 @@ void CardItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ((event->modifiers() != Qt::AltModifier) && (event->button() == Qt::LeftButton) &&
|
if ((event->modifiers() != Qt::AltModifier) && (event->button() == Qt::LeftButton) &&
|
||||||
(!SettingsCache::instance().getDoubleClickToPlay())) {
|
(!SettingsCache::instance().userInterface().getDoubleClickToPlay())) {
|
||||||
handleClickedToPlay(event->modifiers().testFlag(Qt::ShiftModifier));
|
handleClickedToPlay(event->modifiers().testFlag(Qt::ShiftModifier));
|
||||||
}
|
}
|
||||||
if (owner != nullptr) {
|
if (owner != nullptr) {
|
||||||
|
|
@ -491,7 +493,7 @@ void CardItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
||||||
void CardItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
|
void CardItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
|
||||||
{
|
{
|
||||||
if ((event->modifiers() != Qt::AltModifier) && (event->buttons() == Qt::LeftButton) &&
|
if ((event->modifiers() != Qt::AltModifier) && (event->buttons() == Qt::LeftButton) &&
|
||||||
(SettingsCache::instance().getDoubleClickToPlay())) {
|
(SettingsCache::instance().userInterface().getDoubleClickToPlay())) {
|
||||||
handleClickedToPlay(event->modifiers().testFlag(Qt::ShiftModifier));
|
handleClickedToPlay(event->modifiers().testFlag(Qt::ShiftModifier));
|
||||||
}
|
}
|
||||||
event->accept();
|
event->accept();
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@
|
||||||
#include "abstract_card_item.h"
|
#include "abstract_card_item.h"
|
||||||
|
|
||||||
#include <libcockatrice/network/server/remote/game/server_card.h>
|
#include <libcockatrice/network/server/remote/game/server_card.h>
|
||||||
#include <libcockatrice/utility/trice_limits.h>
|
|
||||||
|
|
||||||
class CardDatabase;
|
class CardDatabase;
|
||||||
class CardDragItem;
|
class CardDragItem;
|
||||||
|
|
@ -138,7 +137,7 @@ public:
|
||||||
void resetState(bool keepAnnotations = false);
|
void resetState(bool keepAnnotations = false);
|
||||||
void processCardInfo(const ServerInfo_Card &_info);
|
void processCardInfo(const ServerInfo_Card &_info);
|
||||||
|
|
||||||
bool animationEvent();
|
bool animationEvent() override;
|
||||||
CardDragItem *createDragItem(int _id, const QPointF &_pos, const QPointF &_scenePos, bool forceFaceDown);
|
CardDragItem *createDragItem(int _id, const QPointF &_pos, const QPointF &_scenePos, bool forceFaceDown);
|
||||||
void deleteDragItem();
|
void deleteDragItem();
|
||||||
void drawArrow(const QColor &arrowColor);
|
void drawArrow(const QColor &arrowColor);
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,8 @@ enum GraphicsItemType
|
||||||
typeZone = QGraphicsItem::UserType + 3,
|
typeZone = QGraphicsItem::UserType + 3,
|
||||||
typePlayerTarget = QGraphicsItem::UserType + 4,
|
typePlayerTarget = QGraphicsItem::UserType + 4,
|
||||||
typeDeckViewCardContainer = QGraphicsItem::UserType + 5,
|
typeDeckViewCardContainer = QGraphicsItem::UserType + 5,
|
||||||
typeOther = QGraphicsItem::UserType + 6
|
typeOther = QGraphicsItem::UserType + 6,
|
||||||
|
typeArrow = QGraphicsItem::UserType + 7
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // COCKATRICE_GRAPHICS_ITEM_TYPE_H
|
#endif // COCKATRICE_GRAPHICS_ITEM_TYPE_H
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
#include <libcockatrice/card/card_info.h>
|
#include <libcockatrice/card/card_info.h>
|
||||||
#include <libcockatrice/deck_list/deck_list.h>
|
#include <libcockatrice/deck_list/deck_list.h>
|
||||||
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
|
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
|
||||||
|
#include <libcockatrice/settings/cards_display_settings.h>
|
||||||
|
|
||||||
DeckViewCardDragItem::DeckViewCardDragItem(DeckViewCard *_item,
|
DeckViewCardDragItem::DeckViewCardDragItem(DeckViewCard *_item,
|
||||||
const QPointF &_hotSpot,
|
const QPointF &_hotSpot,
|
||||||
|
|
@ -77,11 +78,12 @@ DeckViewCard::DeckViewCard(QGraphicsItem *parent, const CardRef &cardRef, const
|
||||||
{
|
{
|
||||||
setAcceptHoverEvents(true);
|
setAcceptHoverEvents(true);
|
||||||
|
|
||||||
connect(&SettingsCache::instance(), &SettingsCache::roundCardCornersChanged, this, [this](bool _roundCardCorners) {
|
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::roundCardCornersChanged, this,
|
||||||
Q_UNUSED(_roundCardCorners);
|
[this](bool _roundCardCorners) {
|
||||||
|
Q_UNUSED(_roundCardCorners);
|
||||||
|
|
||||||
update();
|
update();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
DeckViewCard::~DeckViewCard()
|
DeckViewCard::~DeckViewCard()
|
||||||
|
|
@ -99,7 +101,8 @@ void DeckViewCard::paint(QPainter *painter, const QStyleOptionGraphicsItem *opti
|
||||||
pen.setJoinStyle(Qt::MiterJoin);
|
pen.setJoinStyle(Qt::MiterJoin);
|
||||||
pen.setColor(originZone == DECK_ZONE_MAIN ? Qt::green : Qt::red);
|
pen.setColor(originZone == DECK_ZONE_MAIN ? Qt::green : Qt::red);
|
||||||
painter->setPen(pen);
|
painter->setPen(pen);
|
||||||
qreal cardRadius = SettingsCache::instance().getRoundCardCorners() ? 0.05 * (CardDimensions::WIDTH_F - 3) : 0.0;
|
qreal cardRadius =
|
||||||
|
SettingsCache::instance().cardsDisplay().getRoundCardCorners() ? 0.05 * (CardDimensions::WIDTH_F - 3) : 0.0;
|
||||||
painter->drawRoundedRect(QRectF(1.5, 1.5, CardDimensions::WIDTH_F - 3, CardDimensions::HEIGHT_F - 3), cardRadius,
|
painter->drawRoundedRect(QRectF(1.5, 1.5, CardDimensions::WIDTH_F - 3, CardDimensions::HEIGHT_F - 3), cardRadius,
|
||||||
cardRadius);
|
cardRadius);
|
||||||
painter->restore();
|
painter->restore();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#include "deck_view_container.h"
|
#include "deck_view_container.h"
|
||||||
|
|
||||||
#include "../../client/settings/cache_settings.h"
|
#include "../../client/settings/cache_settings.h"
|
||||||
|
#include "../../client/settings/shortcuts_settings.h"
|
||||||
#include "../../interface/card_picture_loader/card_picture_loader.h"
|
#include "../../interface/card_picture_loader/card_picture_loader.h"
|
||||||
#include "../../interface/deck_loader/deck_loader.h"
|
#include "../../interface/deck_loader/deck_loader.h"
|
||||||
#include "../../interface/widgets/dialogs/dlg_load_deck.h"
|
#include "../../interface/widgets/dialogs/dlg_load_deck.h"
|
||||||
|
|
@ -13,13 +14,17 @@
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <libcockatrice/card/database/card_database.h>
|
#include <libcockatrice/card/database/card_database.h>
|
||||||
#include <libcockatrice/card/database/card_database_manager.h>
|
#include <libcockatrice/card/database/card_database_manager.h>
|
||||||
|
#include <libcockatrice/deck_list/playmat_resolver.h>
|
||||||
#include <libcockatrice/protocol/pb/command_deck_select.pb.h>
|
#include <libcockatrice/protocol/pb/command_deck_select.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_ready_start.pb.h>
|
#include <libcockatrice/protocol/pb/command_ready_start.pb.h>
|
||||||
|
#include <libcockatrice/protocol/pb/command_set_playmat.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_set_sideboard_lock.pb.h>
|
#include <libcockatrice/protocol/pb/command_set_sideboard_lock.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_set_sideboard_plan.pb.h>
|
#include <libcockatrice/protocol/pb/command_set_sideboard_plan.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/response_deck_download.pb.h>
|
#include <libcockatrice/protocol/pb/response_deck_download.pb.h>
|
||||||
#include <libcockatrice/protocol/pending_command.h>
|
#include <libcockatrice/protocol/pending_command.h>
|
||||||
#include <libcockatrice/utility/trice_limits.h>
|
#include <libcockatrice/settings/interface_settings.h>
|
||||||
|
#include <libcockatrice/settings/visual_deck_storage_settings.h>
|
||||||
|
#include <libcockatrice/utility/string_limits.h>
|
||||||
|
|
||||||
ToggleButton::ToggleButton(QWidget *parent) : QPushButton(parent), state(false)
|
ToggleButton::ToggleButton(QWidget *parent) : QPushButton(parent), state(false)
|
||||||
{
|
{
|
||||||
|
|
@ -95,8 +100,11 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent)
|
||||||
&DeckViewContainer::refreshShortcuts);
|
&DeckViewContainer::refreshShortcuts);
|
||||||
refreshShortcuts();
|
refreshShortcuts();
|
||||||
|
|
||||||
connect(&SettingsCache::instance(), &SettingsCache::visualDeckStorageInGameChanged, this,
|
connect(&SettingsCache::instance().visualDeckStorage(), &VisualDeckStorageSettings::visualDeckStorageInGameChanged,
|
||||||
&DeckViewContainer::setVisualDeckStorageExists);
|
this, &DeckViewContainer::setVisualDeckStorageExists);
|
||||||
|
|
||||||
|
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::playmatSettingsChanged, this,
|
||||||
|
&DeckViewContainer::onPlaymatSettingsChanged);
|
||||||
|
|
||||||
switchToDeckSelectView();
|
switchToDeckSelectView();
|
||||||
}
|
}
|
||||||
|
|
@ -138,7 +146,7 @@ static void setVisibility(QPushButton *button, bool visible)
|
||||||
|
|
||||||
void DeckViewContainer::switchToDeckSelectView()
|
void DeckViewContainer::switchToDeckSelectView()
|
||||||
{
|
{
|
||||||
if (SettingsCache::instance().getVisualDeckStorageInGame()) {
|
if (SettingsCache::instance().visualDeckStorage().getVisualDeckStorageInGame()) {
|
||||||
deckView->setHidden(true);
|
deckView->setHidden(true);
|
||||||
|
|
||||||
tryCreateVisualDeckStorageWidget();
|
tryCreateVisualDeckStorageWidget();
|
||||||
|
|
@ -275,6 +283,8 @@ void DeckViewContainer::loadDeckFromFile(const QString &filePath)
|
||||||
|
|
||||||
void DeckViewContainer::loadDeckFromDeckList(const DeckList &deck)
|
void DeckViewContainer::loadDeckFromDeckList(const DeckList &deck)
|
||||||
{
|
{
|
||||||
|
currentDeck = deck;
|
||||||
|
|
||||||
QString deckString = deck.writeToString_Native();
|
QString deckString = deck.writeToString_Native();
|
||||||
|
|
||||||
if (deckString.length() > MAX_FILE_LENGTH) {
|
if (deckString.length() > MAX_FILE_LENGTH) {
|
||||||
|
|
@ -287,6 +297,52 @@ void DeckViewContainer::loadDeckFromDeckList(const DeckList &deck)
|
||||||
PendingCommand *pend = parentGame->getGame()->getGameEventHandler()->prepareGameCommand(cmd);
|
PendingCommand *pend = parentGame->getGame()->getGameEventHandler()->prepareGameCommand(cmd);
|
||||||
connect(pend, &PendingCommand::finished, this, &DeckViewContainer::deckSelectFinished);
|
connect(pend, &PendingCommand::finished, this, &DeckViewContainer::deckSelectFinished);
|
||||||
parentGame->getGame()->getGameEventHandler()->sendGameCommand(pend, playerId);
|
parentGame->getGame()->getGameEventHandler()->sendGameCommand(pend, playerId);
|
||||||
|
|
||||||
|
resolveAndSendPlaymat();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeckViewContainer::resolveAndSendPlaymat()
|
||||||
|
{
|
||||||
|
if (currentDeck.getCardRefList().isEmpty() && currentDeck.getPlaymat().card.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto &settings = SettingsCache::instance().userInterface();
|
||||||
|
const auto fallbackBehavior = static_cast<PlaymatFallbackMode>(settings.getPlaymatFallbackBehavior());
|
||||||
|
|
||||||
|
QList<PlaymatInfo> fallbackList = settings.getPlaymatFallbackList();
|
||||||
|
|
||||||
|
// In random mode with 2+ entries, remove the last-resolved mat to avoid repeats.
|
||||||
|
if (fallbackBehavior == PlaymatFallbackModeRandom && fallbackList.size() > 1) {
|
||||||
|
fallbackList.removeAll(lastResolvedPlaymat);
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlaymatInfo resolved =
|
||||||
|
resolvePlaymatForDeck(currentDeck, fallbackList, static_cast<PlaymatMode>(settings.getPlaymatMode()),
|
||||||
|
fallbackBehavior, playmatRotationIndex);
|
||||||
|
|
||||||
|
lastResolvedPlaymat = resolved;
|
||||||
|
|
||||||
|
Command_SetPlaymat playmatCmd;
|
||||||
|
auto *pp = playmatCmd.mutable_playmat_params();
|
||||||
|
pp->set_card_name(resolved.card.name.toStdString());
|
||||||
|
pp->set_card_provider_id(resolved.card.providerId.toStdString());
|
||||||
|
pp->set_margin_pct_l(resolved.params.marginPctL);
|
||||||
|
pp->set_margin_pct_r(resolved.params.marginPctR);
|
||||||
|
pp->set_vertical_offset(resolved.params.verticalOffset);
|
||||||
|
pp->set_zoom(resolved.params.zoom);
|
||||||
|
PendingCommand *playmatPend = parentGame->getGame()->getGameEventHandler()->prepareGameCommand(playmatCmd);
|
||||||
|
parentGame->getGame()->getGameEventHandler()->sendGameCommand(playmatPend, playerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeckViewContainer::onPlaymatSettingsChanged()
|
||||||
|
{
|
||||||
|
resolveAndSendPlaymat();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeckViewContainer::advancePlaymatRotation()
|
||||||
|
{
|
||||||
|
playmatRotationIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
void DeckViewContainer::loadRemoteDeck()
|
void DeckViewContainer::loadRemoteDeck()
|
||||||
|
|
@ -377,6 +433,10 @@ void DeckViewContainer::sideboardPlanChanged()
|
||||||
*/
|
*/
|
||||||
void DeckViewContainer::sendReadyStartCommand(bool ready)
|
void DeckViewContainer::sendReadyStartCommand(bool ready)
|
||||||
{
|
{
|
||||||
|
if (ready) {
|
||||||
|
resolveAndSendPlaymat();
|
||||||
|
}
|
||||||
|
|
||||||
Command_ReadyStart cmd;
|
Command_ReadyStart cmd;
|
||||||
cmd.set_ready(ready);
|
cmd.set_ready(ready);
|
||||||
parentGame->getGame()->getGameEventHandler()->sendGameCommand(cmd, playerId);
|
parentGame->getGame()->getGameEventHandler()->sendGameCommand(cmd, playerId);
|
||||||
|
|
@ -414,6 +474,7 @@ void DeckViewContainer::setSideboardLocked(bool locked)
|
||||||
|
|
||||||
void DeckViewContainer::setDeck(const DeckList &deck)
|
void DeckViewContainer::setDeck(const DeckList &deck)
|
||||||
{
|
{
|
||||||
|
currentDeck = deck;
|
||||||
deckView->setDeck(deck);
|
deckView->setDeck(deck);
|
||||||
switchToDeckLoadedView();
|
switchToDeckLoadedView();
|
||||||
}
|
}
|
||||||
|
|
@ -57,6 +57,9 @@ private:
|
||||||
VisualDeckStorageWidget *visualDeckStorageWidget;
|
VisualDeckStorageWidget *visualDeckStorageWidget;
|
||||||
TabGame *parentGame;
|
TabGame *parentGame;
|
||||||
int playerId;
|
int playerId;
|
||||||
|
int playmatRotationIndex = 0; ///< Per-match cursor for round-robin playmat mode.
|
||||||
|
DeckList currentDeck; ///< Cached deck for live settings re-resolution.
|
||||||
|
PlaymatInfo lastResolvedPlaymat; ///< Tracks last sent playmat to avoid repeats in random mode.
|
||||||
|
|
||||||
void tryCreateVisualDeckStorageWidget();
|
void tryCreateVisualDeckStorageWidget();
|
||||||
void sendReadyStartCommand(bool ready);
|
void sendReadyStartCommand(bool ready);
|
||||||
|
|
@ -75,6 +78,7 @@ private slots:
|
||||||
void sideboardLockButtonClicked();
|
void sideboardLockButtonClicked();
|
||||||
void updateSideboardLockButtonText();
|
void updateSideboardLockButtonText();
|
||||||
void refreshShortcuts();
|
void refreshShortcuts();
|
||||||
|
void onPlaymatSettingsChanged();
|
||||||
signals:
|
signals:
|
||||||
void newCardAdded(AbstractCardItem *card);
|
void newCardAdded(AbstractCardItem *card);
|
||||||
void notIdle();
|
void notIdle();
|
||||||
|
|
@ -87,6 +91,8 @@ public:
|
||||||
void setSideboardLocked(bool locked);
|
void setSideboardLocked(bool locked);
|
||||||
void setDeck(const DeckList &deck);
|
void setDeck(const DeckList &deck);
|
||||||
void setVisualDeckStorageExists(bool exists);
|
void setVisualDeckStorageExists(bool exists);
|
||||||
|
void advancePlaymatRotation();
|
||||||
|
void resolveAndSendPlaymat();
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void loadDeckFromFile(const QString &filePath);
|
void loadDeckFromFile(const QString &filePath);
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,10 @@
|
||||||
#include <libcockatrice/deck_list/deck_list.h>
|
#include <libcockatrice/deck_list/deck_list.h>
|
||||||
#include <libcockatrice/models/database/card_database_model.h>
|
#include <libcockatrice/models/database/card_database_model.h>
|
||||||
#include <libcockatrice/models/database/token/token_display_model.h>
|
#include <libcockatrice/models/database/token/token_display_model.h>
|
||||||
#include <libcockatrice/utility/trice_limits.h>
|
#include <libcockatrice/settings/card_override_settings.h>
|
||||||
|
#include <libcockatrice/settings/interface_settings.h>
|
||||||
|
#include <libcockatrice/settings/layouts_settings.h>
|
||||||
|
#include <libcockatrice/utility/string_limits.h>
|
||||||
|
|
||||||
DlgCreateToken::DlgCreateToken(const QStringList &_predefinedTokens, QWidget *parent)
|
DlgCreateToken::DlgCreateToken(const QStringList &_predefinedTokens, QWidget *parent)
|
||||||
: QDialog(parent), predefinedTokens(_predefinedTokens)
|
: QDialog(parent), predefinedTokens(_predefinedTokens)
|
||||||
|
|
@ -186,7 +189,7 @@ void DlgCreateToken::tokenSelectionChanged(const QModelIndex ¤t, const QMo
|
||||||
const QChar cardColor = cardInfo->getColorChar();
|
const QChar cardColor = cardInfo->getColorChar();
|
||||||
colorEdit->setCurrentIndex(colorEdit->findData(cardColor, Qt::UserRole, Qt::MatchFixedString));
|
colorEdit->setCurrentIndex(colorEdit->findData(cardColor, Qt::UserRole, Qt::MatchFixedString));
|
||||||
ptEdit->setText(cardInfo->getPowTough());
|
ptEdit->setText(cardInfo->getPowTough());
|
||||||
if (SettingsCache::instance().getAnnotateTokens()) {
|
if (SettingsCache::instance().userInterface().getAnnotateTokens()) {
|
||||||
annotationEdit->setText(cardInfo->getText());
|
annotationEdit->setText(cardInfo->getText());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
#include <QSpinBox>
|
#include <QSpinBox>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
#include <libcockatrice/utility/trice_limits.h>
|
#include <libcockatrice/utility/dice_limits.h>
|
||||||
|
|
||||||
DlgRollDice::DlgRollDice(QWidget *parent) : QDialog(parent)
|
DlgRollDice::DlgRollDice(QWidget *parent) : QDialog(parent)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QGraphicsSceneMouseEvent>
|
#include <QGraphicsSceneMouseEvent>
|
||||||
#include <QGraphicsView>
|
#include <QGraphicsView>
|
||||||
#include <QSet>
|
|
||||||
#include <QtMath>
|
#include <QtMath>
|
||||||
|
#include <libcockatrice/settings/interface_settings.h>
|
||||||
#include <libcockatrice/utility/zone_names.h>
|
#include <libcockatrice/utility/zone_names.h>
|
||||||
#include <numeric>
|
#include <numeric>
|
||||||
|
|
||||||
|
|
@ -36,7 +36,7 @@ GameScene::GameScene(PhasesToolbar *_phasesToolbar, QObject *parent)
|
||||||
{
|
{
|
||||||
animationTimer = new QBasicTimer;
|
animationTimer = new QBasicTimer;
|
||||||
addItem(phasesToolbar);
|
addItem(phasesToolbar);
|
||||||
connect(&SettingsCache::instance(), &SettingsCache::minPlayersForMultiColumnLayoutChanged, this,
|
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::minPlayersForMultiColumnLayoutChanged, this,
|
||||||
&GameScene::rearrange);
|
&GameScene::rearrange);
|
||||||
|
|
||||||
rearrange();
|
rearrange();
|
||||||
|
|
@ -44,7 +44,25 @@ GameScene::GameScene(PhasesToolbar *_phasesToolbar, QObject *parent)
|
||||||
|
|
||||||
GameScene::~GameScene()
|
GameScene::~GameScene()
|
||||||
{
|
{
|
||||||
|
// Sever all incoming connections (animated item destroy-tracking) before the
|
||||||
|
// members below are destroyed: the base QGraphicsScene destructor destroys the
|
||||||
|
// remaining items, and their destroyed() signals must not reach slots that
|
||||||
|
// reference members that no longer exist.
|
||||||
|
disconnect(this);
|
||||||
|
|
||||||
delete animationTimer;
|
delete animationTimer;
|
||||||
|
animationTimer = nullptr;
|
||||||
|
|
||||||
|
// Delete all ArrowItems before QGraphicsScene's base destructor runs.
|
||||||
|
// QGraphicsScene::~QGraphicsScene() destroys items in arbitrary order.
|
||||||
|
// If a PlayerTarget is destroyed before an ArrowItem pointing to it,
|
||||||
|
// ArrowItem::onTargetDestroyed fires and emits on the partially-destroyed
|
||||||
|
// GameScene, causing a segfault.
|
||||||
|
for (auto *item : items()) {
|
||||||
|
if (auto *arrow = qgraphicsitem_cast<ArrowItem *>(item)) {
|
||||||
|
delete arrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// DO NOT call clearViews() here
|
// DO NOT call clearViews() here
|
||||||
// clearViews calls close() on the zoneViews, which sends signals; sending signals in destructors leads to segfaults
|
// clearViews calls close() on the zoneViews, which sends signals; sending signals in destructors leads to segfaults
|
||||||
|
|
@ -234,17 +252,27 @@ void GameScene::adjustPlayerRotation(int rotationAdjustment)
|
||||||
*/
|
*/
|
||||||
void GameScene::rearrange()
|
void GameScene::rearrange()
|
||||||
{
|
{
|
||||||
int firstPlayerIndex = 0;
|
if (rearranging) {
|
||||||
auto playersPlaying = collectActivePlayers(firstPlayerIndex);
|
needsReArrange = true;
|
||||||
playersPlaying = rotatePlayers(playersPlaying, firstPlayerIndex);
|
return;
|
||||||
|
}
|
||||||
|
rearranging = true;
|
||||||
|
do {
|
||||||
|
needsReArrange = false;
|
||||||
|
|
||||||
int columns = determineColumnCount(playersPlaying.size());
|
int firstPlayerIndex = 0;
|
||||||
QSizeF sceneSize = computeSceneSizeAndPlayerLayout(playersPlaying, columns);
|
auto playersPlaying = collectActivePlayers(firstPlayerIndex);
|
||||||
|
playersPlaying = rotatePlayers(playersPlaying, firstPlayerIndex);
|
||||||
|
|
||||||
phasesToolbar->setHeight(sceneSize.height());
|
int columns = determineColumnCount(playersPlaying.size());
|
||||||
setSceneRect(0, 0, sceneSize.width(), sceneSize.height());
|
QSizeF sceneSize = computeSceneSizeAndPlayerLayout(playersPlaying, columns);
|
||||||
|
|
||||||
processViewSizeChange(viewSize);
|
phasesToolbar->setHeight(sceneSize.height());
|
||||||
|
setSceneRect(0, 0, sceneSize.width(), sceneSize.height());
|
||||||
|
|
||||||
|
processViewSizeChange(viewSize);
|
||||||
|
} while (needsReArrange);
|
||||||
|
rearranging = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- View Size ----------
|
// ---------- View Size ----------
|
||||||
|
|
@ -324,7 +352,7 @@ QList<PlayerLogic *> GameScene::rotatePlayers(const QList<PlayerLogic *> &active
|
||||||
|
|
||||||
int GameScene::determineColumnCount(int playerCount)
|
int GameScene::determineColumnCount(int playerCount)
|
||||||
{
|
{
|
||||||
return playerCount < SettingsCache::instance().getMinPlayersForMultiColumnLayout() ? 1 : 2;
|
return playerCount < SettingsCache::instance().userInterface().getMinPlayersForMultiColumnLayout() ? 1 : 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -441,8 +469,14 @@ void GameScene::resizeColumnsAndPlayers(const QList<qreal> &minWidthByColumn, qr
|
||||||
qreal extraWidthPerColumn = (newWidth - minWidth) / playersByColumn.size();
|
qreal extraWidthPerColumn = (newWidth - minWidth) / playersByColumn.size();
|
||||||
qreal newx = phasesToolbar->getWidth();
|
qreal newx = phasesToolbar->getWidth();
|
||||||
|
|
||||||
for (int col = 0; col < playersByColumn.size(); ++col) {
|
// Snapshot the columns: resizing a player's table can synchronously trigger
|
||||||
for (PlayerGraphicsItem *player : playersByColumn[col]) {
|
// GameScene::rearrange (table width -> sizeChanged -> updateBoundingRect ->
|
||||||
|
// sizeChanged -> rearrange), and rearrange rebuilds playersByColumn. Iterating
|
||||||
|
// the live container across that re-entrant call would use invalidated iterators.
|
||||||
|
const QList<QList<PlayerGraphicsItem *>> columns = playersByColumn;
|
||||||
|
|
||||||
|
for (int col = 0; col < columns.size(); ++col) {
|
||||||
|
for (PlayerGraphicsItem *player : columns[col]) {
|
||||||
player->processSceneSizeChange(minWidthByColumn[col] + extraWidthPerColumn);
|
player->processSceneSizeChange(minWidthByColumn[col] + extraWidthPerColumn);
|
||||||
player->setPos(newx, player->y());
|
player->setPos(newx, player->y());
|
||||||
}
|
}
|
||||||
|
|
@ -484,6 +518,7 @@ void GameScene::addArrow(QSharedPointer<ArrowData> data)
|
||||||
|
|
||||||
auto *arrow = new ArrowItem(data, startCard, targetItem);
|
auto *arrow = new ArrowItem(data, startCard, targetItem);
|
||||||
addItem(arrow);
|
addItem(arrow);
|
||||||
|
arrow->startDrawAnimation();
|
||||||
arrowRegistry.insert(data, arrow);
|
arrowRegistry.insert(data, arrow);
|
||||||
connect(arrow, &ArrowItem::requestDeletion, this, &GameScene::requestArrowDeletion);
|
connect(arrow, &ArrowItem::requestDeletion, this, &GameScene::requestArrowDeletion);
|
||||||
}
|
}
|
||||||
|
|
@ -529,7 +564,9 @@ void GameScene::clearArrowsForPlayer(int playerId)
|
||||||
void GameScene::clearArrowsForPlayerLocally(int playerId)
|
void GameScene::clearArrowsForPlayerLocally(int playerId)
|
||||||
{
|
{
|
||||||
for (int arrowId : arrowRegistry.idsForPlayer(playerId)) {
|
for (int arrowId : arrowRegistry.idsForPlayer(playerId)) {
|
||||||
arrowRegistry.take(playerId, arrowId)->delArrow();
|
if (auto *arrow = arrowRegistry.take(playerId, arrowId)) {
|
||||||
|
arrow->delArrow();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -722,30 +759,45 @@ bool GameScene::event(QEvent *event)
|
||||||
|
|
||||||
void GameScene::timerEvent(QTimerEvent * /*event*/)
|
void GameScene::timerEvent(QTimerEvent * /*event*/)
|
||||||
{
|
{
|
||||||
QMutableSetIterator<CardItem *> i(cardsToAnimate);
|
QMutableHashIterator<QObject *, IAnimatedItem *> i(animatedItems);
|
||||||
while (i.hasNext()) {
|
while (i.hasNext()) {
|
||||||
i.next();
|
i.next();
|
||||||
if (!i.value()->animationEvent()) {
|
if (!i.value()->animationEvent()) {
|
||||||
i.remove();
|
i.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (cardsToAnimate.isEmpty()) {
|
if (animatedItems.isEmpty()) {
|
||||||
animationTimer->stop();
|
animationTimer->stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameScene::registerAnimationItem(AbstractCardItem *card)
|
void GameScene::registerAnimationItem(IAnimatedItem *item)
|
||||||
{
|
{
|
||||||
cardsToAnimate.insert(static_cast<CardItem *>(card));
|
auto *object = dynamic_cast<QObject *>(item);
|
||||||
if (!animationTimer->isActive()) {
|
if (!object) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!animatedItems.contains(object)) {
|
||||||
|
connect(object, &QObject::destroyed, this, &GameScene::removeAnimatedItem);
|
||||||
|
}
|
||||||
|
animatedItems.insert(object, item);
|
||||||
|
if (animationTimer && !animationTimer->isActive()) {
|
||||||
animationTimer->start(10, this);
|
animationTimer->start(10, this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameScene::unregisterAnimationItem(AbstractCardItem *card)
|
void GameScene::unregisterAnimationItem(IAnimatedItem *item)
|
||||||
{
|
{
|
||||||
cardsToAnimate.remove(static_cast<CardItem *>(card));
|
animatedItems.remove(dynamic_cast<QObject *>(item));
|
||||||
if (cardsToAnimate.isEmpty()) {
|
if (animationTimer && animatedItems.isEmpty()) {
|
||||||
|
animationTimer->stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameScene::removeAnimatedItem(QObject *item)
|
||||||
|
{
|
||||||
|
animatedItems.remove(item);
|
||||||
|
if (animationTimer && animatedItems.isEmpty()) {
|
||||||
animationTimer->stop();
|
animationTimer->stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,14 @@
|
||||||
#include "../game/arrow_registry.h"
|
#include "../game/arrow_registry.h"
|
||||||
#include "../game/board/arrow_data.h"
|
#include "../game/board/arrow_data.h"
|
||||||
#include "../game/zones/card_zone_logic.h"
|
#include "../game/zones/card_zone_logic.h"
|
||||||
|
#include "animated_item.h"
|
||||||
#include "board/arrow_item.h"
|
#include "board/arrow_item.h"
|
||||||
|
|
||||||
#include <QGraphicsScene>
|
#include <QGraphicsScene>
|
||||||
|
#include <QHash>
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QLoggingCategory>
|
#include <QLoggingCategory>
|
||||||
#include <QPointer>
|
#include <QPointer>
|
||||||
#include <QSet>
|
|
||||||
|
|
||||||
inline Q_LOGGING_CATEGORY(GameSceneLog, "game_scene");
|
inline Q_LOGGING_CATEGORY(GameSceneLog, "game_scene");
|
||||||
inline Q_LOGGING_CATEGORY(GameScenePlayerAdditionRemovalLog, "game_scene.player_addition_removal");
|
inline Q_LOGGING_CATEGORY(GameScenePlayerAdditionRemovalLog, "game_scene.player_addition_removal");
|
||||||
|
|
@ -24,6 +25,7 @@ class CardItem;
|
||||||
class ServerInfo_Card;
|
class ServerInfo_Card;
|
||||||
class PhasesToolbar;
|
class PhasesToolbar;
|
||||||
class QBasicTimer;
|
class QBasicTimer;
|
||||||
|
class QObject;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @class GameScene
|
* @class GameScene
|
||||||
|
|
@ -50,9 +52,11 @@ private:
|
||||||
QList<ZoneViewWidget *> zoneViews; ///< Active zone view widgets
|
QList<ZoneViewWidget *> zoneViews; ///< Active zone view widgets
|
||||||
QSize viewSize; ///< Current view size
|
QSize viewSize; ///< Current view size
|
||||||
QPointer<CardItem> hoveredCard; ///< Currently hovered card
|
QPointer<CardItem> hoveredCard; ///< Currently hovered card
|
||||||
QBasicTimer *animationTimer; ///< Timer for card animations
|
QBasicTimer *animationTimer; ///< Timer for scene animations
|
||||||
QSet<CardItem *> cardsToAnimate; ///< Cards currently animating
|
QHash<QObject *, IAnimatedItem *> animatedItems; ///< Items currently animating
|
||||||
int playerRotation; ///< Rotation offset for player layout
|
int playerRotation; ///< Rotation offset for player layout
|
||||||
|
bool rearranging = false; ///< Guard against re-entrant rearrange
|
||||||
|
bool needsReArrange = false; ///< Pending rearrange requested during a pass
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Updates which card is currently hovered based on scene coordinates.
|
* @brief Updates which card is currently hovered based on scene coordinates.
|
||||||
|
|
@ -182,15 +186,24 @@ public:
|
||||||
/** @brief Updates hovered card highlighting. */
|
/** @brief Updates hovered card highlighting. */
|
||||||
void updateHoveredCard(CardItem *newCard);
|
void updateHoveredCard(CardItem *newCard);
|
||||||
|
|
||||||
/** @brief Registers a card for animation updates. */
|
/**
|
||||||
void registerAnimationItem(AbstractCardItem *card);
|
* @brief Registers an item for animation updates with the shared scene timer.
|
||||||
|
*
|
||||||
|
* The item must inherit QObject; it is unregistered automatically when it is
|
||||||
|
* destroyed, so it may be deleted mid-animation without a dangling pointer.
|
||||||
|
*/
|
||||||
|
void registerAnimationItem(IAnimatedItem *item);
|
||||||
|
|
||||||
/** @brief Unregisters a card from animation updates. */
|
/** @brief Unregisters an item from animation updates. */
|
||||||
void unregisterAnimationItem(AbstractCardItem *card);
|
void unregisterAnimationItem(IAnimatedItem *item);
|
||||||
void startRubberBand(const QPointF &selectionOrigin);
|
void startRubberBand(const QPointF &selectionOrigin);
|
||||||
void resizeRubberBand(const QPointF &cursorPoint, int selectedCount);
|
void resizeRubberBand(const QPointF &cursorPoint, int selectedCount);
|
||||||
void stopRubberBand();
|
void stopRubberBand();
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
/** @brief Removes a destroyed item from the animation set. */
|
||||||
|
void removeAnimatedItem(QObject *item);
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void onCardSelectionChanged(AbstractCardItem *card, bool selected);
|
void onCardSelectionChanged(AbstractCardItem *card, bool selected);
|
||||||
void onCardRightClicked(AbstractCardItem *card, QPoint screenPos);
|
void onCardRightClicked(AbstractCardItem *card, QPoint screenPos);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
#include "game_view.h"
|
#include "game_view.h"
|
||||||
|
|
||||||
#include "../client/settings/cache_settings.h"
|
#include "../client/settings/cache_settings.h"
|
||||||
#include "../game/selection_subtype_tally.h"
|
#include "../client/settings/shortcuts_settings.h"
|
||||||
#include "game_scene.h"
|
#include "game_scene.h"
|
||||||
|
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
#include <QLayout>
|
#include <QLayout>
|
||||||
#include <QResizeEvent>
|
#include <QResizeEvent>
|
||||||
#include <QRubberBand>
|
#include <QRubberBand>
|
||||||
|
#include <libcockatrice/settings/interface_settings.h>
|
||||||
#include <libcockatrice/utility/qt_utils.h>
|
#include <libcockatrice/utility/qt_utils.h>
|
||||||
|
|
||||||
// QRubberBand calls raise() in showEvent() and changeEvent() to stay on top of siblings.
|
// QRubberBand calls raise() in showEvent() and changeEvent() to stay on top of siblings.
|
||||||
|
|
@ -46,9 +47,12 @@ GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, par
|
||||||
connect(scene, &GameScene::sigResizeRubberBand, this, &GameView::resizeRubberBand);
|
connect(scene, &GameScene::sigResizeRubberBand, this, &GameView::resizeRubberBand);
|
||||||
connect(scene, &GameScene::sigStopRubberBand, this, &GameView::stopRubberBand);
|
connect(scene, &GameScene::sigStopRubberBand, this, &GameView::stopRubberBand);
|
||||||
connect(scene, &QGraphicsScene::selectionChanged, this, [this]() { updateTotalSelectionCount(); });
|
connect(scene, &QGraphicsScene::selectionChanged, this, [this]() { updateTotalSelectionCount(); });
|
||||||
|
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::tallyTypeChanged, this,
|
||||||
|
[this] { updateTotalSelectionCount(); });
|
||||||
|
|
||||||
setFocusDisabled(SettingsCache::instance().getKeepGameChatFocus());
|
setFocusDisabled(SettingsCache::instance().userInterface().getKeepGameChatFocus());
|
||||||
connect(&SettingsCache::instance(), &SettingsCache::keepGameChatFocusChanged, this, &GameView::setFocusDisabled);
|
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::keepGameChatFocusChanged, this,
|
||||||
|
&GameView::setFocusDisabled);
|
||||||
|
|
||||||
aCloseMostRecentZoneView = new QAction(this);
|
aCloseMostRecentZoneView = new QAction(this);
|
||||||
|
|
||||||
|
|
@ -79,12 +83,12 @@ GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, par
|
||||||
totalCountLabel->setStyleSheet(totalCountLabelStyle);
|
totalCountLabel->setStyleSheet(totalCountLabelStyle);
|
||||||
totalCountLabel->hide();
|
totalCountLabel->hide();
|
||||||
|
|
||||||
subtypeTallyContainer = new QWidget(this);
|
tallyContainer = new QWidget(this);
|
||||||
subtypeTallyContainer->setStyleSheet(subtypeTallyLabelStyle);
|
tallyContainer->setStyleSheet(subtypeTallyLabelStyle);
|
||||||
subtypeTallyLayout = new QGridLayout(subtypeTallyContainer);
|
tallyLayout = new QGridLayout(tallyContainer);
|
||||||
subtypeTallyLayout->setContentsMargins(2, 2, 2, 2);
|
tallyLayout->setContentsMargins(2, 2, 2, 2);
|
||||||
subtypeTallyLayout->setSpacing(2);
|
tallyLayout->setSpacing(2);
|
||||||
subtypeTallyContainer->hide();
|
tallyContainer->hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameView::resizeEvent(QResizeEvent *event)
|
void GameView::resizeEvent(QResizeEvent *event)
|
||||||
|
|
@ -110,6 +114,7 @@ void GameView::startRubberBand(const QPointF &_selectionOrigin)
|
||||||
}
|
}
|
||||||
|
|
||||||
selectionOrigin = _selectionOrigin;
|
selectionOrigin = _selectionOrigin;
|
||||||
|
previousBandRect = QRect();
|
||||||
rubberBand->setGeometry(QRect(mapFromScene(selectionOrigin), QSize(0, 0)));
|
rubberBand->setGeometry(QRect(mapFromScene(selectionOrigin), QSize(0, 0)));
|
||||||
rubberBand->show();
|
rubberBand->show();
|
||||||
}
|
}
|
||||||
|
|
@ -124,9 +129,19 @@ void GameView::resizeRubberBand(const QPointF &cursorPoint, int selectedCount)
|
||||||
|
|
||||||
QPoint cursor = cursorPoint.toPoint();
|
QPoint cursor = cursorPoint.toPoint();
|
||||||
QRect rect = QRect(mapFromScene(selectionOrigin), cursor).normalized();
|
QRect rect = QRect(mapFromScene(selectionOrigin), cursor).normalized();
|
||||||
rubberBand->setGeometry(rect);
|
|
||||||
|
|
||||||
if (!SettingsCache::instance().getShowDragSelectionCount()) {
|
rubberBand->setGeometry(rect);
|
||||||
|
if (viewport()) {
|
||||||
|
// Repaint the union of the previous and current band rects: the vacated
|
||||||
|
// strip of a child widget is not reliably invalidated on all platforms
|
||||||
|
// (notably macOS), leaving stale pixels under the selection.
|
||||||
|
QRect dirty = previousBandRect.isNull() ? rect : previousBandRect.united(rect);
|
||||||
|
dirty.adjust(-1, -1, 1, 1);
|
||||||
|
viewport()->update(dirty);
|
||||||
|
previousBandRect = rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SettingsCache::instance().userInterface().getShowDragSelectionCount()) {
|
||||||
dragCountLabel->hide();
|
dragCountLabel->hide();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -167,7 +182,13 @@ void GameView::stopRubberBand()
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Same rationale as resizeRubberBand: repaint the last known band area
|
||||||
|
// since hiding a child widget doesn't reliably invalidate its region.
|
||||||
rubberBand->hide();
|
rubberBand->hide();
|
||||||
|
if (viewport() && !previousBandRect.isNull()) {
|
||||||
|
viewport()->update(previousBandRect.adjusted(-1, -1, 1, 1));
|
||||||
|
previousBandRect = QRect();
|
||||||
|
}
|
||||||
dragCountLabel->hide();
|
dragCountLabel->hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -177,14 +198,14 @@ void GameView::refreshShortcuts()
|
||||||
SettingsCache::instance().shortcuts().getShortcut("Player/aCloseMostRecentZoneView"));
|
SettingsCache::instance().shortcuts().getShortcut("Player/aCloseMostRecentZoneView"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameView::clearSubtypeLabels()
|
void GameView::clearTallyLabels()
|
||||||
{
|
{
|
||||||
QtUtils::clearLayoutRec(subtypeTallyLayout);
|
QtUtils::clearLayoutRec(tallyLayout);
|
||||||
}
|
}
|
||||||
|
|
||||||
QSize GameView::rebuildSubtypeLabels(const QList<SubtypeEntry> &entries)
|
QSize GameView::rebuildTallyLabels(const QList<TallyRow> &entries)
|
||||||
{
|
{
|
||||||
clearSubtypeLabels();
|
clearTallyLabels();
|
||||||
|
|
||||||
const QString nameStyle = QStringLiteral("color: white; font-size: 12px; background: transparent;");
|
const QString nameStyle = QStringLiteral("color: white; font-size: 12px; background: transparent;");
|
||||||
const QString countStyle =
|
const QString countStyle =
|
||||||
|
|
@ -195,16 +216,16 @@ QSize GameView::rebuildSubtypeLabels(const QList<SubtypeEntry> &entries)
|
||||||
int maxCountWidth = 0;
|
int maxCountWidth = 0;
|
||||||
|
|
||||||
int row = 0;
|
int row = 0;
|
||||||
for (const SubtypeEntry &entry : entries) {
|
for (const TallyRow &entry : entries) {
|
||||||
auto *nameLabel = new QLabel(entry.name, subtypeTallyContainer);
|
auto *nameLabel = new QLabel(entry.name, tallyContainer);
|
||||||
nameLabel->setStyleSheet(nameStyle);
|
nameLabel->setStyleSheet(nameStyle);
|
||||||
nameLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
nameLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||||||
subtypeTallyLayout->addWidget(nameLabel, row, 0);
|
tallyLayout->addWidget(nameLabel, row, 0);
|
||||||
|
|
||||||
auto *countLabel = new QLabel(QString::number(entry.count), subtypeTallyContainer);
|
auto *countLabel = new QLabel(entry.value, tallyContainer);
|
||||||
countLabel->setStyleSheet(countStyle);
|
countLabel->setStyleSheet(countStyle);
|
||||||
countLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
countLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||||||
subtypeTallyLayout->addWidget(countLabel, row, 1);
|
tallyLayout->addWidget(countLabel, row, 1);
|
||||||
|
|
||||||
QSize nameSize = nameLabel->sizeHint();
|
QSize nameSize = nameLabel->sizeHint();
|
||||||
QSize countSize = countLabel->sizeHint();
|
QSize countSize = countLabel->sizeHint();
|
||||||
|
|
@ -215,9 +236,9 @@ QSize GameView::rebuildSubtypeLabels(const QList<SubtypeEntry> &entries)
|
||||||
++row;
|
++row;
|
||||||
}
|
}
|
||||||
|
|
||||||
int spacing = subtypeTallyLayout->spacing();
|
int spacing = tallyLayout->spacing();
|
||||||
int margins = subtypeTallyLayout->contentsMargins().left() + subtypeTallyLayout->contentsMargins().right();
|
int margins = tallyLayout->contentsMargins().left() + tallyLayout->contentsMargins().right();
|
||||||
int verticalMargins = subtypeTallyLayout->contentsMargins().top() + subtypeTallyLayout->contentsMargins().bottom();
|
int verticalMargins = tallyLayout->contentsMargins().top() + tallyLayout->contentsMargins().bottom();
|
||||||
|
|
||||||
int width = maxNameWidth + spacing + maxCountWidth + margins;
|
int width = maxNameWidth + spacing + maxCountWidth + margins;
|
||||||
int height = totalHeight + (row - 1) * spacing + verticalMargins;
|
int height = totalHeight + (row - 1) * spacing + verticalMargins;
|
||||||
|
|
@ -235,7 +256,7 @@ void GameView::updateTotalSelectionCount(const QSize &viewSize)
|
||||||
|
|
||||||
int count = scene()->selectedItems().count();
|
int count = scene()->selectedItems().count();
|
||||||
|
|
||||||
if (!SettingsCache::instance().getShowTotalSelectionCount() || count <= 1) {
|
if (!SettingsCache::instance().userInterface().getShowTotalSelectionCount() || count <= 1) {
|
||||||
totalCountLabel->hide();
|
totalCountLabel->hide();
|
||||||
} else {
|
} else {
|
||||||
totalCountLabel->setText(QString::number(count));
|
totalCountLabel->setText(QString::number(count));
|
||||||
|
|
@ -247,29 +268,25 @@ void GameView::updateTotalSelectionCount(const QSize &viewSize)
|
||||||
totalCountLabel->show();
|
totalCountLabel->show();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!SettingsCache::instance().getShowSubtypeSelectionTally() || count <= 1) {
|
TallyType tallyType = Tally::intToType(SettingsCache::instance().userInterface().getTallyType());
|
||||||
subtypeTallyContainer->hide();
|
|
||||||
cachedSubtypeEntries.clear();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
GameScene *gameScene = static_cast<GameScene *>(scene());
|
GameScene *gameScene = static_cast<GameScene *>(scene());
|
||||||
QList<SubtypeEntry> entries = SelectionSubtypeTally::countSubtypes(gameScene->selectedCards());
|
QList<TallyRow> entries = Tally::compute(gameScene->selectedCards(), tallyType);
|
||||||
|
|
||||||
if (entries.isEmpty()) {
|
if (entries.isEmpty()) {
|
||||||
subtypeTallyContainer->hide();
|
tallyContainer->hide();
|
||||||
cachedSubtypeEntries.clear();
|
cachedTallyRows.clear();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only rebuild labels if entries changed
|
// Only rebuild labels if entries changed
|
||||||
QSize containerSize;
|
QSize containerSize;
|
||||||
if (entries != cachedSubtypeEntries) {
|
if (entries != cachedTallyRows) {
|
||||||
cachedSubtypeEntries = entries;
|
cachedTallyRows = entries;
|
||||||
containerSize = rebuildSubtypeLabels(entries);
|
containerSize = rebuildTallyLabels(entries);
|
||||||
subtypeTallyContainer->resize(containerSize);
|
tallyContainer->resize(containerSize);
|
||||||
} else {
|
} else {
|
||||||
containerSize = subtypeTallyContainer->size();
|
containerSize = tallyContainer->size();
|
||||||
}
|
}
|
||||||
|
|
||||||
int x = availableWidth - containerSize.width() - kMarginInPixels;
|
int x = availableWidth - containerSize.width() - kMarginInPixels;
|
||||||
|
|
@ -283,8 +300,8 @@ void GameView::updateTotalSelectionCount(const QSize &viewSize)
|
||||||
|
|
||||||
y = qMax(kMarginInPixels, y);
|
y = qMax(kMarginInPixels, y);
|
||||||
|
|
||||||
subtypeTallyContainer->move(x, y);
|
tallyContainer->move(x, y);
|
||||||
subtypeTallyContainer->show();
|
tallyContainer->show();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
#ifndef GAMEVIEW_H
|
#ifndef GAMEVIEW_H
|
||||||
#define GAMEVIEW_H
|
#define GAMEVIEW_H
|
||||||
|
|
||||||
#include "../game/selection_subtype_tally.h"
|
#include "tally/tally.h"
|
||||||
|
|
||||||
#include <QGraphicsView>
|
#include <QGraphicsView>
|
||||||
|
|
||||||
|
|
@ -24,13 +24,14 @@ private:
|
||||||
QRubberBand *rubberBand;
|
QRubberBand *rubberBand;
|
||||||
QLabel *dragCountLabel;
|
QLabel *dragCountLabel;
|
||||||
QLabel *totalCountLabel;
|
QLabel *totalCountLabel;
|
||||||
QWidget *subtypeTallyContainer;
|
QWidget *tallyContainer;
|
||||||
QGridLayout *subtypeTallyLayout;
|
QGridLayout *tallyLayout;
|
||||||
QPointF selectionOrigin;
|
QPointF selectionOrigin;
|
||||||
QList<SubtypeEntry> cachedSubtypeEntries; ///< Cached entries to avoid redundant rebuilds
|
QRect previousBandRect; ///< Last rubber-band rect for targeted repaint
|
||||||
|
QList<TallyRow> cachedTallyRows; ///< Cached entries to avoid redundant rebuilds
|
||||||
|
|
||||||
QSize rebuildSubtypeLabels(const QList<SubtypeEntry> &entries);
|
QSize rebuildTallyLabels(const QList<TallyRow> &entries);
|
||||||
void clearSubtypeLabels();
|
void clearTallyLabels();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void resizeEvent(QResizeEvent *event) override;
|
void resizeEvent(QResizeEvent *event) override;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#include "card_menu.h"
|
#include "card_menu.h"
|
||||||
|
|
||||||
#include "../../../client/settings/card_counter_settings.h"
|
#include "../../../client/settings/card_counter_settings.h"
|
||||||
|
#include "../../../client/settings/shortcuts_settings.h"
|
||||||
#include "../../../interface/widgets/tabs/tab_game.h"
|
#include "../../../interface/widgets/tabs/tab_game.h"
|
||||||
#include "../../board/card_item.h"
|
#include "../../board/card_item.h"
|
||||||
#include "../../game/player/player_actions.h"
|
#include "../../game/player/player_actions.h"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#include "grave_menu.h"
|
#include "grave_menu.h"
|
||||||
|
|
||||||
|
#include "../../../client/settings/shortcuts_settings.h"
|
||||||
#include "../../game/abstract_game.h"
|
#include "../../game/abstract_game.h"
|
||||||
#include "../../game/player/player_actions.h"
|
#include "../../game/player/player_actions.h"
|
||||||
#include "../../game/player/player_logic.h"
|
#include "../../game/player/player_logic.h"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#include "move_menu.h"
|
#include "move_menu.h"
|
||||||
|
|
||||||
|
#include "../../../client/settings/shortcuts_settings.h"
|
||||||
#include "../../game/player/player_actions.h"
|
#include "../../game/player/player_actions.h"
|
||||||
#include "../../game/player/player_logic.h"
|
#include "../../game/player/player_logic.h"
|
||||||
#include "../card_menu_action_type.h"
|
#include "../card_menu_action_type.h"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#include "player_menu.h"
|
#include "player_menu.h"
|
||||||
|
|
||||||
|
#include "../../../client/settings/shortcuts_settings.h"
|
||||||
#include "../../../game_graphics/zones/hand_zone.h"
|
#include "../../../game_graphics/zones/hand_zone.h"
|
||||||
#include "../../../game_graphics/zones/pile_zone.h"
|
#include "../../../game_graphics/zones/pile_zone.h"
|
||||||
#include "../../../game_graphics/zones/table_zone.h"
|
#include "../../../game_graphics/zones/table_zone.h"
|
||||||
|
|
@ -44,6 +45,8 @@ PlayerMenu::PlayerMenu(PlayerGraphicsItem *_player) : QObject(_player), player(_
|
||||||
utilityMenu = nullptr;
|
utilityMenu = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tallyMenu = addManagedMenu<TallyMenu>();
|
||||||
|
|
||||||
if (player->getLogic()->getPlayerInfo()->getLocal()) {
|
if (player->getLogic()->getPlayerInfo()->getLocal()) {
|
||||||
sayMenu = addManagedMenu<SayMenu>(player);
|
sayMenu = addManagedMenu<SayMenu>(player);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,13 @@
|
||||||
#include "rfg_menu.h"
|
#include "rfg_menu.h"
|
||||||
#include "say_menu.h"
|
#include "say_menu.h"
|
||||||
#include "sideboard_menu.h"
|
#include "sideboard_menu.h"
|
||||||
|
#include "tally_menu.h"
|
||||||
#include "utility_menu.h"
|
#include "utility_menu.h"
|
||||||
|
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
#include <libcockatrice/utility/card_ref.h>
|
||||||
|
|
||||||
class CardItem;
|
class CardItem;
|
||||||
class CardMenu;
|
class CardMenu;
|
||||||
|
|
@ -87,6 +89,7 @@ private:
|
||||||
GraveyardMenu *graveMenu;
|
GraveyardMenu *graveMenu;
|
||||||
RfgMenu *rfgMenu;
|
RfgMenu *rfgMenu;
|
||||||
UtilityMenu *utilityMenu;
|
UtilityMenu *utilityMenu;
|
||||||
|
TallyMenu *tallyMenu;
|
||||||
SayMenu *sayMenu;
|
SayMenu *sayMenu;
|
||||||
CustomZoneMenu *customZonesMenu;
|
CustomZoneMenu *customZonesMenu;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#include "pt_menu.h"
|
#include "pt_menu.h"
|
||||||
|
|
||||||
|
#include "../../../client/settings/shortcuts_settings.h"
|
||||||
#include "../../game/player/player_actions.h"
|
#include "../../game/player/player_actions.h"
|
||||||
#include "../../game/player/player_logic.h"
|
#include "../../game/player/player_logic.h"
|
||||||
#include "../player_graphics_item.h"
|
#include "../player_graphics_item.h"
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
#include "../../game/player/player_logic.h"
|
#include "../../game/player/player_logic.h"
|
||||||
#include "../player_graphics_item.h"
|
#include "../player_graphics_item.h"
|
||||||
|
|
||||||
|
#include <libcockatrice/settings/message_settings.h>
|
||||||
SayMenu::SayMenu(PlayerGraphicsItem *_player) : player(_player)
|
SayMenu::SayMenu(PlayerGraphicsItem *_player) : player(_player)
|
||||||
{
|
{
|
||||||
connect(&SettingsCache::instance().messages(), &MessageSettings::messageMacrosChanged, this, &SayMenu::initSayMenu);
|
connect(&SettingsCache::instance().messages(), &MessageSettings::messageMacrosChanged, this, &SayMenu::initSayMenu);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#include "sideboard_menu.h"
|
#include "sideboard_menu.h"
|
||||||
|
|
||||||
|
#include "../../../client/settings/shortcuts_settings.h"
|
||||||
#include "../../game/player/player_actions.h"
|
#include "../../game/player/player_actions.h"
|
||||||
#include "../../game/player/player_logic.h"
|
#include "../../game/player/player_logic.h"
|
||||||
#include "../player_graphics_item.h"
|
#include "../player_graphics_item.h"
|
||||||
|
|
|
||||||
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 "utility_menu.h"
|
||||||
|
|
||||||
|
#include "../../../client/settings/shortcuts_settings.h"
|
||||||
#include "../../../interface/deck_loader/deck_loader.h"
|
#include "../../../interface/deck_loader/deck_loader.h"
|
||||||
#include "../../game/player/player_actions.h"
|
#include "../../game/player/player_actions.h"
|
||||||
#include "../../game/player/player_logic.h"
|
#include "../../game/player/player_logic.h"
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
#include <QInputDialog>
|
#include <QInputDialog>
|
||||||
#include <libcockatrice/card/relation/card_relation.h>
|
#include <libcockatrice/card/relation/card_relation.h>
|
||||||
|
#include <libcockatrice/utility/string_limits.h>
|
||||||
|
|
||||||
PlayerDialogs::PlayerDialogs(PlayerGraphicsItem *_player, PlayerActions *_playerActions)
|
PlayerDialogs::PlayerDialogs(PlayerGraphicsItem *_player, PlayerActions *_playerActions)
|
||||||
: QObject(_player), player(_player), playerActions(_playerActions)
|
: QObject(_player), player(_player), playerActions(_playerActions)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
#include "player_graphics_item.h"
|
#include "player_graphics_item.h"
|
||||||
|
|
||||||
#include "../../game/player/player_actions.h"
|
#include "../../game/player/player_actions.h"
|
||||||
|
#include "../../interface/card_picture_loader/card_picture_loader.h"
|
||||||
|
#include "../../interface/widgets/cards/art_crop_attribution.h"
|
||||||
|
#include "../../interface/widgets/playmat/playmat_utils.h"
|
||||||
#include "../../interface/widgets/tabs/tab_game.h"
|
#include "../../interface/widgets/tabs/tab_game.h"
|
||||||
#include "../board/abstract_card_item.h"
|
#include "../board/abstract_card_item.h"
|
||||||
#include "../board/counter_general.h"
|
#include "../board/counter_general.h"
|
||||||
|
|
@ -13,12 +16,16 @@
|
||||||
#include "player_dialogs.h"
|
#include "player_dialogs.h"
|
||||||
|
|
||||||
#include <QGraphicsView>
|
#include <QGraphicsView>
|
||||||
|
#include <libcockatrice/card/database/card_database_manager.h>
|
||||||
|
#include <libcockatrice/deck_list/deck_list.h>
|
||||||
|
#include <libcockatrice/deck_list/playmat_resolver.h>
|
||||||
|
#include <libcockatrice/settings/interface_settings.h>
|
||||||
|
|
||||||
PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player)
|
PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player)
|
||||||
{
|
{
|
||||||
connect(&SettingsCache::instance(), &SettingsCache::horizontalHandChanged, this,
|
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::horizontalHandChanged, this,
|
||||||
&PlayerGraphicsItem::rearrangeZones);
|
&PlayerGraphicsItem::rearrangeZones);
|
||||||
connect(&SettingsCache::instance(), &SettingsCache::handJustificationChanged, this,
|
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::handJustificationChanged, this,
|
||||||
&PlayerGraphicsItem::rearrangeZones);
|
&PlayerGraphicsItem::rearrangeZones);
|
||||||
connect(player, &PlayerLogic::rearrangeCounters, this, &PlayerGraphicsItem::rearrangeCounters);
|
connect(player, &PlayerLogic::rearrangeCounters, this, &PlayerGraphicsItem::rearrangeCounters);
|
||||||
connect(player, &PlayerLogic::activeChanged, this, &PlayerGraphicsItem::onPlayerActiveChanged);
|
connect(player, &PlayerLogic::activeChanged, this, &PlayerGraphicsItem::onPlayerActiveChanged);
|
||||||
|
|
@ -27,6 +34,10 @@ PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player)
|
||||||
|
|
||||||
connect(player, &PlayerLogic::counterAdded, this, &PlayerGraphicsItem::onCounterAdded);
|
connect(player, &PlayerLogic::counterAdded, this, &PlayerGraphicsItem::onCounterAdded);
|
||||||
connect(player, &PlayerLogic::counterRemoved, this, &PlayerGraphicsItem::onCounterRemoved);
|
connect(player, &PlayerLogic::counterRemoved, this, &PlayerGraphicsItem::onCounterRemoved);
|
||||||
|
connect(player, &PlayerLogic::deckChanged, this, &PlayerGraphicsItem::updatePlaymat);
|
||||||
|
connect(player, &PlayerLogic::playmatChanged, this, &PlayerGraphicsItem::updatePlaymat);
|
||||||
|
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::playmatVisibilityChanged, this,
|
||||||
|
[this](int) { updatePlaymat(); });
|
||||||
|
|
||||||
playerMenu = new PlayerMenu(this);
|
playerMenu = new PlayerMenu(this);
|
||||||
|
|
||||||
|
|
@ -66,6 +77,9 @@ PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player)
|
||||||
|
|
||||||
connect(tableZoneGraphicsItem, &TableZone::sizeChanged, this, &PlayerGraphicsItem::updateBoundingRect);
|
connect(tableZoneGraphicsItem, &TableZone::sizeChanged, this, &PlayerGraphicsItem::updateBoundingRect);
|
||||||
|
|
||||||
|
connect(this, &PlayerGraphicsItem::playmatChanged, tableZoneGraphicsItem, &TableZone::onPlaymatChanged);
|
||||||
|
connect(this, &PlayerGraphicsItem::playmatChanged, stackZoneGraphicsItem, &StackZone::onPlaymatChanged);
|
||||||
|
|
||||||
updateBoundingRect();
|
updateBoundingRect();
|
||||||
|
|
||||||
rearrangeZones();
|
rearrangeZones();
|
||||||
|
|
@ -111,7 +125,6 @@ void PlayerGraphicsItem::initializeZones()
|
||||||
rfgZoneGraphicsItem->setPos(base + QPointF(0, 2 * h + h2 + 10));
|
rfgZoneGraphicsItem->setPos(base + QPointF(0, 2 * h + h2 + 10));
|
||||||
|
|
||||||
tableZoneGraphicsItem = new TableZone(player->getTableZone(), mirrored, this);
|
tableZoneGraphicsItem = new TableZone(player->getTableZone(), mirrored, this);
|
||||||
connect(tableZoneGraphicsItem, &TableZone::sizeChanged, this, &PlayerGraphicsItem::updateBoundingRect);
|
|
||||||
connect(this, &PlayerGraphicsItem::mirroredChanged, tableZoneGraphicsItem, &TableZone::setMirrored);
|
connect(this, &PlayerGraphicsItem::mirroredChanged, tableZoneGraphicsItem, &TableZone::setMirrored);
|
||||||
|
|
||||||
stackZoneGraphicsItem =
|
stackZoneGraphicsItem =
|
||||||
|
|
@ -148,16 +161,67 @@ qreal PlayerGraphicsItem::getMinimumWidth() const
|
||||||
{
|
{
|
||||||
qreal result = tableZoneGraphicsItem->getMinimumWidth() + CardDimensions::HEIGHT_F + 15 + counterAreaWidth +
|
qreal result = tableZoneGraphicsItem->getMinimumWidth() + CardDimensions::HEIGHT_F + 15 + counterAreaWidth +
|
||||||
stackZoneGraphicsItem->boundingRect().width();
|
stackZoneGraphicsItem->boundingRect().width();
|
||||||
if (!SettingsCache::instance().getHorizontalHand()) {
|
if (!SettingsCache::instance().userInterface().getHorizontalHand()) {
|
||||||
result += handZoneGraphicsItem->boundingRect().width();
|
result += handZoneGraphicsItem->boundingRect().width();
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlayerGraphicsItem::paint(QPainter * /*painter*/,
|
void PlayerGraphicsItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *)
|
||||||
const QStyleOptionGraphicsItem * /*option*/,
|
|
||||||
QWidget * /*widget*/)
|
|
||||||
{
|
{
|
||||||
|
if (!hasPlaymat || playmatPixmap.isNull()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the combined bounding rect of stack + table zones
|
||||||
|
QPointF stackPos = stackZoneGraphicsItem->pos();
|
||||||
|
QPointF tablePos = tableZoneGraphicsItem->pos();
|
||||||
|
QSizeF stackSize = stackZoneGraphicsItem->boundingRect().size();
|
||||||
|
QSizeF tableSize = tableZoneGraphicsItem->boundingRect().size();
|
||||||
|
|
||||||
|
// Combined area: from stack left edge to table right edge
|
||||||
|
double combinedLeft = qMin(stackPos.x(), tablePos.x());
|
||||||
|
double combinedTop = qMin(stackPos.y(), tablePos.y());
|
||||||
|
double combinedRight = qMax(stackPos.x() + stackSize.width(), tablePos.x() + tableSize.width());
|
||||||
|
double combinedBottom = qMax(stackPos.y() + stackSize.height(), tablePos.y() + tableSize.height());
|
||||||
|
|
||||||
|
QRectF combinedArea(combinedLeft, combinedTop, combinedRight - combinedLeft, combinedBottom - combinedTop);
|
||||||
|
|
||||||
|
const QRectF srcRect = computeArtSourceRect(playmatPixmap.size(), playmatParams);
|
||||||
|
const QRectF dstRect = coverFitRect(combinedArea, srcRect.size());
|
||||||
|
|
||||||
|
painter->save();
|
||||||
|
painter->setClipRect(combinedArea);
|
||||||
|
painter->setRenderHint(QPainter::SmoothPixmapTransform, true);
|
||||||
|
|
||||||
|
// Render from a down-scaled copy of the art so the full-resolution source
|
||||||
|
// pixmap is never re-sampled at a tiny device size (also much cheaper than
|
||||||
|
// scaling it on every frame).
|
||||||
|
const QPixmap scaledPixmap = scaledPlaymatFor(srcRect, painter->worldTransform().mapRect(dstRect).size());
|
||||||
|
painter->drawPixmap(dstRect, scaledPixmap, QRectF(scaledPixmap.rect()));
|
||||||
|
|
||||||
|
painter->restore();
|
||||||
|
|
||||||
|
if (!playmatAttribution.isEmpty()) {
|
||||||
|
paintArtAttribution(*painter, combinedArea, playmatAttribution, Qt::AlignRight | Qt::AlignBottom, 0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QPixmap PlayerGraphicsItem::scaledPlaymatFor(const QRectF &srcRect, const QSizeF &deviceDstSize)
|
||||||
|
{
|
||||||
|
// Bucket the render size so the source pixmap is re-scaled at most once per
|
||||||
|
// zoom step instead of once per frame.
|
||||||
|
constexpr int bucketSize = 32;
|
||||||
|
const QSize target = QSize(qMax(1, qRound(deviceDstSize.width() / bucketSize) * bucketSize),
|
||||||
|
qMax(1, qRound(deviceDstSize.height() / bucketSize) * bucketSize))
|
||||||
|
.boundedTo(srcRect.toAlignedRect().size());
|
||||||
|
|
||||||
|
if (scaledPlaymatKey != target) {
|
||||||
|
const QPixmap crop = playmatPixmap.copy(srcRect.toAlignedRect());
|
||||||
|
scaledPlaymatPixmap = crop.scaled(target, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||||
|
scaledPlaymatKey = target;
|
||||||
|
}
|
||||||
|
return scaledPlaymatPixmap;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlayerGraphicsItem::processSceneSizeChange(int newPlayerWidth)
|
void PlayerGraphicsItem::processSceneSizeChange(int newPlayerWidth)
|
||||||
|
|
@ -165,7 +229,7 @@ void PlayerGraphicsItem::processSceneSizeChange(int newPlayerWidth)
|
||||||
// Extend table (and hand, if horizontal) to accommodate the new player width.
|
// Extend table (and hand, if horizontal) to accommodate the new player width.
|
||||||
qreal tableWidth = newPlayerWidth - CardDimensions::HEIGHT_F - 15 - counterAreaWidth -
|
qreal tableWidth = newPlayerWidth - CardDimensions::HEIGHT_F - 15 - counterAreaWidth -
|
||||||
stackZoneGraphicsItem->boundingRect().width();
|
stackZoneGraphicsItem->boundingRect().width();
|
||||||
if (!SettingsCache::instance().getHorizontalHand()) {
|
if (!SettingsCache::instance().userInterface().getHorizontalHand()) {
|
||||||
tableWidth -= handZoneGraphicsItem->boundingRect().width();
|
tableWidth -= handZoneGraphicsItem->boundingRect().width();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -187,6 +251,11 @@ void PlayerGraphicsItem::onCounterAdded(CounterState *state)
|
||||||
AbstractCounter *widget;
|
AbstractCounter *widget;
|
||||||
if (state->getName() == "life") {
|
if (state->getName() == "life") {
|
||||||
widget = playerTarget->addCounter(state);
|
widget = playerTarget->addCounter(state);
|
||||||
|
connect(state, &CounterState::valueChanged, this, [this](int oldValue, int newValue) {
|
||||||
|
if (newValue < oldValue) {
|
||||||
|
tableZoneGraphicsItem->triggerDamageShimmer();
|
||||||
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
widget = new GeneralCounter(state, player, true, this);
|
widget = new GeneralCounter(state, player, true, this);
|
||||||
}
|
}
|
||||||
|
|
@ -233,7 +302,7 @@ void PlayerGraphicsItem::rearrangeCounters()
|
||||||
void PlayerGraphicsItem::rearrangeZones()
|
void PlayerGraphicsItem::rearrangeZones()
|
||||||
{
|
{
|
||||||
auto base = QPointF(CardDimensions::HEIGHT_F + counterAreaWidth + 15, 0);
|
auto base = QPointF(CardDimensions::HEIGHT_F + counterAreaWidth + 15, 0);
|
||||||
if (SettingsCache::instance().getHorizontalHand()) {
|
if (SettingsCache::instance().userInterface().getHorizontalHand()) {
|
||||||
if (mirrored) {
|
if (mirrored) {
|
||||||
if (player->getHandZone()->contentsKnown()) {
|
if (player->getHandZone()->contentsKnown()) {
|
||||||
handVisible = true;
|
handVisible = true;
|
||||||
|
|
@ -284,7 +353,7 @@ void PlayerGraphicsItem::updateBoundingRect()
|
||||||
{
|
{
|
||||||
prepareGeometryChange();
|
prepareGeometryChange();
|
||||||
qreal width = CardDimensions::HEIGHT_F + 15 + counterAreaWidth + stackZoneGraphicsItem->boundingRect().width();
|
qreal width = CardDimensions::HEIGHT_F + 15 + counterAreaWidth + stackZoneGraphicsItem->boundingRect().width();
|
||||||
if (SettingsCache::instance().getHorizontalHand()) {
|
if (SettingsCache::instance().userInterface().getHorizontalHand()) {
|
||||||
qreal handHeight = handVisible ? handZoneGraphicsItem->boundingRect().height() : 0;
|
qreal handHeight = handVisible ? handZoneGraphicsItem->boundingRect().height() : 0;
|
||||||
bRect = QRectF(0, 0, width + tableZoneGraphicsItem->boundingRect().width(),
|
bRect = QRectF(0, 0, width + tableZoneGraphicsItem->boundingRect().width(),
|
||||||
tableZoneGraphicsItem->boundingRect().height() + handHeight);
|
tableZoneGraphicsItem->boundingRect().height() + handHeight);
|
||||||
|
|
@ -297,3 +366,100 @@ void PlayerGraphicsItem::updateBoundingRect()
|
||||||
|
|
||||||
emit sizeChanged();
|
emit sizeChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PlayerGraphicsItem::updatePlaymat()
|
||||||
|
{
|
||||||
|
int visibility = SettingsCache::instance().userInterface().getPlaymatVisibility();
|
||||||
|
|
||||||
|
// "Don't use playmats" — never show
|
||||||
|
if (visibility == PlaymatVisibilityNone) {
|
||||||
|
clearPlaymat();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Show own playmat only" — hide playmats for remote players
|
||||||
|
if (visibility == PlaymatVisibilityOwnOnly && !player->getPlayerInfo()->getLocal()) {
|
||||||
|
clearPlaymat();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CardRef playmatCard;
|
||||||
|
PlaymatParams params;
|
||||||
|
|
||||||
|
if (player->getHasRemotePlaymat()) {
|
||||||
|
// Prefer the server-confirmed playmat (updated by Command_SetPlaymat).
|
||||||
|
playmatCard = player->getRemotePlaymatCard();
|
||||||
|
params = player->getRemotePlaymatParams();
|
||||||
|
} else if (player->getPlayerInfo()->getLocal()) {
|
||||||
|
// Local player without a server broadcast yet: apply the full
|
||||||
|
// settings-based resolution chain (mode, fallback list, behavior).
|
||||||
|
const auto &settings = SettingsCache::instance().userInterface();
|
||||||
|
const PlaymatInfo resolved = resolvePlaymatForDeck(
|
||||||
|
player->getDeck(), settings.getPlaymatFallbackList(), static_cast<PlaymatMode>(settings.getPlaymatMode()),
|
||||||
|
static_cast<PlaymatFallbackMode>(settings.getPlaymatFallbackBehavior()), 0);
|
||||||
|
playmatCard = resolved.card;
|
||||||
|
params = resolved.params;
|
||||||
|
} else {
|
||||||
|
// Opponent without a server broadcast: use the deck-embedded playmat.
|
||||||
|
const DeckList &deck = player->getDeck();
|
||||||
|
const PlaymatInfo &deckPlaymat = deck.getPlaymat();
|
||||||
|
if (!deckPlaymat.card.isEmpty()) {
|
||||||
|
playmatCard = deckPlaymat.card;
|
||||||
|
params = deckPlaymat.params;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playmatCard.isEmpty()) {
|
||||||
|
clearPlaymat();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
playmatParams = params;
|
||||||
|
scaledPlaymatKey = QSize(); // the art crop depends on the params, drop any cached scale
|
||||||
|
|
||||||
|
ExactCard card = CardDatabaseManager::query()->getCard(playmatCard);
|
||||||
|
if (!card) {
|
||||||
|
clearPlaymat();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
playmatAttribution = buildArtAttribution(card);
|
||||||
|
|
||||||
|
QPixmap fullRes;
|
||||||
|
CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040));
|
||||||
|
|
||||||
|
if (fullRes.isNull()) {
|
||||||
|
disconnect(playmatPixmapConnection);
|
||||||
|
CardInfo *cardInfo = card.getCardPtr().data();
|
||||||
|
if (cardInfo) {
|
||||||
|
playmatPixmapConnection =
|
||||||
|
connect(cardInfo, &CardInfo::pixmapUpdated, this, &PlayerGraphicsItem::onPlaymatPixmapReady);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasPlaymat) {
|
||||||
|
hasPlaymat = true;
|
||||||
|
emit playmatChanged(true);
|
||||||
|
}
|
||||||
|
playmatPixmap = fullRes;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlayerGraphicsItem::clearPlaymat()
|
||||||
|
{
|
||||||
|
disconnect(playmatPixmapConnection);
|
||||||
|
playmatAttribution.clear();
|
||||||
|
if (hasPlaymat) {
|
||||||
|
hasPlaymat = false;
|
||||||
|
playmatPixmap = QPixmap();
|
||||||
|
scaledPlaymatKey = QSize();
|
||||||
|
emit playmatChanged(false);
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlayerGraphicsItem::onPlaymatPixmapReady()
|
||||||
|
{
|
||||||
|
updatePlaymat();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
#include "../game_scene.h"
|
#include "../game_scene.h"
|
||||||
|
|
||||||
#include <QGraphicsObject>
|
#include <QGraphicsObject>
|
||||||
|
#include <libcockatrice/deck_list/deck_list.h>
|
||||||
|
|
||||||
class HandZone;
|
class HandZone;
|
||||||
class PileZone;
|
class PileZone;
|
||||||
|
|
@ -126,6 +127,7 @@ signals:
|
||||||
void playerCountChanged();
|
void playerCountChanged();
|
||||||
void mirroredChanged(bool isMirrored);
|
void mirroredChanged(bool isMirrored);
|
||||||
void cardInfoRequested(const CardRef &cardRef);
|
void cardInfoRequested(const CardRef &cardRef);
|
||||||
|
void playmatChanged(bool hasPlaymat);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
PlayerLogic *player;
|
PlayerLogic *player;
|
||||||
|
|
@ -146,9 +148,23 @@ private:
|
||||||
bool mirrored;
|
bool mirrored;
|
||||||
bool handVisible = false;
|
bool handVisible = false;
|
||||||
|
|
||||||
|
QPixmap playmatPixmap;
|
||||||
|
QPixmap scaledPlaymatPixmap; // down-scaled copy of playmatPixmap for the current render size
|
||||||
|
QSize scaledPlaymatKey; // size bucket scaledPlaymatPixmap was rendered for
|
||||||
|
PlaymatParams playmatParams;
|
||||||
|
QString playmatAttribution;
|
||||||
|
bool hasPlaymat = false;
|
||||||
|
QMetaObject::Connection playmatPixmapConnection;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void updateBoundingRect();
|
void updateBoundingRect();
|
||||||
void rearrangeZones();
|
void rearrangeZones();
|
||||||
|
void clearPlaymat();
|
||||||
|
void updatePlaymat();
|
||||||
|
void onPlaymatPixmapReady();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QPixmap scaledPlaymatFor(const QRectF &srcRect, const QSizeF &deviceDstSize);
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // COCKATRICE_PLAYER_GRAPHICS_ITEM_H
|
#endif // COCKATRICE_PLAYER_GRAPHICS_ITEM_H
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,7 @@ bool PlayerListItemDelegate::editorEvent(QEvent *event,
|
||||||
if ((event->type() == QEvent::MouseButtonPress) && index.isValid()) {
|
if ((event->type() == QEvent::MouseButtonPress) && index.isValid()) {
|
||||||
auto *const mouseEvent = static_cast<QMouseEvent *>(event);
|
auto *const mouseEvent = static_cast<QMouseEvent *>(event);
|
||||||
if (mouseEvent->button() == Qt::RightButton) {
|
if (mouseEvent->button() == Qt::RightButton) {
|
||||||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
|
|
||||||
static_cast<PlayerListWidget *>(parent())->showContextMenu(mouseEvent->globalPosition().toPoint(), index);
|
static_cast<PlayerListWidget *>(parent())->showContextMenu(mouseEvent->globalPosition().toPoint(), index);
|
||||||
#else
|
|
||||||
static_cast<PlayerListWidget *>(parent())->showContextMenu(mouseEvent->globalPos(), index);
|
|
||||||
#endif
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
#include "player_target.h"
|
#include "player_target.h"
|
||||||
|
|
||||||
|
#include "../../client/settings/cache_settings.h"
|
||||||
#include "../../game/player/player_logic.h"
|
#include "../../game/player/player_logic.h"
|
||||||
#include "../../interface/pixel_map_generator.h"
|
#include "../../interface/pixel_map_generator.h"
|
||||||
|
#include "../game_scene.h"
|
||||||
|
|
||||||
|
#include <QApplication>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QPixmapCache>
|
#include <QPixmapCache>
|
||||||
|
|
@ -21,17 +24,24 @@ QRectF PlayerCounter::boundingRect() const
|
||||||
|
|
||||||
void PlayerCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
|
void PlayerCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
|
||||||
{
|
{
|
||||||
const int radius = 8;
|
const int radius = 15;
|
||||||
const qreal border = 1;
|
const qreal border = 1.5;
|
||||||
QPainterPath path(QPointF(50 - border / 2, border / 2));
|
// The box is drawn with a border-wide stroke straddling the path, so the
|
||||||
path.lineTo(radius, border / 2);
|
// visible outline spans [inset, inset + border]. Fills that must not cover
|
||||||
path.arcTo(border / 2, border / 2, 2 * radius, 2 * radius, 90, 90);
|
// the outline (e.g. the life-change flash) use a path inset by `border`.
|
||||||
path.lineTo(border / 2, 30 - border / 2);
|
const auto makePath = [](qreal inset) {
|
||||||
path.lineTo(50 - border / 2, 30 - border / 2);
|
QPainterPath path(QPointF(50 - inset, inset));
|
||||||
path.closeSubpath();
|
path.lineTo(radius, inset);
|
||||||
|
path.arcTo(inset, inset, 2 * radius, 2 * radius, 90, 90);
|
||||||
|
path.lineTo(inset, 30 - inset);
|
||||||
|
path.lineTo(50 - inset, 30 - inset);
|
||||||
|
path.closeSubpath();
|
||||||
|
return path;
|
||||||
|
};
|
||||||
|
QPainterPath path = makePath(border / 2);
|
||||||
|
|
||||||
QPen pen(QColor(100, 100, 100));
|
QPen pen(QColor(100, 100, 100));
|
||||||
pen.setWidth(border);
|
pen.setWidthF(border);
|
||||||
painter->setPen(pen);
|
painter->setPen(pen);
|
||||||
painter->setBrush(hovered ? QColor(50, 50, 50, 160) : QColor(0, 0, 0, 160));
|
painter->setBrush(hovered ? QColor(50, 50, 50, 160) : QColor(0, 0, 0, 160));
|
||||||
|
|
||||||
|
|
@ -45,6 +55,48 @@ void PlayerCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*
|
||||||
painter->setFont(font);
|
painter->setFont(font);
|
||||||
painter->setPen(Qt::white);
|
painter->setPen(Qt::white);
|
||||||
painter->drawText(translatedRect, Qt::AlignCenter, QString::number(value));
|
painter->drawText(translatedRect, Qt::AlignCenter, QString::number(value));
|
||||||
|
|
||||||
|
// Life-change flash: emerald on gain, red on loss, decaying over a few ticks.
|
||||||
|
if (flashAlpha > 0) {
|
||||||
|
painter->save();
|
||||||
|
QColor flashColor = flashDelta > 0 ? QColor(52, 224, 122) : QColor(239, 68, 68);
|
||||||
|
flashColor.setAlphaF(0.45 * flashAlpha);
|
||||||
|
painter->setPen(Qt::NoPen);
|
||||||
|
painter->setBrush(flashColor);
|
||||||
|
painter->setOpacity(0.85);
|
||||||
|
painter->drawPath(makePath(border));
|
||||||
|
painter->restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlayerCounter::onValueChanged(int oldValue, int newValue)
|
||||||
|
{
|
||||||
|
flashDelta = newValue - oldValue;
|
||||||
|
if (flashDelta == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SettingsCache::instance().userInterface().getLifeCounterAnimationsEnabled()) {
|
||||||
|
flashAlpha = 0.0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
flashAlpha = 1.0;
|
||||||
|
flashClock.start();
|
||||||
|
if (scene()) {
|
||||||
|
static_cast<GameScene *>(scene())->registerAnimationItem(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PlayerCounter::animationEvent()
|
||||||
|
{
|
||||||
|
flashAlpha = 1.0 - flashClock.elapsed() / flashDurationMs;
|
||||||
|
if (flashAlpha <= 0.0) {
|
||||||
|
flashAlpha = 0.0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
update();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
PlayerTarget::PlayerTarget(PlayerLogic *_owner, QGraphicsItem *parentItem)
|
PlayerTarget::PlayerTarget(PlayerLogic *_owner, QGraphicsItem *parentItem)
|
||||||
|
|
|
||||||
|
|
@ -7,21 +7,34 @@
|
||||||
#ifndef PLAYERTARGET_H
|
#ifndef PLAYERTARGET_H
|
||||||
#define PLAYERTARGET_H
|
#define PLAYERTARGET_H
|
||||||
|
|
||||||
|
#include "../animated_item.h"
|
||||||
#include "../board/abstract_counter.h"
|
#include "../board/abstract_counter.h"
|
||||||
#include "../board/arrow_target.h"
|
#include "../board/arrow_target.h"
|
||||||
#include "../board/graphics_item_type.h"
|
#include "../board/graphics_item_type.h"
|
||||||
|
|
||||||
|
#include <QElapsedTimer>
|
||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
|
|
||||||
class PlayerLogic;
|
class PlayerLogic;
|
||||||
|
|
||||||
class PlayerCounter : public AbstractCounter
|
class PlayerCounter : public AbstractCounter, public IAnimatedItem
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
protected:
|
||||||
|
void onValueChanged(int oldValue, int newValue) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr qreal flashDurationMs = 450.0;
|
||||||
|
|
||||||
|
QElapsedTimer flashClock;
|
||||||
|
qreal flashAlpha = 0.0;
|
||||||
|
int flashDelta = 0;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
PlayerCounter(CounterState *state, PlayerLogic *player, QGraphicsItem *parent);
|
PlayerCounter(CounterState *state, PlayerLogic *player, QGraphicsItem *parent);
|
||||||
QRectF boundingRect() const override;
|
QRectF boundingRect() const override;
|
||||||
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
|
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
|
||||||
|
bool animationEvent() override;
|
||||||
};
|
};
|
||||||
|
|
||||||
class PlayerTarget : public ArrowTarget
|
class PlayerTarget : public ArrowTarget
|
||||||
|
|
|
||||||
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
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
#include "selection_subtype_tally.h"
|
#include "subtype_tally.h"
|
||||||
|
|
||||||
#include "../game_graphics/board/card_item.h"
|
#include "../board/card_item.h"
|
||||||
|
|
||||||
#include <QMap>
|
#include <QMap>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
@ -19,12 +19,19 @@ QStringList extractSubtypesFromFace(const QString &faceType)
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @brief A single subtype (e.g., "Goblin", "Warrior") with its occurrence count. */
|
||||||
|
struct SubtypeEntry
|
||||||
|
{
|
||||||
|
QString name;
|
||||||
|
int count;
|
||||||
|
};
|
||||||
|
|
||||||
} // anonymous namespace
|
} // anonymous namespace
|
||||||
|
|
||||||
namespace SelectionSubtypeTally
|
namespace SubtypeTally
|
||||||
{
|
{
|
||||||
|
|
||||||
QList<SubtypeEntry> countSubtypes(const QList<CardItem *> &cards)
|
QList<TallyRow> countSubtypes(const QList<CardItem *> &cards)
|
||||||
{
|
{
|
||||||
QMap<QString, int> subtypeCounts;
|
QMap<QString, int> subtypeCounts;
|
||||||
|
|
||||||
|
|
@ -58,7 +65,13 @@ QList<SubtypeEntry> countSubtypes(const QList<CardItem *> &cards)
|
||||||
return a.name < b.name;
|
return a.name < b.name;
|
||||||
});
|
});
|
||||||
|
|
||||||
return entries;
|
// 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 SelectionSubtypeTally
|
} // 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 <QPainter>
|
||||||
#include <libcockatrice/protocol/pb/command_move_card.pb.h>
|
#include <libcockatrice/protocol/pb/command_move_card.pb.h>
|
||||||
|
#include <libcockatrice/settings/interface_settings.h>
|
||||||
|
|
||||||
HandZone::HandZone(HandZoneLogic *_logic, int _zoneHeight, QGraphicsItem *parent)
|
HandZone::HandZone(HandZoneLogic *_logic, int _zoneHeight, QGraphicsItem *parent)
|
||||||
: SelectZone(_logic, parent), zoneHeight(_zoneHeight)
|
: SelectZone(_logic, parent), zoneHeight(_zoneHeight)
|
||||||
|
|
@ -33,7 +34,7 @@ void HandZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
|
||||||
|
|
||||||
QPoint point = dropPoint + scenePos().toPoint();
|
QPoint point = dropPoint + scenePos().toPoint();
|
||||||
int x = -1;
|
int x = -1;
|
||||||
if (SettingsCache::instance().getHorizontalHand()) {
|
if (SettingsCache::instance().userInterface().getHorizontalHand()) {
|
||||||
for (x = 0; x < getLogic()->getCards().size(); x++) {
|
for (x = 0; x < getLogic()->getCards().size(); x++) {
|
||||||
if (point.x() < static_cast<CardItem *>(getLogic()->getCards().at(x))->scenePos().x()) {
|
if (point.x() < static_cast<CardItem *>(getLogic()->getCards().at(x))->scenePos().x()) {
|
||||||
break;
|
break;
|
||||||
|
|
@ -60,7 +61,7 @@ void HandZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
|
||||||
|
|
||||||
QRectF HandZone::boundingRect() const
|
QRectF HandZone::boundingRect() const
|
||||||
{
|
{
|
||||||
if (SettingsCache::instance().getHorizontalHand()) {
|
if (SettingsCache::instance().userInterface().getHorizontalHand()) {
|
||||||
return QRectF(0, 0, width, CardDimensions::HEIGHT_F + 10);
|
return QRectF(0, 0, width, CardDimensions::HEIGHT_F + 10);
|
||||||
} else {
|
} else {
|
||||||
return QRectF(0, 0, CardDimensions::WIDTH_F * 1.5, zoneHeight);
|
return QRectF(0, 0, CardDimensions::WIDTH_F * 1.5, zoneHeight);
|
||||||
|
|
@ -77,8 +78,8 @@ void HandZone::reorganizeCards()
|
||||||
{
|
{
|
||||||
if (!getLogic()->getCards().isEmpty()) {
|
if (!getLogic()->getCards().isEmpty()) {
|
||||||
const int cardCount = getLogic()->getCards().size();
|
const int cardCount = getLogic()->getCards().size();
|
||||||
if (SettingsCache::instance().getHorizontalHand()) {
|
if (SettingsCache::instance().userInterface().getHorizontalHand()) {
|
||||||
bool leftJustified = SettingsCache::instance().getLeftJustified();
|
bool leftJustified = SettingsCache::instance().userInterface().getLeftJustified();
|
||||||
qreal cardWidth = getLogic()->getCards().at(0)->boundingRect().width();
|
qreal cardWidth = getLogic()->getCards().at(0)->boundingRect().width();
|
||||||
const int xPadding = leftJustified ? cardWidth * 1.4 : 5;
|
const int xPadding = leftJustified ? cardWidth * 1.4 : 5;
|
||||||
qreal totalWidth =
|
qreal totalWidth =
|
||||||
|
|
@ -126,7 +127,7 @@ void HandZone::sortHand(const QList<CardList::SortOption> &options)
|
||||||
|
|
||||||
void HandZone::setWidth(qreal _width)
|
void HandZone::setWidth(qreal _width)
|
||||||
{
|
{
|
||||||
if (SettingsCache::instance().getHorizontalHand()) {
|
if (SettingsCache::instance().userInterface().getHorizontalHand()) {
|
||||||
prepareGeometryChange();
|
prepareGeometryChange();
|
||||||
width = _width;
|
width = _width;
|
||||||
reorganizeCards();
|
reorganizeCards();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#include "pile_zone.h"
|
#include "pile_zone.h"
|
||||||
|
|
||||||
|
#include "../../client/settings/cache_settings.h"
|
||||||
#include "../../game/player/player_actions.h"
|
#include "../../game/player/player_actions.h"
|
||||||
#include "../../game/player/player_logic.h"
|
#include "../../game/player/player_logic.h"
|
||||||
#include "../../game/zones/pile_zone_logic.h"
|
#include "../../game/zones/pile_zone_logic.h"
|
||||||
|
|
@ -11,6 +12,7 @@
|
||||||
#include <QGraphicsSceneMouseEvent>
|
#include <QGraphicsSceneMouseEvent>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <libcockatrice/protocol/pb/command_move_card.pb.h>
|
#include <libcockatrice/protocol/pb/command_move_card.pb.h>
|
||||||
|
#include <libcockatrice/settings/cards_display_settings.h>
|
||||||
|
|
||||||
PileZone::PileZone(PileZoneLogic *_logic, QGraphicsItem *parent) : CardZone(_logic, parent)
|
PileZone::PileZone(PileZoneLogic *_logic, QGraphicsItem *parent) : CardZone(_logic, parent)
|
||||||
{
|
{
|
||||||
|
|
@ -23,12 +25,13 @@ PileZone::PileZone(PileZoneLogic *_logic, QGraphicsItem *parent) : CardZone(_log
|
||||||
.rotate(90)
|
.rotate(90)
|
||||||
.translate(-CardDimensions::WIDTH_HALF_F, -CardDimensions::HEIGHT_HALF_F));
|
.translate(-CardDimensions::WIDTH_HALF_F, -CardDimensions::HEIGHT_HALF_F));
|
||||||
|
|
||||||
connect(&SettingsCache::instance(), &SettingsCache::roundCardCornersChanged, this, [this](bool _roundCardCorners) {
|
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::roundCardCornersChanged, this,
|
||||||
Q_UNUSED(_roundCardCorners);
|
[this](bool _roundCardCorners) {
|
||||||
|
Q_UNUSED(_roundCardCorners);
|
||||||
|
|
||||||
prepareGeometryChange();
|
prepareGeometryChange();
|
||||||
update();
|
update();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
QRectF PileZone::boundingRect() const
|
QRectF PileZone::boundingRect() const
|
||||||
|
|
@ -39,7 +42,8 @@ QRectF PileZone::boundingRect() const
|
||||||
QPainterPath PileZone::shape() const
|
QPainterPath PileZone::shape() const
|
||||||
{
|
{
|
||||||
QPainterPath shape;
|
QPainterPath shape;
|
||||||
qreal cardCornerRadius = SettingsCache::instance().getRoundCardCorners() ? 0.05 * CardDimensions::WIDTH_F : 0.0;
|
qreal cardCornerRadius =
|
||||||
|
SettingsCache::instance().cardsDisplay().getRoundCardCorners() ? 0.05 * CardDimensions::WIDTH_F : 0.0;
|
||||||
shape.addRoundedRect(boundingRect(), cardCornerRadius, cardCornerRadius);
|
shape.addRoundedRect(boundingRect(), cardCornerRadius, cardCornerRadius);
|
||||||
return shape;
|
return shape;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,11 @@
|
||||||
#include <QGraphicsRectItem>
|
#include <QGraphicsRectItem>
|
||||||
#include <QGraphicsSceneMouseEvent>
|
#include <QGraphicsSceneMouseEvent>
|
||||||
#include <QtMath>
|
#include <QtMath>
|
||||||
|
#include <libcockatrice/settings/cards_display_settings.h>
|
||||||
|
|
||||||
static qreal stackingOffset(qreal cardHeight)
|
static qreal stackingOffset(qreal cardHeight)
|
||||||
{
|
{
|
||||||
const qreal overlapPercent = SettingsCache::instance().getStackCardOverlapPercent();
|
const qreal overlapPercent = SettingsCache::instance().cardsDisplay().getStackCardOverlapPercent();
|
||||||
return cardHeight * (100.0 - overlapPercent) / 100.0;
|
return cardHeight * (100.0 - overlapPercent) / 100.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -21,40 +22,32 @@ SelectZone::ZoneLayout SelectZone::computeZoneLayout(const StackLayoutParams &pa
|
||||||
}
|
}
|
||||||
qreal effectiveOffset = params.desiredOffset;
|
qreal effectiveOffset = params.desiredOffset;
|
||||||
if (params.cardCount > 1) {
|
if (params.cardCount > 1) {
|
||||||
qreal fitOffset;
|
qreal reservedForBottomCard;
|
||||||
if (params.totalHeight < params.cardHeight && params.minOffset > 0.0) {
|
if (params.allowBottomOverflow) {
|
||||||
// Zone is shorter than a card (e.g. minimized). Compress offsets so
|
// Allow the bottom card to partially overflow in tight zones, scaling the
|
||||||
// every card has at least minOffset pixels of its top visible.
|
// overflow allowance by sqrt(cardCount-1) so offsets decrease smoothly
|
||||||
fitOffset = (params.totalHeight - params.minOffset) / (params.cardCount - 1);
|
// as cards are added rather than dropping by 1/(n-1) each time.
|
||||||
effectiveOffset = qMax(0.0, qMin(params.desiredOffset, fitOffset));
|
// The 0.75 ratio was tuned experimentally to balance card visibility vs. overflow.
|
||||||
|
constexpr qreal bottomCardZoneRatio = 0.75;
|
||||||
|
const qreal adjustedRatio = bottomCardZoneRatio / qSqrt(static_cast<qreal>(params.cardCount - 1));
|
||||||
|
reservedForBottomCard = qMin(params.cardHeight, params.totalHeight * adjustedRatio);
|
||||||
} else {
|
} else {
|
||||||
qreal reservedForBottomCard;
|
// No overflow: reserve full card height for the bottom card
|
||||||
if (params.allowBottomOverflow) {
|
reservedForBottomCard = params.cardHeight;
|
||||||
// Allow the bottom card to partially overflow in tight zones, scaling the
|
}
|
||||||
// overflow allowance by sqrt(cardCount-1) so offsets decrease smoothly
|
qreal fitOffset = (params.totalHeight - reservedForBottomCard) / (params.cardCount - 1);
|
||||||
// as cards are added rather than dropping by 1/(n-1) each time.
|
|
||||||
// The 0.75 ratio was tuned experimentally to balance card visibility vs. overflow.
|
|
||||||
constexpr qreal bottomCardZoneRatio = 0.75;
|
|
||||||
const qreal adjustedRatio = bottomCardZoneRatio / qSqrt(static_cast<qreal>(params.cardCount - 1));
|
|
||||||
reservedForBottomCard = qMin(params.cardHeight, params.totalHeight * adjustedRatio);
|
|
||||||
} else {
|
|
||||||
// No overflow: reserve full card height for the bottom card
|
|
||||||
reservedForBottomCard = params.cardHeight;
|
|
||||||
}
|
|
||||||
fitOffset = (params.totalHeight - reservedForBottomCard) / (params.cardCount - 1);
|
|
||||||
|
|
||||||
if (!params.allowBottomOverflow) {
|
if (!params.allowBottomOverflow) {
|
||||||
// Constrain offset so all card tops remain within zone bounds.
|
// Constrain offset so all card tops remain within zone bounds.
|
||||||
// With start=0, last card top at (cardCount-1) * effectiveOffset must be < totalHeight.
|
// With start=0, last card top at (cardCount-1) * effectiveOffset must be < totalHeight.
|
||||||
qreal maxOffsetForTops = params.totalHeight / (params.cardCount - 1);
|
qreal maxOffsetForTops = params.totalHeight / (params.cardCount - 1);
|
||||||
fitOffset = qMin(fitOffset, maxOffsetForTops);
|
fitOffset = qMin(fitOffset, maxOffsetForTops);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply minOffset only if it fits; otherwise compress further to keep all card tops visible.
|
// Apply minOffset only if it fits; otherwise compress further to keep all card tops visible.
|
||||||
effectiveOffset = qMin(params.desiredOffset, fitOffset);
|
effectiveOffset = qMin(params.desiredOffset, fitOffset);
|
||||||
if (fitOffset >= params.minOffset) {
|
if (fitOffset >= params.minOffset) {
|
||||||
effectiveOffset = qMax(params.minOffset, effectiveOffset);
|
effectiveOffset = qMax(params.minOffset, effectiveOffset);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
qreal stackHeight = (params.cardCount - 1) * effectiveOffset + params.cardHeight;
|
qreal stackHeight = (params.cardCount - 1) * effectiveOffset + params.cardHeight;
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,22 @@ QRectF StackZone::boundingRect() const
|
||||||
|
|
||||||
void StackZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
|
void StackZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
|
||||||
{
|
{
|
||||||
QBrush brush = themeManager->getExtraBgBrush(ThemeManager::Stack, getLogic()->getPlayer()->getZoneId());
|
if (playmatActive) {
|
||||||
painter->fillRect(boundingRect(), brush);
|
// Subtle overlay to distinguish stack zone from table zone (slightly darker)
|
||||||
|
painter->fillRect(boundingRect(), QColor(0, 0, 0, 80));
|
||||||
|
} else {
|
||||||
|
QBrush brush = themeManager->getExtraBgBrush(ThemeManager::Stack, getLogic()->getPlayer()->getZoneId());
|
||||||
|
painter->fillRect(boundingRect(), brush);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void StackZone::onPlaymatChanged(bool active)
|
||||||
|
{
|
||||||
|
playmatActive = active;
|
||||||
|
// See TableZone::onPlaymatChanged for the rationale. Translucent overlay
|
||||||
|
// over a dynamic playmat should not be held in the device cache.
|
||||||
|
setCacheMode(active ? QGraphicsItem::NoCache : QGraphicsItem::DeviceCoordinateCache);
|
||||||
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
void StackZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
|
void StackZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
|
||||||
|
|
@ -43,12 +57,18 @@ void StackZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int index = calcDropIndexFromY(dropPoint.y(), MIN_CARD_VISIBLE);
|
|
||||||
|
|
||||||
// Same-zone no-op: don't move a card onto itself
|
|
||||||
const auto &cards = getLogic()->getCards();
|
const auto &cards = getLogic()->getCards();
|
||||||
if (!cards.isEmpty() && startZone == getLogic() && cards.at(index)->getId() == dragItems.at(0)->getId()) {
|
int index;
|
||||||
return;
|
if (startZone == getLogic()) {
|
||||||
|
// Reordering within the zone: use drop position
|
||||||
|
index = calcDropIndexFromY(dropPoint.y(), MIN_CARD_VISIBLE);
|
||||||
|
// Same-zone no-op: don't move a card onto itself
|
||||||
|
if (!cards.isEmpty() && cards.at(index)->getId() == dragItems.at(0)->getId()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Coming from another zone: append at end (top of stack, rendered on top)
|
||||||
|
index = static_cast<int>(cards.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
Command_MoveCard cmd;
|
Command_MoveCard cmd;
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,13 @@ class StackZone : public SelectZone
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
private:
|
private:
|
||||||
qreal zoneHeight;
|
qreal zoneHeight;
|
||||||
|
bool playmatActive = false;
|
||||||
private slots:
|
private slots:
|
||||||
void updateBg();
|
void updateBg();
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void onPlaymatChanged(bool active);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
StackZone(StackZoneLogic *_logic, int _zoneHeight, QGraphicsItem *parent);
|
StackZone(StackZoneLogic *_logic, int _zoneHeight, QGraphicsItem *parent);
|
||||||
/** @brief Resizes the stack zone height, e.g. when sharing vertical space with the command zone. */
|
/** @brief Resizes the stack zone height, e.g. when sharing vertical space with the command zone. */
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
#include "../board/arrow_item.h"
|
#include "../board/arrow_item.h"
|
||||||
#include "../board/card_drag_item.h"
|
#include "../board/card_drag_item.h"
|
||||||
#include "../board/card_item.h"
|
#include "../board/card_item.h"
|
||||||
|
#include "../game_scene.h"
|
||||||
#include "../z_values.h"
|
#include "../z_values.h"
|
||||||
|
|
||||||
#include <QGraphicsScene>
|
#include <QGraphicsScene>
|
||||||
|
|
@ -15,6 +16,7 @@
|
||||||
#include <libcockatrice/card/card_info.h>
|
#include <libcockatrice/card/card_info.h>
|
||||||
#include <libcockatrice/protocol/pb/command_move_card.pb.h>
|
#include <libcockatrice/protocol/pb/command_move_card.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_set_card_attr.pb.h>
|
#include <libcockatrice/protocol/pb/command_set_card_attr.pb.h>
|
||||||
|
#include <libcockatrice/settings/interface_settings.h>
|
||||||
#include <libcockatrice/utility/zone_names.h>
|
#include <libcockatrice/utility/zone_names.h>
|
||||||
|
|
||||||
const QColor TableZone::BACKGROUND_COLOR = QColor(100, 100, 100);
|
const QColor TableZone::BACKGROUND_COLOR = QColor(100, 100, 100);
|
||||||
|
|
@ -28,7 +30,7 @@ TableZone::TableZone(TableZoneLogic *_logic, bool _mirrored, QGraphicsItem *pare
|
||||||
connect(_logic, &TableZoneLogic::contentSizeChanged, this, &TableZone::resizeToContents);
|
connect(_logic, &TableZoneLogic::contentSizeChanged, this, &TableZone::resizeToContents);
|
||||||
connect(_logic, &TableZoneLogic::toggleTapped, this, &TableZone::toggleTapped);
|
connect(_logic, &TableZoneLogic::toggleTapped, this, &TableZone::toggleTapped);
|
||||||
connect(themeManager, &ThemeManager::themeChanged, this, &TableZone::updateBg);
|
connect(themeManager, &ThemeManager::themeChanged, this, &TableZone::updateBg);
|
||||||
connect(&SettingsCache::instance(), &SettingsCache::invertVerticalCoordinateChanged, this,
|
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::invertVerticalCoordinateChanged, this,
|
||||||
&TableZone::reorganizeCards);
|
&TableZone::reorganizeCards);
|
||||||
|
|
||||||
updateBg();
|
updateBg();
|
||||||
|
|
@ -46,6 +48,31 @@ void TableZone::updateBg()
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TableZone::triggerDamageShimmer()
|
||||||
|
{
|
||||||
|
if (!SettingsCache::instance().userInterface().getBattlefieldFlashEnabled()) {
|
||||||
|
damageShimmerAlpha = 0.0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
damageShimmerAlpha = 1.0;
|
||||||
|
shimmerClock.start();
|
||||||
|
if (scene()) {
|
||||||
|
static_cast<GameScene *>(scene())->registerAnimationItem(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TableZone::animationEvent()
|
||||||
|
{
|
||||||
|
damageShimmerAlpha = 1.0 - shimmerClock.elapsed() / shimmerDurationMs;
|
||||||
|
if (damageShimmerAlpha <= 0.0) {
|
||||||
|
damageShimmerAlpha = 0.0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
update();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
QRectF TableZone::boundingRect() const
|
QRectF TableZone::boundingRect() const
|
||||||
{
|
{
|
||||||
return QRectF(0, 0, width, height);
|
return QRectF(0, 0, width, height);
|
||||||
|
|
@ -59,26 +86,49 @@ void TableZone::setMirrored(bool isMirrored)
|
||||||
|
|
||||||
bool TableZone::isInverted() const
|
bool TableZone::isInverted() const
|
||||||
{
|
{
|
||||||
return ((mirrored && !SettingsCache::instance().getInvertVerticalCoordinate()) ||
|
return ((mirrored && !SettingsCache::instance().userInterface().getInvertVerticalCoordinate()) ||
|
||||||
(!mirrored && SettingsCache::instance().getInvertVerticalCoordinate()));
|
(!mirrored && SettingsCache::instance().userInterface().getInvertVerticalCoordinate()));
|
||||||
}
|
}
|
||||||
|
|
||||||
void TableZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
|
void TableZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
|
||||||
{
|
{
|
||||||
QBrush brush = themeManager->getExtraBgBrush(ThemeManager::Table, getLogic()->getPlayer()->getZoneId());
|
if (playmatActive) {
|
||||||
painter->fillRect(boundingRect(), brush);
|
// Subtle overlay to distinguish table zone from stack zone
|
||||||
|
painter->fillRect(boundingRect(), QColor(0, 0, 0, 60));
|
||||||
|
} else {
|
||||||
|
QBrush brush = themeManager->getExtraBgBrush(ThemeManager::Table, getLogic()->getPlayer()->getZoneId());
|
||||||
|
painter->fillRect(boundingRect(), brush);
|
||||||
|
}
|
||||||
|
|
||||||
if (active) {
|
if (active) {
|
||||||
paintZoneOutline(painter);
|
paintZoneOutline(painter);
|
||||||
} else {
|
} else {
|
||||||
// inactive player gets a darker table zone with a semi transparent black mask
|
// inactive player gets a darker table zone with a semi transparent black mask
|
||||||
// this means if the user provides a custom background it will fade
|
// this means if the user provides a custom background or playmat it will fade
|
||||||
painter->fillRect(boundingRect(), FADE_MASK);
|
painter->fillRect(boundingRect(), FADE_MASK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Decaying crimson wash from taking damage.
|
||||||
|
if (damageShimmerAlpha > 0.0) {
|
||||||
|
QColor shimmerColor(239, 68, 68);
|
||||||
|
shimmerColor.setAlphaF(0.22 * damageShimmerAlpha);
|
||||||
|
painter->fillRect(boundingRect(), shimmerColor);
|
||||||
|
}
|
||||||
|
|
||||||
paintLandDivider(painter);
|
paintLandDivider(painter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TableZone::onPlaymatChanged(bool active)
|
||||||
|
{
|
||||||
|
playmatActive = active;
|
||||||
|
// While a playmat is shown the zone paints a translucent overlay over the
|
||||||
|
// dynamic playmat behind it. Keep it out of the device cache so the cached
|
||||||
|
// pixels are never stale relative to the playmat (and to avoid compositing
|
||||||
|
// artifacts of cached translucent content on some platforms).
|
||||||
|
setCacheMode(active ? QGraphicsItem::NoCache : QGraphicsItem::DeviceCoordinateCache);
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Render a soft outline around the edge of the TableZone.
|
Render a soft outline around the edge of the TableZone.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,16 +8,19 @@
|
||||||
#define TABLEZONE_H
|
#define TABLEZONE_H
|
||||||
|
|
||||||
#include "../../game/zones/table_zone_logic.h"
|
#include "../../game/zones/table_zone_logic.h"
|
||||||
|
#include "../animated_item.h"
|
||||||
#include "../board/abstract_card_item.h"
|
#include "../board/abstract_card_item.h"
|
||||||
#include "select_zone.h"
|
#include "select_zone.h"
|
||||||
|
|
||||||
|
#include <QElapsedTimer>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief TableZone is the grid based rect where CardItems may be placed.
|
* @brief TableZone is the grid based rect where CardItems may be placed.
|
||||||
*
|
*
|
||||||
* It is the main play zone and can be customized with background images.
|
* It is the main play zone and can be customized with background images.
|
||||||
*/
|
*/
|
||||||
//! \todo Refactor methods to make more readable, extract logic to private methods (especially reorganizeCards()).
|
//! \todo Refactor methods to make more readable, extract logic to private methods (especially reorganizeCards()).
|
||||||
class TableZone : public SelectZone
|
class TableZone : public SelectZone, public IAnimatedItem
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
|
|
@ -83,6 +86,7 @@ private:
|
||||||
*/
|
*/
|
||||||
bool active = false;
|
bool active = false;
|
||||||
bool mirrored = false;
|
bool mirrored = false;
|
||||||
|
bool playmatActive = false;
|
||||||
|
|
||||||
[[nodiscard]] bool isInverted() const;
|
[[nodiscard]] bool isInverted() const;
|
||||||
|
|
||||||
|
|
@ -92,6 +96,9 @@ private slots:
|
||||||
*/
|
*/
|
||||||
void updateBg();
|
void updateBg();
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void onPlaymatChanged(bool active);
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
/**
|
/**
|
||||||
Reorganizes CardItems in the TableZone
|
Reorganizes CardItems in the TableZone
|
||||||
|
|
@ -121,6 +128,16 @@ public:
|
||||||
*/
|
*/
|
||||||
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
|
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
Flashes the table surface after a player loses life.
|
||||||
|
|
||||||
|
Wired up through the life counter so the battlefield glows when life drops.
|
||||||
|
*/
|
||||||
|
void triggerDamageShimmer();
|
||||||
|
|
||||||
|
/** @brief Decays the damage shimmer by one timer tick. */
|
||||||
|
bool animationEvent() override;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Toggles the selected items as tapped.
|
Toggles the selected items as tapped.
|
||||||
*/
|
*/
|
||||||
|
|
@ -171,8 +188,17 @@ public:
|
||||||
}
|
}
|
||||||
void setWidth(qreal _width)
|
void setWidth(qreal _width)
|
||||||
{
|
{
|
||||||
|
// The width is stored as an int; truncate to match the previous implicit conversion.
|
||||||
|
const int newWidth = static_cast<int>(_width);
|
||||||
|
if (width == newWidth) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
prepareGeometryChange();
|
prepareGeometryChange();
|
||||||
width = _width;
|
width = newWidth;
|
||||||
|
// The parent player item's boundingRect (which clips the playmat painting) is
|
||||||
|
// derived from this zone's size. Without this signal the playmat is cut off at
|
||||||
|
// the stale boundingRect edge whenever the scene is resized wider.
|
||||||
|
emit sizeChanged();
|
||||||
}
|
}
|
||||||
[[nodiscard]] qreal getWidth() const
|
[[nodiscard]] qreal getWidth() const
|
||||||
{
|
{
|
||||||
|
|
@ -185,6 +211,11 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
static constexpr qreal shimmerDurationMs = 450.0;
|
||||||
|
|
||||||
|
QElapsedTimer shimmerClock;
|
||||||
|
qreal damageShimmerAlpha = 0.0;
|
||||||
|
|
||||||
void paintZoneOutline(QPainter *painter);
|
void paintZoneOutline(QPainter *painter);
|
||||||
void paintLandDivider(QPainter *painter);
|
void paintLandDivider(QPainter *painter);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
#include <QStyle>
|
#include <QStyle>
|
||||||
#include <QStyleOption>
|
#include <QStyleOption>
|
||||||
#include <libcockatrice/protocol/pb/command_shuffle.pb.h>
|
#include <libcockatrice/protocol/pb/command_shuffle.pb.h>
|
||||||
|
#include <libcockatrice/settings/interface_settings.h>
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
|
@ -65,7 +66,7 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player,
|
||||||
|
|
||||||
connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(&searchEdit); });
|
connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(&searchEdit); });
|
||||||
|
|
||||||
if (SettingsCache::instance().getFocusCardViewSearchBar()) {
|
if (SettingsCache::instance().userInterface().getFocusCardViewSearchBar()) {
|
||||||
this->setActive(true);
|
this->setActive(true);
|
||||||
searchEdit.setFocus();
|
searchEdit.setFocus();
|
||||||
}
|
}
|
||||||
|
|
@ -76,9 +77,9 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player,
|
||||||
vbox->addItem(searchEditProxy);
|
vbox->addItem(searchEditProxy);
|
||||||
|
|
||||||
// hide search bar if chat autofocus setting is enabled, since typing into it will no longer work anyway
|
// hide search bar if chat autofocus setting is enabled, since typing into it will no longer work anyway
|
||||||
searchEditProxy->setVisible(!SettingsCache::instance().getKeepGameChatFocus());
|
searchEditProxy->setVisible(!SettingsCache::instance().userInterface().getKeepGameChatFocus());
|
||||||
connect(&SettingsCache::instance(), &SettingsCache::keepGameChatFocusChanged, searchEditProxy,
|
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::keepGameChatFocusChanged,
|
||||||
[searchEditProxy](bool keepFocus) { searchEditProxy->setVisible(!keepFocus); });
|
searchEditProxy, [searchEditProxy](bool keepFocus) { searchEditProxy->setVisible(!keepFocus); });
|
||||||
|
|
||||||
// top row
|
// top row
|
||||||
QGraphicsLinearLayout *hTopRow = new QGraphicsLinearLayout(Qt::Horizontal);
|
QGraphicsLinearLayout *hTopRow = new QGraphicsLinearLayout(Qt::Horizontal);
|
||||||
|
|
@ -158,9 +159,9 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player,
|
||||||
connect(&sortBySelector, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this,
|
connect(&sortBySelector, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this,
|
||||||
&ZoneViewWidget::processSortBy);
|
&ZoneViewWidget::processSortBy);
|
||||||
connect(&pileViewCheckBox, &QCheckBox::QT_STATE_CHANGED, this, &ZoneViewWidget::processSetPileView);
|
connect(&pileViewCheckBox, &QCheckBox::QT_STATE_CHANGED, this, &ZoneViewWidget::processSetPileView);
|
||||||
groupBySelector.setCurrentIndex(SettingsCache::instance().getZoneViewGroupByIndex());
|
groupBySelector.setCurrentIndex(SettingsCache::instance().userInterface().getZoneViewGroupByIndex());
|
||||||
sortBySelector.setCurrentIndex(SettingsCache::instance().getZoneViewSortByIndex());
|
sortBySelector.setCurrentIndex(SettingsCache::instance().userInterface().getZoneViewSortByIndex());
|
||||||
pileViewCheckBox.setChecked(SettingsCache::instance().getZoneViewPileView());
|
pileViewCheckBox.setChecked(SettingsCache::instance().userInterface().getZoneViewPileView());
|
||||||
|
|
||||||
if (CardList::NoSort == static_cast<CardList::SortOption>(groupBySelector.currentData().toInt())) {
|
if (CardList::NoSort == static_cast<CardList::SortOption>(groupBySelector.currentData().toInt())) {
|
||||||
pileViewCheckBox.setEnabled(false);
|
pileViewCheckBox.setEnabled(false);
|
||||||
|
|
@ -190,7 +191,7 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player,
|
||||||
void ZoneViewWidget::processGroupBy(int index)
|
void ZoneViewWidget::processGroupBy(int index)
|
||||||
{
|
{
|
||||||
auto option = static_cast<CardList::SortOption>(groupBySelector.itemData(index).toInt());
|
auto option = static_cast<CardList::SortOption>(groupBySelector.itemData(index).toInt());
|
||||||
SettingsCache::instance().setZoneViewGroupByIndex(index);
|
SettingsCache::instance().userInterface().setZoneViewGroupByIndex(index);
|
||||||
zone->setGroupBy(option);
|
zone->setGroupBy(option);
|
||||||
|
|
||||||
// disable pile view checkbox if we're not grouping by anything
|
// disable pile view checkbox if we're not grouping by anything
|
||||||
|
|
@ -214,13 +215,13 @@ void ZoneViewWidget::processSortBy(int index)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsCache::instance().setZoneViewSortByIndex(index);
|
SettingsCache::instance().userInterface().setZoneViewSortByIndex(index);
|
||||||
zone->setSortBy(option);
|
zone->setSortBy(option);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ZoneViewWidget::processSetPileView(QT_STATE_CHANGED_T value)
|
void ZoneViewWidget::processSetPileView(QT_STATE_CHANGED_T value)
|
||||||
{
|
{
|
||||||
SettingsCache::instance().setZoneViewPileView(value);
|
SettingsCache::instance().userInterface().setZoneViewPileView(value);
|
||||||
zone->setPileView(value);
|
zone->setPileView(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -477,7 +478,7 @@ static qreal rowsToHeight(int rows)
|
||||||
**/
|
**/
|
||||||
static qreal calcMaxInitialHeight()
|
static qreal calcMaxInitialHeight()
|
||||||
{
|
{
|
||||||
return rowsToHeight(SettingsCache::instance().getCardViewInitialRowsMax());
|
return rowsToHeight(SettingsCache::instance().userInterface().getCardViewInitialRowsMax());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -559,7 +560,7 @@ void ZoneViewWidget::initStyleOption(QStyleOption *option) const
|
||||||
void ZoneViewWidget::expandWindow()
|
void ZoneViewWidget::expandWindow()
|
||||||
{
|
{
|
||||||
qreal maxInitialHeight = calcMaxInitialHeight();
|
qreal maxInitialHeight = calcMaxInitialHeight();
|
||||||
qreal maxExpandedHeight = rowsToHeight(SettingsCache::instance().getCardViewExpandedRowsMax());
|
qreal maxExpandedHeight = rowsToHeight(SettingsCache::instance().userInterface().getCardViewExpandedRowsMax());
|
||||||
qreal height = rect().height() - extraHeight - 10;
|
qreal height = rect().height() - extraHeight - 10;
|
||||||
qreal maxHeight = maximumHeight() - extraHeight - 10;
|
qreal maxHeight = maximumHeight() - extraHeight - 10;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
#include "card_picture_loader.h"
|
#include "card_picture_loader.h"
|
||||||
|
|
||||||
#include "../../client/settings/cache_settings.h"
|
#include "../../client/settings/cache_settings.h"
|
||||||
|
#include "card_picture_loader_cache_method.h"
|
||||||
|
#include "card_picture_loader_local_schemes.h"
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QBuffer>
|
#include <QBuffer>
|
||||||
|
|
@ -16,16 +18,23 @@
|
||||||
#include <QStatusBar>
|
#include <QStatusBar>
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <libcockatrice/settings/cache_storage_settings.h>
|
||||||
|
#include <libcockatrice/settings/download_settings.h>
|
||||||
|
#include <libcockatrice/settings/paths_settings.h>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
// never cache more than 300 cards at once for a single deck
|
// never cache more than 300 cards at once for a single deck
|
||||||
#define CACHED_CARD_PER_DECK_MAX 300
|
#define CACHED_CARD_PER_DECK_MAX 300
|
||||||
|
|
||||||
|
// wait at least this long before retrying a card whose picture failed to load
|
||||||
|
static constexpr int RETRY_FAILED_CARDS_SECS = 300;
|
||||||
|
|
||||||
CardPictureLoader::CardPictureLoader() : QObject(nullptr)
|
CardPictureLoader::CardPictureLoader() : QObject(nullptr)
|
||||||
{
|
{
|
||||||
worker = new CardPictureLoaderWorker;
|
worker = new CardPictureLoaderWorker;
|
||||||
connect(&SettingsCache::instance(), &SettingsCache::picsPathChanged, this, &CardPictureLoader::picsPathChanged);
|
connect(&SettingsCache::instance().paths(), &PathsSettings::picsPathChanged, this,
|
||||||
connect(&SettingsCache::instance(), &SettingsCache::picDownloadChanged, this,
|
&CardPictureLoader::picsPathChanged);
|
||||||
|
connect(&SettingsCache::instance().downloads(), &DownloadSettings::picDownloadChanged, this,
|
||||||
&CardPictureLoader::picDownloadChanged);
|
&CardPictureLoader::picDownloadChanged);
|
||||||
|
|
||||||
qRegisterMetaType<ExactCard>();
|
qRegisterMetaType<ExactCard>();
|
||||||
|
|
@ -129,7 +138,14 @@ void CardPictureLoader::getPixmap(QPixmap &pixmap, const ExactCard &card, QSize
|
||||||
QPixmap bigPixmap;
|
QPixmap bigPixmap;
|
||||||
if (QPixmapCache::find(key, &bigPixmap)) {
|
if (QPixmapCache::find(key, &bigPixmap)) {
|
||||||
if (bigPixmap.isNull()) {
|
if (bigPixmap.isNull()) {
|
||||||
qCDebug(CardPictureLoaderLog) << "Cached pixmap for key" << key << "is NULL!";
|
getCardBackLoadingFailedPixmap(pixmap, size);
|
||||||
|
QDateTime failedAtTime = getInstance().failedAt.value(key);
|
||||||
|
if (!failedAtTime.isValid() ||
|
||||||
|
failedAtTime.addSecs(RETRY_FAILED_CARDS_SECS) < QDateTime::currentDateTime()) {
|
||||||
|
getInstance().failedAt.remove(key);
|
||||||
|
QPixmapCache::remove(key);
|
||||||
|
getInstance().worker->enqueueImageLoad(card);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -153,8 +169,10 @@ void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image)
|
||||||
QPixmap finalPixmap;
|
QPixmap finalPixmap;
|
||||||
|
|
||||||
if (image.isNull()) {
|
if (image.isNull()) {
|
||||||
|
getInstance().failedAt.insert(card.getPixmapCacheKey(), QDateTime::currentDateTime());
|
||||||
qCDebug(CardPictureLoaderLog) << "Caching NULL pixmap for" << card.getName();
|
qCDebug(CardPictureLoaderLog) << "Caching NULL pixmap for" << card.getName();
|
||||||
} else {
|
} else {
|
||||||
|
getInstance().failedAt.remove(card.getPixmapCacheKey());
|
||||||
if (card.getInfo().getUiAttributes().upsideDownArt) {
|
if (card.getInfo().getUiAttributes().upsideDownArt) {
|
||||||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 9, 0))
|
#if (QT_VERSION >= QT_VERSION_CHECK(6, 9, 0))
|
||||||
QImage mirrorImage = image.flipped(Qt::Horizontal | Qt::Vertical);
|
QImage mirrorImage = image.flipped(Qt::Horizontal | Qt::Vertical);
|
||||||
|
|
@ -169,7 +187,8 @@ void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image)
|
||||||
|
|
||||||
QPixmapCache::insert(card.getPixmapCacheKey(), finalPixmap);
|
QPixmapCache::insert(card.getPixmapCacheKey(), finalPixmap);
|
||||||
|
|
||||||
if (SettingsCache::instance().getCardPictureLoaderCacheMethod() ==
|
if (static_cast<CardPictureLoaderCacheMethod::CacheMethod>(
|
||||||
|
SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) ==
|
||||||
CardPictureLoaderCacheMethod::CacheMethod::FILESYSTEM_CACHE) {
|
CardPictureLoaderCacheMethod::CacheMethod::FILESYSTEM_CACHE) {
|
||||||
saveCardImageToLocalStorage(card, finalPixmap);
|
saveCardImageToLocalStorage(card, finalPixmap);
|
||||||
}
|
}
|
||||||
|
|
@ -177,8 +196,10 @@ void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image)
|
||||||
// imageLoaded should only be reached if the exactCard isn't already in cache.
|
// imageLoaded should only be reached if the exactCard isn't already in cache.
|
||||||
// (plus there's a deduplication mechanism in CardPictureLoaderWorker)
|
// (plus there's a deduplication mechanism in CardPictureLoaderWorker)
|
||||||
// It should be safe to connect the CardInfo here without worrying about redundant connections.
|
// It should be safe to connect the CardInfo here without worrying about redundant connections.
|
||||||
connect(card.getCardPtr().data(), &QObject::destroyed, this,
|
connect(card.getCardPtr().data(), &QObject::destroyed, this, [cacheKey = card.getPixmapCacheKey()] {
|
||||||
[cacheKey = card.getPixmapCacheKey()] { QPixmapCache::remove(cacheKey); });
|
QPixmapCache::remove(cacheKey);
|
||||||
|
getInstance().failedAt.remove(cacheKey);
|
||||||
|
});
|
||||||
|
|
||||||
card.emitPixmapUpdated();
|
card.emitPixmapUpdated();
|
||||||
}
|
}
|
||||||
|
|
@ -189,9 +210,9 @@ void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card, const
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QString picsRoot = SettingsCache::instance().getPicsPath();
|
const QString picsRoot = SettingsCache::instance().paths().getPicsPath();
|
||||||
CardPictureLoaderLocalSchemes::NamingScheme scheme =
|
CardPictureLoaderLocalSchemes::NamingScheme scheme = static_cast<CardPictureLoaderLocalSchemes::NamingScheme>(
|
||||||
SettingsCache::instance().getLocalCardImageStorageNamingScheme();
|
SettingsCache::instance().cacheStorage().getLocalCardImageStorageNamingScheme());
|
||||||
|
|
||||||
QString pattern;
|
QString pattern;
|
||||||
|
|
||||||
|
|
@ -306,7 +327,7 @@ void CardPictureLoader::picsPathChanged()
|
||||||
|
|
||||||
bool CardPictureLoader::hasCustomArt()
|
bool CardPictureLoader::hasCustomArt()
|
||||||
{
|
{
|
||||||
auto picsPath = SettingsCache::instance().getPicsPath();
|
auto picsPath = SettingsCache::instance().paths().getPicsPath();
|
||||||
QDirIterator it(picsPath, QDir::Dirs | QDir::NoDotAndDotDot);
|
QDirIterator it(picsPath, QDir::Dirs | QDir::NoDotAndDotDot);
|
||||||
|
|
||||||
// Check if there is at least one non-directory file in the pics path, other
|
// Check if there is at least one non-directory file in the pics path, other
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@
|
||||||
#include "card_picture_loader_status_bar.h"
|
#include "card_picture_loader_status_bar.h"
|
||||||
#include "card_picture_loader_worker.h"
|
#include "card_picture_loader_worker.h"
|
||||||
|
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QHash>
|
||||||
#include <QLoggingCategory>
|
#include <QLoggingCategory>
|
||||||
|
|
||||||
inline Q_LOGGING_CATEGORY(CardPictureLoaderLog, "card_picture_loader");
|
inline Q_LOGGING_CATEGORY(CardPictureLoaderLog, "card_picture_loader");
|
||||||
|
|
@ -56,6 +58,7 @@ private:
|
||||||
|
|
||||||
CardPictureLoaderWorker *worker; ///< Worker thread for async image loading
|
CardPictureLoaderWorker *worker; ///< Worker thread for async image loading
|
||||||
CardPictureLoaderStatusBar *statusBar; ///< Status bar widget showing load progress
|
CardPictureLoaderStatusBar *statusBar; ///< Status bar widget showing load progress
|
||||||
|
QHash<QString, QDateTime> failedAt; ///< Timestamp of the last failed load attempt per pixmap cache key
|
||||||
|
|
||||||
public:
|
public:
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,16 @@
|
||||||
#include <QDirIterator>
|
#include <QDirIterator>
|
||||||
#include <QMovie>
|
#include <QMovie>
|
||||||
#include <libcockatrice/card/database/card_database_manager.h>
|
#include <libcockatrice/card/database/card_database_manager.h>
|
||||||
|
#include <libcockatrice/settings/paths_settings.h>
|
||||||
|
|
||||||
static constexpr int REFRESH_INTERVAL_MS = 10 * 1000;
|
static constexpr int REFRESH_INTERVAL_MS = 10 * 1000;
|
||||||
|
|
||||||
CardPictureLoaderLocal::CardPictureLoaderLocal(QObject *parent)
|
CardPictureLoaderLocal::CardPictureLoaderLocal(QObject *parent)
|
||||||
: QObject(parent), picsPath(SettingsCache::instance().getPicsPath()),
|
: QObject(parent), picsPath(SettingsCache::instance().paths().getPicsPath()),
|
||||||
customPicsPath(SettingsCache::instance().getCustomPicsPath())
|
customPicsPath(SettingsCache::instance().paths().getCustomPicsPath())
|
||||||
{
|
{
|
||||||
// Hook up signals to settings
|
// Hook up signals to settings
|
||||||
connect(&SettingsCache::instance(), &SettingsCache::picsPathChanged, this,
|
connect(&SettingsCache::instance().paths(), &PathsSettings::picsPathChanged, this,
|
||||||
&CardPictureLoaderLocal::picsPathChanged);
|
&CardPictureLoaderLocal::picsPathChanged);
|
||||||
|
|
||||||
refreshIndex();
|
refreshIndex();
|
||||||
|
|
@ -127,6 +128,6 @@ QImage CardPictureLoaderLocal::tryLoadCardImageFromDisk(const QString &setName,
|
||||||
|
|
||||||
void CardPictureLoaderLocal::picsPathChanged()
|
void CardPictureLoaderLocal::picsPathChanged()
|
||||||
{
|
{
|
||||||
picsPath = SettingsCache::instance().getPicsPath();
|
picsPath = SettingsCache::instance().paths().getPicsPath();
|
||||||
customPicsPath = SettingsCache::instance().getCustomPicsPath();
|
customPicsPath = SettingsCache::instance().paths().getCustomPicsPath();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#include "card_picture_loader_worker.h"
|
#include "card_picture_loader_worker.h"
|
||||||
|
|
||||||
#include "../../client/settings/cache_settings.h"
|
#include "../../client/settings/cache_settings.h"
|
||||||
|
#include "card_picture_loader_cache_method.h"
|
||||||
#include "card_picture_loader_local.h"
|
#include "card_picture_loader_local.h"
|
||||||
#include "card_picture_loader_worker_work.h"
|
#include "card_picture_loader_worker_work.h"
|
||||||
|
|
||||||
|
|
@ -9,13 +10,19 @@
|
||||||
#include <QNetworkDiskCache>
|
#include <QNetworkDiskCache>
|
||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
|
#include <libcockatrice/settings/cache_storage_settings.h>
|
||||||
|
#include <libcockatrice/settings/download_settings.h>
|
||||||
|
#include <libcockatrice/settings/paths_settings.h>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <version_string.h>
|
#include <version_string.h>
|
||||||
|
|
||||||
static constexpr int MAX_REQUESTS_PER_SEC = 10;
|
static constexpr int MAX_REQUESTS_PER_SEC = 10;
|
||||||
|
static constexpr int MIN_HOST_QUOTA = 1; ///< Floor for the per-host request allowance
|
||||||
|
static constexpr qint64 QUOTA_RECOVER_MS = 60000; ///< Idle time before a reduced quota starts recovering
|
||||||
|
|
||||||
CardPictureLoaderWorker::CardPictureLoaderWorker()
|
CardPictureLoaderWorker::CardPictureLoaderWorker()
|
||||||
: QObject(nullptr), picDownload(SettingsCache::instance().getPicDownload()), requestQuota(MAX_REQUESTS_PER_SEC)
|
: QObject(nullptr), picDownload(SettingsCache::instance().downloads().getPicDownload()),
|
||||||
|
requestQuota(MAX_REQUESTS_PER_SEC)
|
||||||
{
|
{
|
||||||
networkManager = new QNetworkAccessManager(this);
|
networkManager = new QNetworkAccessManager(this);
|
||||||
// We need a timeout to ensure requests don't hang indefinitely in case of
|
// We need a timeout to ensure requests don't hang indefinitely in case of
|
||||||
|
|
@ -25,13 +32,14 @@ CardPictureLoaderWorker::CardPictureLoaderWorker()
|
||||||
cache = new QNetworkDiskCache(this);
|
cache = new QNetworkDiskCache(this);
|
||||||
cache->setCacheDirectory(SettingsCache::instance().getNetworkCachePath());
|
cache->setCacheDirectory(SettingsCache::instance().getNetworkCachePath());
|
||||||
cache->setMaximumCacheSize(1024L * 1024L *
|
cache->setMaximumCacheSize(1024L * 1024L *
|
||||||
static_cast<qint64>(SettingsCache::instance().getNetworkCacheSizeInMB()));
|
static_cast<qint64>(SettingsCache::instance().cacheStorage().getNetworkCacheSizeInMB()));
|
||||||
|
|
||||||
connect(&SettingsCache::instance(), &SettingsCache::networkCacheSizeChanged, cache, [this](int newSizeInMB) {
|
connect(&SettingsCache::instance().cacheStorage(), &CacheStorageSettings::networkCacheSizeChanged, cache,
|
||||||
if (cache) {
|
[this](int newSizeInMB) {
|
||||||
cache->setMaximumCacheSize(1024L * 1024L * static_cast<qint64>(newSizeInMB));
|
if (cache) {
|
||||||
}
|
cache->setMaximumCacheSize(1024L * 1024L * static_cast<qint64>(newSizeInMB));
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
networkManager->setCache(cache);
|
networkManager->setCache(cache);
|
||||||
|
|
||||||
|
|
@ -39,7 +47,7 @@ CardPictureLoaderWorker::CardPictureLoaderWorker()
|
||||||
// We can't use NoLessSafeRedirectPolicy because it is not applied with AlwaysCache
|
// We can't use NoLessSafeRedirectPolicy because it is not applied with AlwaysCache
|
||||||
networkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy);
|
networkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy);
|
||||||
|
|
||||||
cacheFilePath = SettingsCache::instance().getRedirectCachePath() + REDIRECT_CACHE_FILENAME;
|
cacheFilePath = SettingsCache::instance().paths().getRedirectCachePath() + REDIRECT_CACHE_FILENAME;
|
||||||
loadRedirectCache();
|
loadRedirectCache();
|
||||||
cleanStaleEntries();
|
cleanStaleEntries();
|
||||||
|
|
||||||
|
|
@ -72,7 +80,8 @@ void CardPictureLoaderWorker::queueRequest(const QUrl &url, CardPictureLoaderWor
|
||||||
queueRequest(cachedRedirect, worker);
|
queueRequest(cachedRedirect, worker);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (SettingsCache::instance().getCardPictureLoaderCacheMethod() ==
|
if (static_cast<CardPictureLoaderCacheMethod::CacheMethod>(
|
||||||
|
SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) ==
|
||||||
CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE &&
|
CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE &&
|
||||||
cache->metaData(url).isValid()) {
|
cache->metaData(url).isValid()) {
|
||||||
// If we hit a cached url, we get to make the request for free, since it won't contribute towards the
|
// If we hit a cached url, we get to make the request for free, since it won't contribute towards the
|
||||||
|
|
@ -98,8 +107,10 @@ QNetworkReply *CardPictureLoaderWorker::makeRequest(const QUrl &url, CardPicture
|
||||||
req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
||||||
req.setRawHeader("Accept", "image/avif,image/webp,image/apng,image/,/*;q=0.8");
|
req.setRawHeader("Accept", "image/avif,image/webp,image/apng,image/,/*;q=0.8");
|
||||||
|
|
||||||
bool useNetworkCache = !picDownload && SettingsCache::instance().getCardPictureLoaderCacheMethod() ==
|
bool useNetworkCache =
|
||||||
CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE;
|
!picDownload && static_cast<CardPictureLoaderCacheMethod::CacheMethod>(
|
||||||
|
SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) ==
|
||||||
|
CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE;
|
||||||
|
|
||||||
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute,
|
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute,
|
||||||
useNetworkCache ? QNetworkRequest::AlwaysCache : QNetworkRequest::AlwaysNetwork);
|
useNetworkCache ? QNetworkRequest::AlwaysCache : QNetworkRequest::AlwaysNetwork);
|
||||||
|
|
@ -115,6 +126,19 @@ QNetworkReply *CardPictureLoaderWorker::makeRequest(const QUrl &url, CardPicture
|
||||||
void CardPictureLoaderWorker::resetRequestQuota()
|
void CardPictureLoaderWorker::resetRequestQuota()
|
||||||
{
|
{
|
||||||
requestQuota = MAX_REQUESTS_PER_SEC;
|
requestQuota = MAX_REQUESTS_PER_SEC;
|
||||||
|
|
||||||
|
QDateTime now = QDateTime::currentDateTime();
|
||||||
|
for (auto it = hostRequestQuota.begin(); it != hostRequestQuota.end(); ++it) {
|
||||||
|
if (!hostLast429.contains(it.key()) || now.msecsTo(hostLast429.value(it.key())) < -QUOTA_RECOVER_MS) {
|
||||||
|
it.value() = qMin(MAX_REQUESTS_PER_SEC, it.value() + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto &request : requestLoadQueue) {
|
||||||
|
const QString host = request.first.host();
|
||||||
|
hostQuotaRemaining.insert(host, hostRequestQuota.value(host, MAX_REQUESTS_PER_SEC));
|
||||||
|
}
|
||||||
|
|
||||||
processQueuedRequests();
|
processQueuedRequests();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -127,14 +151,26 @@ void CardPictureLoaderWorker::processQueuedRequests()
|
||||||
|
|
||||||
bool CardPictureLoaderWorker::processSingleRequest()
|
bool CardPictureLoaderWorker::processSingleRequest()
|
||||||
{
|
{
|
||||||
if (!requestLoadQueue.isEmpty()) {
|
for (int i = 0; i < requestLoadQueue.size(); ++i) {
|
||||||
auto request = requestLoadQueue.takeFirst();
|
const auto &request = requestLoadQueue.at(i);
|
||||||
makeRequest(request.first, request.second);
|
QString host = request.first.host();
|
||||||
return true;
|
int allowance = hostQuotaRemaining.value(host, MAX_REQUESTS_PER_SEC);
|
||||||
|
if (allowance > 0) {
|
||||||
|
hostQuotaRemaining.insert(host, allowance - 1);
|
||||||
|
makeRequest(request.first, request.second);
|
||||||
|
requestLoadQueue.removeAt(i);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CardPictureLoaderWorker::onHostRateLimited(const QString &host)
|
||||||
|
{
|
||||||
|
hostRequestQuota.insert(host, qMax(MIN_HOST_QUOTA, hostRequestQuota.value(host, MAX_REQUESTS_PER_SEC) / 2));
|
||||||
|
hostLast429.insert(host, QDateTime::currentDateTime());
|
||||||
|
}
|
||||||
|
|
||||||
void CardPictureLoaderWorker::enqueueImageLoad(const ExactCard &card)
|
void CardPictureLoaderWorker::enqueueImageLoad(const ExactCard &card)
|
||||||
{
|
{
|
||||||
// Send call through a connection to ensure the handling is run on the pictureLoader thread
|
// Send call through a connection to ensure the handling is run on the pictureLoader thread
|
||||||
|
|
@ -229,7 +265,7 @@ void CardPictureLoaderWorker::cleanStaleEntries()
|
||||||
|
|
||||||
auto it = redirectCache.begin();
|
auto it = redirectCache.begin();
|
||||||
while (it != redirectCache.end()) {
|
while (it != redirectCache.end()) {
|
||||||
if (it.value().second.addDays(SettingsCache::instance().getRedirectCacheTtl()) < now) {
|
if (it.value().second.addDays(SettingsCache::instance().cacheStorage().getRedirectCacheTtl()) < now) {
|
||||||
it = redirectCache.erase(it); // Remove stale entry
|
it = redirectCache.erase(it); // Remove stale entry
|
||||||
} else {
|
} else {
|
||||||
++it;
|
++it;
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@
|
||||||
#include "card_picture_loader_worker_work.h"
|
#include "card_picture_loader_worker_work.h"
|
||||||
#include "card_picture_to_load.h"
|
#include "card_picture_to_load.h"
|
||||||
|
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QHash>
|
||||||
#include <QLoggingCategory>
|
#include <QLoggingCategory>
|
||||||
#include <QMutex>
|
#include <QMutex>
|
||||||
#include <QNetworkAccessManager>
|
#include <QNetworkAccessManager>
|
||||||
|
|
@ -66,6 +68,12 @@ public:
|
||||||
*/
|
*/
|
||||||
void queueRequest(const QUrl &url, CardPictureLoaderWorkerWork *worker);
|
void queueRequest(const QUrl &url, CardPictureLoaderWorkerWork *worker);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Handles a server returning HTTP 429 by reducing that host's request quota.
|
||||||
|
* @param host The host that returned 429
|
||||||
|
*/
|
||||||
|
void onHostRateLimited(const QString &host);
|
||||||
|
|
||||||
/** @brief Clears the network cache and redirect cache. */
|
/** @brief Clears the network cache and redirect cache. */
|
||||||
void clearNetworkCache();
|
void clearNetworkCache();
|
||||||
|
|
||||||
|
|
@ -110,8 +118,11 @@ private:
|
||||||
bool picDownload; ///< Whether downloading images from network is enabled
|
bool picDownload; ///< Whether downloading images from network is enabled
|
||||||
QQueue<QPair<QUrl, CardPictureLoaderWorkerWork *>> requestLoadQueue; ///< Queue of pending network requests
|
QQueue<QPair<QUrl, CardPictureLoaderWorkerWork *>> requestLoadQueue; ///< Queue of pending network requests
|
||||||
|
|
||||||
int requestQuota; ///< Remaining requests allowed per second
|
int requestQuota; ///< Remaining requests allowed per second
|
||||||
QTimer requestTimer; ///< Timer to reset the request quota
|
QTimer requestTimer; ///< Timer to reset the request quota
|
||||||
|
QHash<QString, int> hostRequestQuota; ///< Sustained per-host request allowance
|
||||||
|
QHash<QString, int> hostQuotaRemaining; ///< Per-host allowance left in the current second
|
||||||
|
QHash<QString, QDateTime> hostLast429; ///< When each host was last rate limited
|
||||||
|
|
||||||
CardPictureLoaderLocal *localLoader; ///< Loader for local images
|
CardPictureLoaderLocal *localLoader; ///< Loader for local images
|
||||||
QSet<QString> currentlyLoading; ///< Deduplication: contains pixmapCacheKey currently being loaded
|
QSet<QString> currentlyLoading; ///< Deduplication: contains pixmapCacheKey currently being loaded
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,13 @@
|
||||||
#include <QLoggingCategory>
|
#include <QLoggingCategory>
|
||||||
#include <QMovie>
|
#include <QMovie>
|
||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
|
#include <QRandomGenerator>
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
#include <QThreadPool>
|
#include <QThreadPool>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
|
ServerRateLimiter CardPictureLoaderWorkerWork::s_rateLimiter;
|
||||||
|
#include <libcockatrice/settings/download_settings.h>
|
||||||
|
|
||||||
// Card back returned by gatherer when card is not found
|
// Card back returned by gatherer when card is not found
|
||||||
static const QStringList MD5_BLACKLIST = {
|
static const QStringList MD5_BLACKLIST = {
|
||||||
|
|
@ -19,7 +24,7 @@ static const QStringList MD5_BLACKLIST = {
|
||||||
|
|
||||||
CardPictureLoaderWorkerWork::CardPictureLoaderWorkerWork(const CardPictureLoaderWorker *worker, const ExactCard &toLoad)
|
CardPictureLoaderWorkerWork::CardPictureLoaderWorkerWork(const CardPictureLoaderWorker *worker, const ExactCard &toLoad)
|
||||||
: QObject(nullptr), cardToDownload(CardPictureToLoad(toLoad)),
|
: QObject(nullptr), cardToDownload(CardPictureToLoad(toLoad)),
|
||||||
picDownload(SettingsCache::instance().getPicDownload())
|
picDownload(SettingsCache::instance().downloads().getPicDownload())
|
||||||
{
|
{
|
||||||
// Hook up signals to the orchestrator
|
// Hook up signals to the orchestrator
|
||||||
connect(this, &CardPictureLoaderWorkerWork::requestImageDownload, worker, &CardPictureLoaderWorker::queueRequest);
|
connect(this, &CardPictureLoaderWorkerWork::requestImageDownload, worker, &CardPictureLoaderWorker::queueRequest);
|
||||||
|
|
@ -29,19 +34,48 @@ CardPictureLoaderWorkerWork::CardPictureLoaderWorkerWork(const CardPictureLoader
|
||||||
connect(this, &CardPictureLoaderWorkerWork::imageLoaded, worker, &CardPictureLoaderWorker::handleImageLoaded);
|
connect(this, &CardPictureLoaderWorkerWork::imageLoaded, worker, &CardPictureLoaderWorker::handleImageLoaded);
|
||||||
connect(this, &CardPictureLoaderWorkerWork::requestSucceeded, worker,
|
connect(this, &CardPictureLoaderWorkerWork::requestSucceeded, worker,
|
||||||
&CardPictureLoaderWorker::imageRequestSucceeded);
|
&CardPictureLoaderWorker::imageRequestSucceeded);
|
||||||
|
connect(this, &CardPictureLoaderWorkerWork::rateLimited, worker, &CardPictureLoaderWorker::onHostRateLimited);
|
||||||
|
|
||||||
// Hook up signals to settings
|
// Hook up signals to settings
|
||||||
connect(&SettingsCache::instance(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged()));
|
connect(&SettingsCache::instance().downloads(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged()));
|
||||||
|
|
||||||
startNextPicDownload();
|
startNextPicDownload();
|
||||||
}
|
}
|
||||||
|
|
||||||
void CardPictureLoaderWorkerWork::startNextPicDownload()
|
void CardPictureLoaderWorkerWork::startNextPicDownload()
|
||||||
{
|
{
|
||||||
|
QDateTime now = QDateTime::currentDateTime();
|
||||||
|
while (!cardToDownload.getCurrentUrl().isEmpty() &&
|
||||||
|
s_rateLimiter.isRateLimited(QUrl(cardToDownload.getCurrentUrl()).host(), now)) {
|
||||||
|
QString host = QUrl(cardToDownload.getCurrentUrl()).host();
|
||||||
|
if (s_rateLimiter.rounds(host) == 1) {
|
||||||
|
// First 429 round for this server: wait out the backoff and give it
|
||||||
|
// one more chance instead of immediately falling through to a worse
|
||||||
|
// source. A second 429 makes us fall through instead.
|
||||||
|
qCDebug(CardPictureLoaderWorkerWorkLog).nospace()
|
||||||
|
<< "PictureLoader: [card: " << cardToDownload.getCard().getInfo().getCorrectedName()
|
||||||
|
<< " set: " << cardToDownload.getSetName() << "]: Waiting out backoff for " << host << " to retry "
|
||||||
|
<< cardToDownload.getCurrentUrl();
|
||||||
|
scheduleDeferredRetry();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The server has already 429'd us at least twice, so further retries are
|
||||||
|
// unlikely to succeed: move on to the other configured sources.
|
||||||
|
qCDebug(CardPictureLoaderWorkerWorkLog).nospace()
|
||||||
|
<< "PictureLoader: [card: " << cardToDownload.getCard().getInfo().getCorrectedName()
|
||||||
|
<< " set: " << cardToDownload.getSetName() << "]: Skipping rate-limited URL "
|
||||||
|
<< cardToDownload.getCurrentUrl() << " (server " << host << " still rate limiting)";
|
||||||
|
if (!cardToDownload.nextUrl() && !cardToDownload.nextSet()) {
|
||||||
|
scheduleDeferredRetry();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
QString picUrl = cardToDownload.getCurrentUrl();
|
QString picUrl = cardToDownload.getCurrentUrl();
|
||||||
|
|
||||||
if (picUrl.isEmpty()) {
|
if (picUrl.isEmpty()) {
|
||||||
picDownloadFailed();
|
scheduleDeferredRetry();
|
||||||
} else {
|
} else {
|
||||||
QUrl url(picUrl);
|
QUrl url(picUrl);
|
||||||
qCDebug(CardPictureLoaderWorkerWorkLog).nospace()
|
qCDebug(CardPictureLoaderWorkerWorkLog).nospace()
|
||||||
|
|
@ -107,7 +141,41 @@ static bool imageIsBlackListed(const QByteArray &picData)
|
||||||
void CardPictureLoaderWorkerWork::handleFailedReply(const QNetworkReply *reply)
|
void CardPictureLoaderWorkerWork::handleFailedReply(const QNetworkReply *reply)
|
||||||
{
|
{
|
||||||
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 429) {
|
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 429) {
|
||||||
qCWarning(CardPictureLoaderWorkerWorkLog) << "Too many requests.";
|
QString host = reply->url().host();
|
||||||
|
QDateTime now = QDateTime::currentDateTime();
|
||||||
|
|
||||||
|
qint64 retryAfterMs = 0;
|
||||||
|
const QByteArray retryAfterHeader = reply->rawHeader("Retry-After");
|
||||||
|
if (!retryAfterHeader.isEmpty()) {
|
||||||
|
bool ok = false;
|
||||||
|
int seconds = retryAfterHeader.toInt(&ok);
|
||||||
|
if (ok && seconds > 0) {
|
||||||
|
retryAfterMs = static_cast<qint64>(seconds) * 1000;
|
||||||
|
} else {
|
||||||
|
QDateTime retryAfterDate =
|
||||||
|
QDateTime::fromString(QString::fromLatin1(retryAfterHeader), Qt::RFC2822Date);
|
||||||
|
if (retryAfterDate.isValid()) {
|
||||||
|
retryAfterMs = qMax<qint64>(0, now.msecsTo(retryAfterDate));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QDateTime backoffUntil = s_rateLimiter.on429(host, now, retryAfterMs);
|
||||||
|
emit rateLimited(host);
|
||||||
|
|
||||||
|
if (s_rateLimiter.rounds(host) == 1) {
|
||||||
|
qCWarning(CardPictureLoaderWorkerWorkLog).nospace()
|
||||||
|
<< "PictureLoader: [card: " << cardToDownload.getCard().getName()
|
||||||
|
<< " set: " << cardToDownload.getSetName() << "]: Too many requests from " << host
|
||||||
|
<< ", backing off until " << backoffUntil.toString(Qt::ISODate) << ", retrying the same url";
|
||||||
|
scheduleDeferredRetry();
|
||||||
|
} else {
|
||||||
|
qCWarning(CardPictureLoaderWorkerWorkLog).nospace()
|
||||||
|
<< "PictureLoader: [card: " << cardToDownload.getCard().getName()
|
||||||
|
<< " set: " << cardToDownload.getSetName() << "]: Too many requests from " << host
|
||||||
|
<< ", retry already attempted, falling through to other sources";
|
||||||
|
picDownloadFailed();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
bool isFromCache = reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool();
|
bool isFromCache = reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool();
|
||||||
|
|
||||||
|
|
@ -148,6 +216,9 @@ void CardPictureLoaderWorkerWork::handleSuccessfulReply(QNetworkReply *reply)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A non-redirect successful response means the server is not rate limiting us anymore.
|
||||||
|
s_rateLimiter.onSuccess(reply->url().host());
|
||||||
|
|
||||||
// peek is used to keep the data in the buffer for use by QImageReader
|
// peek is used to keep the data in the buffer for use by QImageReader
|
||||||
const QByteArray &picData = reply->peek(reply->size());
|
const QByteArray &picData = reply->peek(reply->size());
|
||||||
|
|
||||||
|
|
@ -202,6 +273,42 @@ QImage CardPictureLoaderWorkerWork::tryLoadImageFromReply(QNetworkReply *reply)
|
||||||
return imgReader.read();
|
return imgReader.read();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CardPictureLoaderWorkerWork::scheduleDeferredRetry()
|
||||||
|
{
|
||||||
|
QDateTime now = QDateTime::currentDateTime();
|
||||||
|
|
||||||
|
// Prefer waiting on the current URL's server so we retry the same source.
|
||||||
|
QString currentHost = QUrl(cardToDownload.getCurrentUrl()).host();
|
||||||
|
QDateTime backoffUntil = s_rateLimiter.deadline(currentHost);
|
||||||
|
if (!s_rateLimiter.isRateLimited(currentHost, now)) {
|
||||||
|
backoffUntil = s_rateLimiter.earliestDeadline(now);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!backoffUntil.isValid()) {
|
||||||
|
qCWarning(CardPictureLoaderWorkerWorkLog).nospace()
|
||||||
|
<< "PictureLoader: [card: " << cardToDownload.getCard().getInfo().getCorrectedName()
|
||||||
|
<< " set: " << cardToDownload.getSetName() << "]: All URLs exhausted, no servers in backoff: BAILING OUT";
|
||||||
|
concludeImageLoad(QImage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
qint64 waitMs = qMax<qint64>(0, now.msecsTo(backoffUntil));
|
||||||
|
// Add some jitter to desynchronize concurrent retries and avoid a thundering herd.
|
||||||
|
waitMs += QRandomGenerator::global()->bounded(5000);
|
||||||
|
|
||||||
|
qCDebug(CardPictureLoaderWorkerWorkLog).nospace()
|
||||||
|
<< "PictureLoader: [card: " << cardToDownload.getCard().getInfo().getCorrectedName()
|
||||||
|
<< " set: " << cardToDownload.getSetName() << "]: All URLs exhausted, scheduling deferred retry in " << waitMs
|
||||||
|
<< "ms";
|
||||||
|
|
||||||
|
QTimer::singleShot(waitMs, this, [this] {
|
||||||
|
s_rateLimiter.clearExpired(QDateTime::currentDateTime());
|
||||||
|
|
||||||
|
cardToDownload.resetIndices();
|
||||||
|
startNextPicDownload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void CardPictureLoaderWorkerWork::concludeImageLoad(const QImage &image)
|
void CardPictureLoaderWorkerWork::concludeImageLoad(const QImage &image)
|
||||||
{
|
{
|
||||||
emit imageLoaded(cardToDownload.getCard(), image);
|
emit imageLoaded(cardToDownload.getCard(), image);
|
||||||
|
|
@ -210,5 +317,5 @@ void CardPictureLoaderWorkerWork::concludeImageLoad(const QImage &image)
|
||||||
|
|
||||||
void CardPictureLoaderWorkerWork::picDownloadChanged()
|
void CardPictureLoaderWorkerWork::picDownloadChanged()
|
||||||
{
|
{
|
||||||
picDownload = SettingsCache::instance().getPicDownload();
|
picDownload = SettingsCache::instance().downloads().getPicDownload();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,15 @@
|
||||||
#include "card_picture_loader_worker.h"
|
#include "card_picture_loader_worker.h"
|
||||||
#include "card_picture_to_load.h"
|
#include "card_picture_to_load.h"
|
||||||
|
|
||||||
|
#include <QDateTime>
|
||||||
#include <QLoggingCategory>
|
#include <QLoggingCategory>
|
||||||
#include <QMutex>
|
#include <QMutex>
|
||||||
#include <QNetworkAccessManager>
|
#include <QNetworkAccessManager>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
#include <QRandomGenerator>
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
#include <libcockatrice/card/database/card_database.h>
|
#include <libcockatrice/card/database/card_database.h>
|
||||||
|
#include <libcockatrice/utility/server_rate_limiter.h>
|
||||||
|
|
||||||
inline Q_LOGGING_CATEGORY(CardPictureLoaderWorkerWorkLog, "card_picture_loader.worker");
|
inline Q_LOGGING_CATEGORY(CardPictureLoaderWorkerWorkLog, "card_picture_loader.worker");
|
||||||
|
|
||||||
|
|
@ -50,6 +53,8 @@ public slots:
|
||||||
private:
|
private:
|
||||||
bool picDownload; ///< Whether network downloading is enabled
|
bool picDownload; ///< Whether network downloading is enabled
|
||||||
|
|
||||||
|
static ServerRateLimiter s_rateLimiter; ///< Shared per-server 429 backoff state
|
||||||
|
|
||||||
/** @brief Starts downloading the next URL for this card. */
|
/** @brief Starts downloading the next URL for this card. */
|
||||||
void startNextPicDownload();
|
void startNextPicDownload();
|
||||||
|
|
||||||
|
|
@ -77,6 +82,16 @@ private:
|
||||||
*/
|
*/
|
||||||
void concludeImageLoad(const QImage &image);
|
void concludeImageLoad(const QImage &image);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Schedules a deferred retry after the relevant server backoff expires.
|
||||||
|
*
|
||||||
|
* Waits on the current URL's server when it is the reason we are blocked,
|
||||||
|
* otherwise on the earliest active backoff. If no servers are in backoff,
|
||||||
|
* concludes with failure. Otherwise resets the CardPictureToLoad indices and
|
||||||
|
* retries after the backoff period.
|
||||||
|
*/
|
||||||
|
void scheduleDeferredRetry();
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
/** @brief Updates the picDownload setting when it changes. */
|
/** @brief Updates the picDownload setting when it changes. */
|
||||||
void picDownloadChanged();
|
void picDownloadChanged();
|
||||||
|
|
@ -100,6 +115,9 @@ signals:
|
||||||
/** @brief Emitted when a URL has been redirected. */
|
/** @brief Emitted when a URL has been redirected. */
|
||||||
void urlRedirected(const QUrl &originalUrl, const QUrl &redirectUrl);
|
void urlRedirected(const QUrl &originalUrl, const QUrl &redirectUrl);
|
||||||
|
|
||||||
|
/** @brief Emitted when a server returned HTTP 429. */
|
||||||
|
void rateLimited(const QString &host);
|
||||||
|
|
||||||
/** @brief Emitted when a cached URL is invalid and must be removed. */
|
/** @brief Emitted when a cached URL is invalid and must be removed. */
|
||||||
void cachedUrlInvalidated(const QUrl &url);
|
void cachedUrlInvalidated(const QUrl &url);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,17 @@
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <libcockatrice/card/set/card_set_comparator.h>
|
#include <libcockatrice/card/set/card_set_comparator.h>
|
||||||
#include <libcockatrice/interfaces/noop_card_set_priority_controller.h>
|
#include <libcockatrice/interfaces/noop_card_set_priority_controller.h>
|
||||||
|
#include <libcockatrice/settings/cards_display_settings.h>
|
||||||
|
#include <libcockatrice/settings/download_settings.h>
|
||||||
|
|
||||||
CardPictureToLoad::CardPictureToLoad(const ExactCard &_card)
|
CardPictureToLoad::CardPictureToLoad(const ExactCard &_card)
|
||||||
: card(_card), urlTemplates(SettingsCache::instance().downloads().getAllURLs())
|
: card(_card), urlTemplates(SettingsCache::instance().downloads().getAllURLs())
|
||||||
{
|
{
|
||||||
if (card) {
|
if (card) {
|
||||||
sortedSets = extractSetsSorted(card);
|
sortedSets = extractSetsSorted(card);
|
||||||
// The first time called, nextSet will also populate the Urls for the first set.
|
currentSetIndex = 0;
|
||||||
nextSet();
|
currentSet = sortedSets.first();
|
||||||
|
populateSetUrls();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -34,7 +37,7 @@ QList<CardSetPtr> CardPictureToLoad::extractSetsSorted(const ExactCard &card)
|
||||||
std::sort(sortedSets.begin(), sortedSets.end(), SetPriorityComparator());
|
std::sort(sortedSets.begin(), sortedSets.end(), SetPriorityComparator());
|
||||||
|
|
||||||
// If the user hasn't disabled arts other than their personal preference...
|
// If the user hasn't disabled arts other than their personal preference...
|
||||||
if (!SettingsCache::instance().getOverrideAllCardArtWithPersonalPreference()) {
|
if (!SettingsCache::instance().cardsDisplay().getOverrideAllCardArtWithPersonalPreference()) {
|
||||||
// If the pixmapCacheKey corresponds to a specific set, we have to try to load it first.
|
// If the pixmapCacheKey corresponds to a specific set, we have to try to load it first.
|
||||||
qsizetype setIndex = sortedSets.indexOf(card.getPrinting().getSet());
|
qsizetype setIndex = sortedSets.indexOf(card.getPrinting().getSet());
|
||||||
if (setIndex > 0) { // we don't need to move the set if it's already first
|
if (setIndex > 0) { // we don't need to move the set if it's already first
|
||||||
|
|
@ -99,15 +102,19 @@ void CardPictureToLoad::populateSetUrls()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Call nextUrl to make sure currentUrl is up-to-date
|
currentUrlIndex = 0;
|
||||||
but we don't need the result here. */
|
if (!currentSetUrls.isEmpty()) {
|
||||||
(void)nextUrl();
|
currentUrl = currentSetUrls.first();
|
||||||
|
} else {
|
||||||
|
currentUrl = QString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CardPictureToLoad::nextSet()
|
bool CardPictureToLoad::nextSet()
|
||||||
{
|
{
|
||||||
if (!sortedSets.isEmpty()) {
|
currentSetIndex++;
|
||||||
currentSet = sortedSets.takeFirst();
|
if (currentSetIndex < sortedSets.size()) {
|
||||||
|
currentSet = sortedSets.at(currentSetIndex);
|
||||||
populateSetUrls();
|
populateSetUrls();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -117,8 +124,9 @@ bool CardPictureToLoad::nextSet()
|
||||||
|
|
||||||
bool CardPictureToLoad::nextUrl()
|
bool CardPictureToLoad::nextUrl()
|
||||||
{
|
{
|
||||||
if (!currentSetUrls.isEmpty()) {
|
currentUrlIndex++;
|
||||||
currentUrl = currentSetUrls.takeFirst();
|
if (currentUrlIndex < currentSetUrls.size()) {
|
||||||
|
currentUrl = currentSetUrls.at(currentUrlIndex);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
currentUrl = QString();
|
currentUrl = QString();
|
||||||
|
|
@ -134,6 +142,28 @@ QString CardPictureToLoad::getSetName() const
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString CardPictureToLoad::peekNextUrl() const
|
||||||
|
{
|
||||||
|
int nextIndex = currentUrlIndex + 1;
|
||||||
|
if (nextIndex < currentSetUrls.size()) {
|
||||||
|
return currentSetUrls.at(nextIndex);
|
||||||
|
}
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CardPictureToLoad::resetIndices()
|
||||||
|
{
|
||||||
|
currentSetIndex = 0;
|
||||||
|
if (!sortedSets.isEmpty()) {
|
||||||
|
currentSet = sortedSets.first();
|
||||||
|
populateSetUrls();
|
||||||
|
} else {
|
||||||
|
currentSet = {};
|
||||||
|
currentSetUrls.clear();
|
||||||
|
currentUrl = QString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static int parse(const QString &urlTemplate,
|
static int parse(const QString &urlTemplate,
|
||||||
const QString &propType,
|
const QString &propType,
|
||||||
const QString &cardName,
|
const QString &cardName,
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ private:
|
||||||
QList<QString> currentSetUrls; ///< URLs for the current set being attempted
|
QList<QString> currentSetUrls; ///< URLs for the current set being attempted
|
||||||
QString currentUrl; ///< Currently active URL to download
|
QString currentUrl; ///< Currently active URL to download
|
||||||
CardSetPtr currentSet; ///< Currently active set
|
CardSetPtr currentSet; ///< Currently active set
|
||||||
|
int currentSetIndex = 0; ///< Current position in sortedSets
|
||||||
|
int currentUrlIndex = 0; ///< Current position in currentSetUrls
|
||||||
|
|
||||||
public:
|
public:
|
||||||
/**
|
/**
|
||||||
|
|
@ -56,6 +58,9 @@ public:
|
||||||
/** @return The short name of the current set, or empty string if no set. */
|
/** @return The short name of the current set, or empty string if no set. */
|
||||||
[[nodiscard]] QString getSetName() const;
|
[[nodiscard]] QString getSetName() const;
|
||||||
|
|
||||||
|
/** @return The next URL in the current set's list without advancing, or empty if at end. */
|
||||||
|
[[nodiscard]] QString peekNextUrl() const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Transforms a URL template into a concrete URL for this card/set.
|
* @brief Transforms a URL template into a concrete URL for this card/set.
|
||||||
* @param urlTemplate The URL template to transform
|
* @param urlTemplate The URL template to transform
|
||||||
|
|
@ -88,6 +93,14 @@ public:
|
||||||
*/
|
*/
|
||||||
void populateSetUrls();
|
void populateSetUrls();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Resets iteration indices to the beginning.
|
||||||
|
*
|
||||||
|
* Restarts URL/set iteration from the first set and first URL.
|
||||||
|
* Used for deferred retry after server backoff expires.
|
||||||
|
*/
|
||||||
|
void resetIndices();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Extract all sets from the card and sort them by priority.
|
* @brief Extract all sets from the card and sort them by priority.
|
||||||
* @param card The card to extract sets from
|
* @param card The card to extract sets from
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
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