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
|
||||
if [[ $USE_VCPKG ]]; then
|
||||
flags+=("-DUSE_VCPKG=1")
|
||||
flags+=("-DVCPKG_INSTALL_OPTIONS=--x-abi-tools-use-exact-versions")
|
||||
fi
|
||||
|
||||
# Add cmake --build flags
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
exclude_paths:
|
||||
- '**/translations/*.ts'
|
||||
|
||||
# codacy config documentation: https://support.codacy.com/hc/en-us/articles/115002130625-Codacy-Configuration-File
|
||||
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
|
|
@ -334,7 +334,7 @@ the tr() call, also you can add an extra string as a hint for translators:
|
|||
QString message = tr("Everyone draws %n cards", "english hint for translators", amount);
|
||||
```
|
||||
See [Qt's wiki on translations](
|
||||
https://doc.qt.io/qt-5/i18n-source-translation.html#handling-plurals)
|
||||
https://doc.qt.io/qt-6/i18n-source-translation.html#handling-plurals)
|
||||
|
||||
If you're about to propose a change that adds or modifies any translatable
|
||||
string in the code, you don't need to take care of adding the new strings to
|
||||
|
|
|
|||
48
.github/workflows/desktop-build.yml
vendored
48
.github/workflows/desktop-build.yml
vendored
|
|
@ -3,7 +3,7 @@ name: Build Desktop
|
|||
permissions:
|
||||
actions: write # needed to delete entries in GHA cache (update ccache)
|
||||
attestations: write # needed to persist the attestation.
|
||||
contents: write
|
||||
contents: write # needed for e.g. vcpkg dependency graph updates
|
||||
id-token: write # needed for signing certificate in attestation
|
||||
|
||||
on:
|
||||
|
|
@ -42,7 +42,7 @@ concurrency:
|
|||
jobs:
|
||||
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:
|
||||
tag: ${{ steps.configure.outputs.tag }}
|
||||
sha: ${{ steps.configure.outputs.sha }}
|
||||
|
|
@ -146,7 +146,7 @@ jobs:
|
|||
|
||||
name: ${{ matrix.distro }} ${{ matrix.version }}
|
||||
needs: configure
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-latest # https://github.com/actions/runner-images
|
||||
continue-on-error: ${{ matrix.allow-failure == 'yes' }}
|
||||
timeout-minutes: 70
|
||||
env:
|
||||
|
|
@ -262,84 +262,80 @@ jobs:
|
|||
matrix:
|
||||
include:
|
||||
- os: macOS
|
||||
target: 13
|
||||
runner: macos-15-intel
|
||||
target: 13 # EOL 2025-09-15
|
||||
runner: macos-15-intel # https://github.com/actions/runner-images/blob/main/images/macos/macos-15-Readme.md
|
||||
|
||||
ccache_eviction_age: 7d
|
||||
cmake_generator: Ninja
|
||||
make_package: 1
|
||||
override_target: 13
|
||||
package_suffix: "-macOS13_Intel"
|
||||
qt_arch: clang_64
|
||||
qt_archives: qtbase qtimageformats qtmultimedia qtsvg qttools qttranslations
|
||||
qt_modules: qtwebsockets
|
||||
qt_version: 6.11.0
|
||||
qt_version: 6.11.1
|
||||
soc: Intel
|
||||
type: Release
|
||||
use_ccache: 1
|
||||
xcode: "16.4"
|
||||
xcode: "26.3"
|
||||
|
||||
- os: macOS
|
||||
target: 14
|
||||
runner: macos-14
|
||||
target: 14 # EOL 2026-??
|
||||
runner: macos-15 # https://github.com/actions/runner-images/blob/main/images/macos/macos-15-arm64-Readme.md
|
||||
|
||||
ccache_eviction_age: 7d
|
||||
cmake_generator: Ninja
|
||||
make_package: 1
|
||||
override_target: 14
|
||||
package_suffix: "-macOS14"
|
||||
qt_arch: clang_64
|
||||
qt_archives: qtbase qtimageformats qtmultimedia qtsvg qttools qttranslations
|
||||
qt_modules: qtwebsockets
|
||||
qt_version: 6.11.0
|
||||
qt_version: 6.11.1
|
||||
soc: Apple
|
||||
type: Release
|
||||
use_ccache: 1
|
||||
xcode: "15.4"
|
||||
xcode: "26.3"
|
||||
|
||||
- os: macOS
|
||||
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
|
||||
cmake_generator: Ninja
|
||||
make_package: 1
|
||||
package_suffix: "-macOS15"
|
||||
qt_arch: clang_64
|
||||
qt_archives: qtbase qtimageformats qtmultimedia qtsvg qttools qttranslations
|
||||
qt_modules: qtwebsockets
|
||||
qt_version: 6.11.0
|
||||
qt_version: 6.11.1
|
||||
soc: Apple
|
||||
type: Release
|
||||
use_ccache: 1
|
||||
xcode: "16.4"
|
||||
xcode: "26.3"
|
||||
|
||||
- os: macOS
|
||||
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
|
||||
cmake_generator: Ninja
|
||||
qt_arch: clang_64
|
||||
qt_archives: qtbase qtimageformats qtmultimedia qtsvg qttools qttranslations
|
||||
qt_modules: qtwebsockets
|
||||
qt_version: 6.11.0
|
||||
qt_version: 6.11.1
|
||||
soc: Apple
|
||||
type: Debug
|
||||
use_ccache: 1
|
||||
xcode: "16.4"
|
||||
xcode: "26.3"
|
||||
|
||||
- os: Windows
|
||||
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_platform: x64
|
||||
make_package: 1
|
||||
package_suffix: "-Win10"
|
||||
qt_arch: win64_msvc2022_64
|
||||
qt_archives: qtbase qtimageformats qtmultimedia qtsvg qttools qttranslations
|
||||
qt_modules: qtwebsockets
|
||||
qt_version: 6.11.0
|
||||
qt_version: 6.11.1
|
||||
type: Release
|
||||
|
||||
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'
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
arch: ${{ matrix.qt_arch }}
|
||||
archives: ${{ matrix.qt_archives }}
|
||||
cache: false
|
||||
dir: ${{ github.workspace }}
|
||||
|
|
@ -427,7 +422,6 @@ jobs:
|
|||
with:
|
||||
# Qt 6.11.0 only works with aqtinstall directly from git until aqtinstall 3.4 is released
|
||||
aqtsource: git+https://github.com/miurahr/aqtinstall.git
|
||||
arch: ${{ matrix.qt_arch }}
|
||||
archives: ${{ matrix.qt_archives }}
|
||||
cache: true
|
||||
modules: ${{ matrix.qt_modules }}
|
||||
|
|
@ -454,6 +448,7 @@ jobs:
|
|||
CMAKE_GENERATOR: ${{ matrix.cmake_generator }}
|
||||
CMAKE_GENERATOR_PLATFORM: ${{ matrix.cmake_generator_platform }}
|
||||
DEVELOPER_DIR: '/Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer'
|
||||
GITHUB_TOKEN: ${{ github.token }} # needed for vcpkg dependency graph updates, see VCPKG_FEATURE_FLAGS
|
||||
MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }}
|
||||
MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
|
||||
MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }}
|
||||
|
|
@ -464,6 +459,7 @@ jobs:
|
|||
USE_CCACHE: ${{ matrix.use_ccache }}
|
||||
VCPKG_BINARY_SOURCES: 'clear;files,${{ steps.vcpkg-cache.outputs.path }},readwrite'
|
||||
VCPKG_DISABLE_METRICS: 1
|
||||
VCPKG_FEATURE_FLAGS: dependencygraph
|
||||
run: .ci/compile.sh --server --test --vcpkg
|
||||
|
||||
# Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342
|
||||
|
|
|
|||
7
.github/workflows/docker-release.yml
vendored
7
.github/workflows/docker-release.yml
vendored
|
|
@ -56,8 +56,9 @@ jobs:
|
|||
- name: "Set up Docker buildx"
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: "Login to GitHub Container Registry"
|
||||
if: contains(github.event.release.tag_name, 'Release') && github.event.release.target_commitish == 'master'
|
||||
- name: "Login to GitHub Container Registry (GHCR)"
|
||||
if: github.event_name == 'release' && github.event.release.prerelease == false
|
||||
id: login
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
password: ${{ github.token }}
|
||||
|
|
@ -73,5 +74,5 @@ jobs:
|
|||
context: .
|
||||
labels: ${{ steps.metadata.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: ${{ github.ref_type == 'tag' }}
|
||||
push: ${{ steps.login.outcome == 'success' }}
|
||||
tags: ${{ steps.metadata.outputs.tags }}
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -6,6 +6,7 @@ mysql.cnf
|
|||
.DS_Store
|
||||
.idea/
|
||||
*.aps
|
||||
*.cache
|
||||
cmake-build*
|
||||
preferences
|
||||
compile_commands.json
|
||||
|
|
|
|||
|
|
@ -64,10 +64,11 @@ if(WIN32 OR USE_VCPKG)
|
|||
else()
|
||||
set(QTDIR
|
||||
""
|
||||
CACHE PATH "Path to Qt (e.g. C:/Qt/5.7/msvc2015_64)"
|
||||
CACHE PATH "Path to Qt (e.g. C:/Qt/6.4.2/msvc2019_64)"
|
||||
)
|
||||
message(
|
||||
WARNING "QTDIR variable is missing. Please set this variable to specify path to Qt (e.g. C:/Qt/5.7/msvc2015_64)"
|
||||
WARNING
|
||||
"QTDIR variable is missing. Please set this variable to specify path to Qt (e.g. C:/Qt/6.4.2/msvc2019_64)"
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
|
@ -174,7 +175,7 @@ elseif(CMAKE_COMPILER_IS_GNUCXX)
|
|||
-Wno-error=delete-non-virtual-dtor
|
||||
-Wno-error=sign-compare
|
||||
-Wno-error=missing-declarations
|
||||
-Wno-error=sfinae-incomplete # GCC 16+: Qt MOC + protobuf forward decls trigger this
|
||||
-Wno-error=sfinae-incomplete # GCC 16+: Qt MOC + protobuf forward decls trigger this
|
||||
)
|
||||
|
||||
foreach(FLAG ${ADDITIONAL_DEBUG_FLAGS})
|
||||
|
|
@ -280,11 +281,7 @@ if(UNIX)
|
|||
if(CPACK_GENERATOR STREQUAL "RPM")
|
||||
set(CPACK_RPM_PACKAGE_LICENSE "GPLv2")
|
||||
set(CPACK_RPM_MAIN_COMPONENT "cockatrice")
|
||||
if(Qt6_FOUND)
|
||||
set(CPACK_RPM_PACKAGE_REQUIRES "protobuf, qt6-qttools, qt6-qtsvg, qt6-qtmultimedia, qt6-qtimageformats")
|
||||
elseif(Qt5_FOUND)
|
||||
set(CPACK_RPM_PACKAGE_REQUIRES "protobuf, qt5-qttools, qt5-qtsvg, qt5-qtmultimedia")
|
||||
endif()
|
||||
set(CPACK_RPM_PACKAGE_REQUIRES "protobuf, qt6-qttools, qt6-qtsvg, qt6-qtmultimedia, qt6-qtimageformats")
|
||||
set(CPACK_RPM_PACKAGE_GROUP "Amusements/Games")
|
||||
set(CPACK_RPM_PACKAGE_URL "http://github.com/Cockatrice/Cockatrice")
|
||||
# stop directories from making package conflicts
|
||||
|
|
@ -302,12 +299,8 @@ if(UNIX)
|
|||
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
|
||||
set(CPACK_DEBIAN_PACKAGE_SECTION "games")
|
||||
set(CPACK_DEBIAN_PACKAGE_HOMEPAGE "http://github.com/Cockatrice/Cockatrice")
|
||||
if(Qt6_FOUND)
|
||||
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt6multimedia6, libqt6svg6, qt6-qpa-plugins, qt6-image-formats-plugins")
|
||||
set(CPACK_DEBIAN_PACKAGE_RECOMMENDS "libqt6sql6-mysql") # for connecting servatrice to a mysql db
|
||||
elseif(Qt5_FOUND)
|
||||
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt5multimedia5-plugins, libqt5svg5")
|
||||
endif()
|
||||
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt6multimedia6, libqt6svg6, qt6-qpa-plugins, qt6-image-formats-plugins")
|
||||
set(CPACK_DEBIAN_PACKAGE_RECOMMENDS "libqt6sql6-mysql") # for connecting servatrice to a mysql db
|
||||
endif()
|
||||
endif()
|
||||
elseif(WIN32)
|
||||
|
|
|
|||
8
Doxyfile
8
Doxyfile
|
|
@ -349,7 +349,7 @@ OPTIMIZE_OUTPUT_SLICE = NO
|
|||
#
|
||||
# Note see also the list of default file extension mappings.
|
||||
|
||||
EXTENSION_MAPPING =
|
||||
EXTENSION_MAPPING = proto=C++
|
||||
|
||||
# If the MARKDOWN_SUPPORT tag is enabled then Doxygen pre-processes all comments
|
||||
# according to the Markdown format, which allows for more readable
|
||||
|
|
@ -1086,7 +1086,8 @@ FILE_PATTERNS = *.cc \
|
|||
*.h++ \
|
||||
*.markdown \
|
||||
*.md \
|
||||
*.dox
|
||||
*.dox \
|
||||
*.proto
|
||||
|
||||
# The RECURSIVE tag can be used to specify whether or not subdirectories should
|
||||
# be searched for input files as well.
|
||||
|
|
@ -1103,6 +1104,7 @@ RECURSIVE = YES
|
|||
|
||||
EXCLUDE = build/ \
|
||||
cmake/ \
|
||||
cmake-build-debug/ \
|
||||
doc/doxygen/theme/docs/ \
|
||||
doc/doxygen/theme/include/ \
|
||||
vcpkg/
|
||||
|
|
@ -1195,7 +1197,7 @@ INPUT_FILTER =
|
|||
# need to set EXTENSION_MAPPING for the extension otherwise the files are not
|
||||
# properly processed by Doxygen.
|
||||
|
||||
FILTER_PATTERNS =
|
||||
FILTER_PATTERNS = "*.proto=python doc/doxygen/filters/proto2cpp.py"
|
||||
|
||||
# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using
|
||||
# INPUT_FILTER) will also be used to filter the input files that are used for
|
||||
|
|
|
|||
|
|
@ -158,7 +158,6 @@ The following flags (with their non-default values) can be passed to `cmake`:
|
|||
| `-DWARNING_AS_ERROR=0` | Don't treat compilation warnings as errors in debug mode |
|
||||
| `-DUPDATE_TRANSLATIONS=1` | Configure `make` to update the translation .ts files for new strings in the source code<br> **Note:** `make clean` will remove the .ts files |
|
||||
| `-DTEST=1` | Enable regression tests<br> **Note:** `make test` to run tests, *googletest* will be downloaded if not available |
|
||||
| `-DFORCE_USE_QT5=1` | Skip looking for Qt6 before trying to find Qt5 |
|
||||
|
||||
|
||||
# Run
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
# Find a compatible Qt version
|
||||
# Inputs: WITH_SERVER, WITH_CLIENT, WITH_ORACLE, FORCE_USE_QT5
|
||||
# Inputs: WITH_SERVER, WITH_CLIENT, WITH_ORACLE
|
||||
# Optional Input: QT6_DIR -- Hint as to where Qt6 lives on the system
|
||||
# Optional Input: QT5_DIR -- Hint as to where Qt5 lives on the system
|
||||
# Output: COCKATRICE_QT_VERSION_NAME -- Example values: Qt5, Qt6
|
||||
# Output: COCKATRICE_QT_VERSION_NAME -- Example values: Qt6
|
||||
# Output: SERVATRICE_QT_MODULES
|
||||
# Output: COCKATRICE_QT_MODULES
|
||||
# Output: ORACLE_QT_MODULES
|
||||
|
|
@ -39,69 +38,37 @@ set(REQUIRED_QT_COMPONENTS ${REQUIRED_QT_COMPONENTS} ${_SERVATRICE_NEEDED} ${_CO
|
|||
)
|
||||
list(REMOVE_DUPLICATES REQUIRED_QT_COMPONENTS)
|
||||
|
||||
if(NOT FORCE_USE_QT5)
|
||||
# Linguist is now a component in Qt6 instead of an external package
|
||||
find_package(
|
||||
Qt6 6.4.2
|
||||
COMPONENTS ${REQUIRED_QT_COMPONENTS} Linguist
|
||||
QUIET HINTS ${Qt6_DIR}
|
||||
)
|
||||
# Linguist is now a component in Qt6 instead of an external package
|
||||
find_package(
|
||||
Qt6 6.4.2
|
||||
COMPONENTS ${REQUIRED_QT_COMPONENTS} Linguist
|
||||
QUIET HINTS ${Qt6_DIR}
|
||||
)
|
||||
if(NOT Qt6_FOUND)
|
||||
message(FATAL_ERROR "No suitable version of Qt was found")
|
||||
endif()
|
||||
if(Qt6_FOUND)
|
||||
set(COCKATRICE_QT_VERSION_NAME Qt6)
|
||||
set(COCKATRICE_QT_VERSION_NAME Qt6)
|
||||
|
||||
list(FIND Qt6LinguistTools_TARGETS Qt6::lrelease QT6_LRELEASE_INDEX)
|
||||
if(QT6_LRELEASE_INDEX EQUAL -1)
|
||||
message(WARNING "Qt6 lrelease not found.")
|
||||
endif()
|
||||
|
||||
list(FIND Qt6LinguistTools_TARGETS Qt6::lupdate QT6_LUPDATE_INDEX)
|
||||
if(QT6_LUPDATE_INDEX EQUAL -1)
|
||||
message(WARNING "Qt6 lupdate not found.")
|
||||
endif()
|
||||
else()
|
||||
find_package(
|
||||
Qt5 5.15.2
|
||||
COMPONENTS ${REQUIRED_QT_COMPONENTS}
|
||||
QUIET HINTS ${Qt5_DIR}
|
||||
)
|
||||
if(Qt5_FOUND)
|
||||
set(COCKATRICE_QT_VERSION_NAME Qt5)
|
||||
else()
|
||||
message(FATAL_ERROR "No suitable version of Qt was found")
|
||||
endif()
|
||||
|
||||
# Qt5 Linguist is in a separate package
|
||||
find_package(Qt5LinguistTools QUIET)
|
||||
if(Qt5LinguistTools_FOUND)
|
||||
if(NOT Qt5_LRELEASE_EXECUTABLE)
|
||||
message(WARNING "Qt5 lrelease not found.")
|
||||
endif()
|
||||
if(NOT Qt5_LUPDATE_EXECUTABLE)
|
||||
message(WARNING "Qt5 lupdate not found.")
|
||||
endif()
|
||||
else()
|
||||
message(WARNING "Linguist Tools not found, cannot handle translations")
|
||||
endif()
|
||||
list(FIND Qt6LinguistTools_TARGETS Qt6::lrelease QT6_LRELEASE_INDEX)
|
||||
if(QT6_LRELEASE_INDEX EQUAL -1)
|
||||
message(WARNING "Qt6 lrelease not found.")
|
||||
endif()
|
||||
|
||||
if(Qt5_POSITION_INDEPENDENT_CODE OR Qt6_FOUND)
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
list(FIND Qt6LinguistTools_TARGETS Qt6::lupdate QT6_LUPDATE_INDEX)
|
||||
if(QT6_LUPDATE_INDEX EQUAL -1)
|
||||
message(WARNING "Qt6 lupdate not found.")
|
||||
endif()
|
||||
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
|
||||
# Establish Qt Plugins directory & Library directories
|
||||
get_target_property(QT_LIBRARY_DIR ${COCKATRICE_QT_VERSION_NAME}::Core LOCATION)
|
||||
get_filename_component(QT_LIBRARY_DIR ${QT_LIBRARY_DIR} DIRECTORY)
|
||||
if(Qt6_FOUND)
|
||||
get_filename_component(QT_PLUGINS_DIR "${Qt6Core_DIR}/../../../${QT6_INSTALL_PLUGINS}" ABSOLUTE)
|
||||
get_filename_component(QT_LIBRARY_DIR "${QT_LIBRARY_DIR}/../../.." ABSOLUTE)
|
||||
if(UNIX AND APPLE)
|
||||
# Mac needs a bit more help finding all necessary components
|
||||
list(APPEND QT_LIBRARY_DIR "/usr/local/lib")
|
||||
endif()
|
||||
elseif(Qt5_FOUND)
|
||||
get_filename_component(QT_PLUGINS_DIR "${Qt5Core_DIR}/../../../plugins" ABSOLUTE)
|
||||
get_filename_component(QT_LIBRARY_DIR "${QT_LIBRARY_DIR}/.." ABSOLUTE)
|
||||
get_filename_component(QT_PLUGINS_DIR "${Qt6Core_DIR}/../../../${QT6_INSTALL_PLUGINS}" ABSOLUTE)
|
||||
get_filename_component(QT_LIBRARY_DIR "${QT_LIBRARY_DIR}/../../.." ABSOLUTE)
|
||||
if(UNIX AND APPLE)
|
||||
# Mac needs a bit more help finding all necessary components
|
||||
list(APPEND QT_LIBRARY_DIR "/usr/local/lib")
|
||||
endif()
|
||||
message(DEBUG "QT_PLUGINS_DIR = ${QT_PLUGINS_DIR}")
|
||||
message(DEBUG "QT_LIBRARY_DIR = ${QT_LIBRARY_DIR}")
|
||||
|
|
|
|||
|
|
@ -1,38 +1,118 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
|
||||
<!-- ========================= -->
|
||||
<!-- CORE BUNDLE METADATA -->
|
||||
<!-- ========================= -->
|
||||
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>English</string>
|
||||
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
|
||||
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>${MACOSX_BUNDLE_INFO_STRING}</string>
|
||||
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>${MACOSX_BUNDLE_ICON_FILE}</string>
|
||||
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
|
||||
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
|
||||
<key>CFBundleLongVersionString</key>
|
||||
<string>${MACOSX_BUNDLE_LONG_VERSION_STRING}</string>
|
||||
|
||||
<key>CFBundleName</key>
|
||||
<string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
|
||||
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
|
||||
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
|
||||
<key>CSResourcesFileMapped</key>
|
||||
<true/>
|
||||
<key>LSRequiresCarbon</key>
|
||||
<true/>
|
||||
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>${MACOSX_BUNDLE_COPYRIGHT}</string>
|
||||
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
|
||||
<!-- ========================= -->
|
||||
<!-- FILE TYPE (.cod) SUPPORT -->
|
||||
<!-- ========================= -->
|
||||
|
||||
<key>UTExportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>org.cockatrice.deck</string>
|
||||
|
||||
<key>UTTypeDescription</key>
|
||||
<string>Cockatrice Deck</string>
|
||||
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.data</string>
|
||||
</array>
|
||||
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>cod</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Cockatrice Deck</string>
|
||||
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Default</string>
|
||||
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>org.cockatrice.deck</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
|
||||
<!-- ========================= -->
|
||||
<!-- URL SCHEME (cockatrice://) -->
|
||||
<!-- ========================= -->
|
||||
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>Cockatrice URL Scheme</string>
|
||||
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>cockatrice</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -36,13 +36,16 @@ set(cockatrice_SOURCES
|
|||
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_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_from_clipboard.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_local_game_options.cpp
|
||||
src/interface/widgets/dialogs/dlg_manage_sets.cpp
|
||||
src/interface/widgets/dialogs/dlg_my_reports.cpp
|
||||
src/interface/widgets/dialogs/dlg_register.cpp
|
||||
src/interface/widgets/dialogs/dlg_report_user.cpp
|
||||
src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp
|
||||
src/interface/widgets/dialogs/dlg_settings.cpp
|
||||
src/interface/widgets/dialogs/dlg_startup_card_check.cpp
|
||||
|
|
@ -83,7 +86,6 @@ set(cockatrice_SOURCES
|
|||
src/game/game_state.cpp
|
||||
src/game_graphics/game_view.cpp
|
||||
src/game_graphics/hand_counter.cpp
|
||||
src/game/selection_subtype_tally.cpp
|
||||
src/game_graphics/log/message_log_widget.cpp
|
||||
src/game/phase.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/say_menu.cpp
|
||||
src/game_graphics/player/menu/sideboard_menu.cpp
|
||||
src/game_graphics/player/menu/tally_menu.cpp
|
||||
src/game_graphics/player/menu/utility_menu.cpp
|
||||
src/game_graphics/tally/stats_tally.cpp
|
||||
src/game_graphics/tally/subtype_tally.cpp
|
||||
src/game_graphics/tally/tally.cpp
|
||||
src/game/player/player_actions.cpp
|
||||
src/game_graphics/player/player_area.cpp
|
||||
src/game_graphics/player/player_dialogs.cpp
|
||||
|
|
@ -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_work.cpp
|
||||
src/interface/card_picture_loader/card_picture_to_load.cpp
|
||||
src/interface/intents/intent.cpp
|
||||
src/interface/intents/intent.h
|
||||
src/interface/intents/intent_open_local_deck.cpp
|
||||
src/interface/intents/intent_open_local_deck.h
|
||||
src/interface/intents/intent_wait_for_database_load.cpp
|
||||
src/interface/intents/intent_wait_for_database_load.h
|
||||
src/interface/layouts/flow_layout.cpp
|
||||
src/interface/layouts/overlap_layout.cpp
|
||||
src/interface/widgets/utility/card_completer_delegate.cpp
|
||||
src/interface/widgets/utility/card_completer_styler.cpp
|
||||
src/interface/widgets/utility/completer_utils.cpp
|
||||
src/interface/widgets/utility/line_edit_completer.cpp
|
||||
src/interface/widgets/utility/reversed_completer_model.cpp
|
||||
src/interface/pixel_map_generator.cpp
|
||||
src/interface/theme_config.cpp
|
||||
src/interface/theme_manager.cpp
|
||||
|
|
@ -146,6 +162,7 @@ set(cockatrice_SOURCES
|
|||
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_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/flat_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_sorting_widget.cpp
|
||||
src/interface/widgets/printing_selector/set_name_and_collectors_number_display_widget.cpp
|
||||
src/interface/widgets/playmat/playmat_collection_dialog.cpp
|
||||
src/interface/widgets/playmat/playmat_collection_dialog.h
|
||||
src/interface/widgets/playmat/playmat_preview_widget.cpp
|
||||
src/interface/widgets/playmat/playmat_settings_dialog.cpp
|
||||
src/interface/widgets/quick_settings/settings_button_widget.cpp
|
||||
src/interface/widgets/quick_settings/settings_popup_widget.cpp
|
||||
src/interface/widgets/replay/replay_manager.cpp
|
||||
src/interface/widgets/replay/replay_quick_settings_widget.cpp
|
||||
src/interface/widgets/replay/replay_timeline_widget.cpp
|
||||
src/interface/widgets/replay/replay_widget.cpp
|
||||
src/interface/widgets/server/chat_view/chat_view.cpp
|
||||
src/interface/widgets/server/game_filter_configs.cpp
|
||||
src/interface/widgets/server/game_link.cpp
|
||||
src/interface/widgets/server/game_selector.cpp
|
||||
src/interface/widgets/server/game_selector_quick_filter_toolbar.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_list_manager.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/settings_page/abstract_settings_page.cpp
|
||||
src/interface/widgets/settings_page/appearance_settings_page.cpp
|
||||
src/interface/widgets/settings_page/deck_editor_settings_page.cpp
|
||||
src/interface/widgets/settings_page/general_settings_page.cpp
|
||||
src/interface/widgets/settings_page/messages_settings_page.cpp
|
||||
src/interface/widgets/settings_page/settings_search_delegate.cpp
|
||||
src/interface/widgets/settings_page/settings_search_model.cpp
|
||||
src/interface/widgets/settings_page/shortcut_settings_page.cpp
|
||||
src/interface/widgets/settings_page/sound_settings_page.cpp
|
||||
src/interface/widgets/settings_page/storage_settings_page.cpp
|
||||
src/interface/widgets/settings_page/user_interface_settings_page.cpp
|
||||
src/interface/widgets/utility/custom_line_edit.cpp
|
||||
src/interface/widgets/utility/get_text_with_max.cpp
|
||||
src/interface/widgets/utility/report_utils.cpp
|
||||
src/interface/widgets/utility/report_utils.h
|
||||
src/interface/widgets/utility/sequence_edit.cpp
|
||||
src/interface/widgets/utility/visibility_change_listener.cpp
|
||||
src/interface/widgets/utility/visibility_change_listener.h
|
||||
|
|
@ -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_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_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_tag_filter_widget.cpp
|
||||
src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp
|
||||
src/interface/window_main.cpp
|
||||
src/main.cpp
|
||||
src/single_instance_manager.cpp
|
||||
src/interface/widgets/tabs/abstract_tab_deck_editor.cpp
|
||||
src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp
|
||||
src/interface/widgets/tabs/api/archidekt/api_response/archidekt_deck_listing_api_response.cpp
|
||||
|
|
@ -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_listings_display_widget.cpp
|
||||
src/interface/widgets/tabs/api/archidekt/display/archidekt_deck_preview_image_display_widget.cpp
|
||||
src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.cpp
|
||||
src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.cpp
|
||||
src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.cpp
|
||||
src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.cpp
|
||||
src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.cpp
|
||||
src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.cpp
|
||||
src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.cpp
|
||||
src/interface/widgets/tabs/api/edhrec/api_response/archidekt_links/edhrec_api_response_archidekt_links.cpp
|
||||
src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_average_deck_api_response.cpp
|
||||
src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_deck_api_response.cpp
|
||||
|
|
@ -339,6 +379,8 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/tabs/tab_home.cpp
|
||||
src/interface/widgets/tabs/tab_logs.cpp
|
||||
src/interface/widgets/tabs/tab_message.cpp
|
||||
src/interface/widgets/tabs/tab_moderation.cpp
|
||||
src/interface/widgets/tabs/tab_report.cpp
|
||||
src/interface/widgets/tabs/tab_replays.cpp
|
||||
src/interface/widgets/tabs/tab_room.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/key_signals.cpp
|
||||
src/interface/logger.cpp
|
||||
src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.cpp
|
||||
src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp
|
||||
src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp
|
||||
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.cpp
|
||||
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.h
|
||||
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.cpp
|
||||
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.h
|
||||
src/interface/widgets/utility/compact_push_button.cpp
|
||||
src/interface/widgets/utility/compact_push_button.h
|
||||
src/single_instance_manager.h
|
||||
src/client/url_scheme_event_filter.h
|
||||
src/interface/intents/intent_connect_to_server.cpp
|
||||
src/interface/intents/intent_connect_to_server.h
|
||||
src/interface/intents/intent_disconnect_from_server.cpp
|
||||
src/interface/intents/intent_disconnect_from_server.h
|
||||
src/interface/intents/intent_join_server_game.cpp
|
||||
src/interface/intents/intent_join_server_game.h
|
||||
src/interface/intents/intent_join_server_room.cpp
|
||||
src/interface/intents/intent_join_server_room.h
|
||||
src/interface/intents/intent_login.cpp
|
||||
src/interface/intents/intent_login.h
|
||||
src/interface/intents/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.h
|
||||
)
|
||||
|
|
@ -398,11 +459,7 @@ if(APPLE)
|
|||
set(cockatrice_SOURCES ${cockatrice_SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/resources/appicon.icns)
|
||||
endif(APPLE)
|
||||
|
||||
if(Qt6_FOUND)
|
||||
qt6_add_resources(cockatrice_RESOURCES_RCC ${cockatrice_RESOURCES})
|
||||
elseif(Qt5_FOUND)
|
||||
qt5_add_resources(cockatrice_RESOURCES_RCC ${cockatrice_RESOURCES})
|
||||
endif()
|
||||
qt6_add_resources(cockatrice_RESOURCES_RCC ${cockatrice_RESOURCES})
|
||||
|
||||
# Declare path variables
|
||||
set(ICONDIR
|
||||
|
|
@ -414,71 +471,37 @@ set(DESKTOPDIR
|
|||
CACHE STRING "desktop file destination"
|
||||
)
|
||||
|
||||
set(MIMEDIR
|
||||
share/mime/packages
|
||||
CACHE STRING "mime file destination"
|
||||
)
|
||||
|
||||
set(COCKATRICE_MAC_QM_INSTALL_DIR "cockatrice.app/Contents/Resources/translations")
|
||||
set(COCKATRICE_UNIX_QM_INSTALL_DIR "share/cockatrice/translations")
|
||||
set(COCKATRICE_WIN32_QM_INSTALL_DIR "translations")
|
||||
|
||||
if(Qt6_FOUND)
|
||||
qt6_add_executable(
|
||||
cockatrice
|
||||
WIN32
|
||||
MACOSX_BUNDLE
|
||||
${cockatrice_SOURCES}
|
||||
${cockatrice_RESOURCES_RCC}
|
||||
${cockatrice_MOC_SRCS}
|
||||
MANUAL_FINALIZATION
|
||||
)
|
||||
elseif(Qt5_FOUND)
|
||||
# Qt5 Translations need to be linked at executable creation time
|
||||
if(Qt5LinguistTools_FOUND)
|
||||
if(UPDATE_TRANSLATIONS)
|
||||
qt5_create_translation(cockatrice_QM ${translate_SRCS} ${cockatrice_TS})
|
||||
else()
|
||||
qt5_add_translation(cockatrice_QM ${cockatrice_TS})
|
||||
endif()
|
||||
endif()
|
||||
add_executable(
|
||||
cockatrice WIN32 MACOSX_BUNDLE ${cockatrice_MOC_SRCS} ${cockatrice_QM} ${cockatrice_RESOURCES_RCC}
|
||||
${cockatrice_SOURCES}
|
||||
)
|
||||
if(UNIX)
|
||||
if(APPLE)
|
||||
install(FILES ${cockatrice_QM} DESTINATION ${COCKATRICE_MAC_QM_INSTALL_DIR})
|
||||
else()
|
||||
install(FILES ${cockatrice_QM} DESTINATION ${COCKATRICE_UNIX_QM_INSTALL_DIR})
|
||||
endif()
|
||||
elseif(WIN32)
|
||||
install(FILES ${cockatrice_QM} DESTINATION ${COCKATRICE_WIN32_QM_INSTALL_DIR})
|
||||
endif()
|
||||
endif()
|
||||
qt6_add_executable(
|
||||
cockatrice
|
||||
WIN32
|
||||
MACOSX_BUNDLE
|
||||
${cockatrice_SOURCES}
|
||||
${cockatrice_RESOURCES_RCC}
|
||||
${cockatrice_MOC_SRCS}
|
||||
MANUAL_FINALIZATION
|
||||
)
|
||||
|
||||
if(Qt5_FOUND)
|
||||
target_link_libraries(
|
||||
cockatrice
|
||||
libcockatrice_card
|
||||
libcockatrice_deck_list
|
||||
libcockatrice_filters
|
||||
libcockatrice_utility
|
||||
libcockatrice_network
|
||||
libcockatrice_models
|
||||
libcockatrice_rng
|
||||
libcockatrice_settings
|
||||
${COCKATRICE_QT_MODULES}
|
||||
)
|
||||
else()
|
||||
target_link_libraries(
|
||||
cockatrice
|
||||
PUBLIC libcockatrice_card
|
||||
libcockatrice_deck_list
|
||||
libcockatrice_filters
|
||||
libcockatrice_utility
|
||||
libcockatrice_network
|
||||
libcockatrice_models
|
||||
libcockatrice_rng
|
||||
libcockatrice_settings
|
||||
${COCKATRICE_QT_MODULES}
|
||||
)
|
||||
endif()
|
||||
target_link_libraries(
|
||||
cockatrice
|
||||
PUBLIC libcockatrice_card
|
||||
libcockatrice_deck_list
|
||||
libcockatrice_filters
|
||||
libcockatrice_utility
|
||||
libcockatrice_network
|
||||
libcockatrice_models
|
||||
libcockatrice_rng
|
||||
libcockatrice_settings
|
||||
${COCKATRICE_QT_MODULES}
|
||||
)
|
||||
|
||||
if(UNIX)
|
||||
if(APPLE)
|
||||
|
|
@ -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.svg DESTINATION ${ICONDIR}/hicolor/scalable/apps)
|
||||
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/cockatrice.desktop DESTINATION ${DESKTOPDIR})
|
||||
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/cockatrice-cod.xml DESTINATION ${MIMEDIR})
|
||||
|
||||
# Refresh the freedesktop databases so the file associations and scheme
|
||||
# handler register without requiring the user to run them manually. The
|
||||
# tools may be missing on minimal systems; that is fine, packaging systems
|
||||
# usually refresh these databases through their own triggers.
|
||||
find_program(UPDATE_MIME_DATABASE update-mime-database)
|
||||
if(UPDATE_MIME_DATABASE)
|
||||
install(CODE "execute_process(COMMAND \"${UPDATE_MIME_DATABASE}\" \"${CMAKE_INSTALL_PREFIX}/share/mime\")")
|
||||
endif()
|
||||
|
||||
find_program(UPDATE_DESKTOP_DATABASE update-desktop-database)
|
||||
if(UPDATE_DESKTOP_DATABASE)
|
||||
install(
|
||||
CODE "execute_process(COMMAND \"${UPDATE_DESKTOP_DATABASE}\" \"${CMAKE_INSTALL_PREFIX}/share/applications\")"
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
elseif(WIN32)
|
||||
install(TARGETS cockatrice RUNTIME DESTINATION ./)
|
||||
|
|
@ -508,7 +548,7 @@ if(APPLE)
|
|||
set(plugin_dest_dir cockatrice.app/Contents/Plugins)
|
||||
set(qtconf_dest_dir cockatrice.app/Contents/Resources)
|
||||
|
||||
# Qt plugins: audio (Qt5), iconengines, imageformats, multimedia (Qt6), platforms, printsupport (Qt5), styles, tls (Qt6)
|
||||
# Qt plugins: audio, iconengines, imageformats, multimedia, platforms, printsupport, styles, tls
|
||||
install(
|
||||
DIRECTORY "${QT_PLUGINS_DIR}/"
|
||||
DESTINATION ${plugin_dest_dir}
|
||||
|
|
@ -575,7 +615,7 @@ if(WIN32)
|
|||
PATTERN "*.ini"
|
||||
)
|
||||
|
||||
# Qt plugins: audio (Qt5), iconengines, imageformats, multimedia (Qt6) platforms, printsupport (Qt5), styles, tls (Qt6)
|
||||
# Qt plugins: audio, iconengines, imageformats, multimedia, platforms, printsupport, styles, tls
|
||||
install(
|
||||
DIRECTORY "${QT_PLUGINS_DIR}/"
|
||||
DESTINATION ${plugin_dest_dir}
|
||||
|
|
@ -631,7 +671,7 @@ Data = Resources\")
|
|||
endif()
|
||||
endif()
|
||||
|
||||
if(Qt6_FOUND AND Qt6LinguistTools_FOUND)
|
||||
if(Qt6LinguistTools_FOUND)
|
||||
#Qt6 Translations happen after the executable is built up
|
||||
if(UPDATE_TRANSLATIONS)
|
||||
qt6_add_translations(
|
||||
|
|
@ -658,6 +698,4 @@ if(Qt6_FOUND AND Qt6LinguistTools_FOUND)
|
|||
endif()
|
||||
endif()
|
||||
|
||||
if(Qt6_FOUND)
|
||||
qt6_finalize_target(cockatrice)
|
||||
endif()
|
||||
qt6_finalize_target(cockatrice)
|
||||
|
|
|
|||
7
cockatrice/cockatrice-cod.xml
Normal file
7
cockatrice/cockatrice-cod.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
|
||||
<mime-type type="application/x-cockatrice">
|
||||
<comment>Cockatrice Deck File</comment>
|
||||
<glob pattern="*.cod"/>
|
||||
</mime-type>
|
||||
</mime-info>
|
||||
|
|
@ -3,6 +3,8 @@
|
|||
Version=1.0
|
||||
Type=Application
|
||||
Name=Cockatrice
|
||||
Exec=cockatrice
|
||||
Exec=cockatrice %U
|
||||
Icon=cockatrice
|
||||
Categories=Game;CardGame;
|
||||
MimeType=application/x-cockatrice;
|
||||
X-Scheme-Handler/cockatrice=true
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -14,6 +14,7 @@
|
|||
#include <QThread>
|
||||
#include <libcockatrice/network/client/remote/remote_client.h>
|
||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||
#include <libcockatrice/settings/servers_settings.h>
|
||||
|
||||
ConnectionController::ConnectionController(QWidget *dialogParent, QObject *parent)
|
||||
: QObject(parent), dialogParent(dialogParent)
|
||||
|
|
@ -295,6 +296,15 @@ void ConnectionController::onLoginError(int r,
|
|||
return;
|
||||
}
|
||||
|
||||
case Response::RespPasswordChangeRequired: {
|
||||
QMessageBox::information(
|
||||
dialogParent, tr("Password Change Required"),
|
||||
tr("An administrator has reset your password. Please contact your server administrator to obtain "
|
||||
"your temporary password, then log in and change it via Account -> Change Password."));
|
||||
remoteClient->disconnectFromServer();
|
||||
return;
|
||||
}
|
||||
|
||||
case Response::RespServerFull: {
|
||||
QMessageBox::critical(dialogParent, tr("Server Full"),
|
||||
tr("The server has reached its maximum user capacity, please check back later."));
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@
|
|||
#include <QtConcurrent>
|
||||
#include <libcockatrice/card/database/card_database.h>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/settings/download_settings.h>
|
||||
#include <libcockatrice/settings/paths_settings.h>
|
||||
#include <version_string.h>
|
||||
|
||||
#define SPOILERS_STATUS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/SpoilerSeasonEnabled"
|
||||
|
|
@ -21,7 +23,7 @@
|
|||
|
||||
SpoilerBackgroundUpdater::SpoilerBackgroundUpdater(QObject *apParent) : QObject(apParent), cardUpdateProcess(nullptr)
|
||||
{
|
||||
isSpoilerDownloadEnabled = SettingsCache::instance().getDownloadSpoilersStatus();
|
||||
isSpoilerDownloadEnabled = SettingsCache::instance().downloads().getDownloadSpoilersStatus();
|
||||
if (isSpoilerDownloadEnabled) {
|
||||
// Start the process of checking if we're in spoiler season
|
||||
// File exists means we're in spoiler season
|
||||
|
|
@ -75,7 +77,7 @@ void SpoilerBackgroundUpdater::actDownloadFinishedSpoilersFile()
|
|||
|
||||
bool SpoilerBackgroundUpdater::deleteSpoilerFile()
|
||||
{
|
||||
QString fileName = SettingsCache::instance().getSpoilerCardDatabasePath();
|
||||
QString fileName = SettingsCache::instance().paths().getSpoilerCardDatabasePath();
|
||||
QFileInfo fi(fileName);
|
||||
QDir fileDir(fi.path());
|
||||
QFile file(fileName);
|
||||
|
|
@ -126,7 +128,7 @@ void SpoilerBackgroundUpdater::actCheckIfSpoilerSeasonEnabled()
|
|||
|
||||
bool SpoilerBackgroundUpdater::saveDownloadedFile(QByteArray data)
|
||||
{
|
||||
QString fileName = SettingsCache::instance().getSpoilerCardDatabasePath();
|
||||
QString fileName = SettingsCache::instance().paths().getSpoilerCardDatabasePath();
|
||||
QFileInfo fi(fileName);
|
||||
QDir fileDir(fi.path());
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -5,7 +5,7 @@
|
|||
#include <QtMath>
|
||||
|
||||
CardCounterSettings::CardCounterSettings(const QString &settingsPath, QObject *parent)
|
||||
: SettingsManager(settingsPath + "global.ini", "cards", "counters", parent)
|
||||
: SettingsManager(settingsPath + "card_counters.ini", "cards", "counters", parent)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -786,6 +786,10 @@ private:
|
|||
ShortcutGroup::Tabs)},
|
||||
{"Tabs/aTabLogs",
|
||||
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Logs"), parseSequenceString(""), ShortcutGroup::Tabs)},
|
||||
{"Tabs/aTabReport",
|
||||
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Report Queue"), parseSequenceString(""), ShortcutGroup::Tabs)},
|
||||
{"Tabs/aTabModeration",
|
||||
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Moderation"), parseSequenceString(""), ShortcutGroup::Tabs)},
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2,12 +2,11 @@
|
|||
|
||||
#include "settings/cache_settings.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QAudioOutput>
|
||||
#include <QDir>
|
||||
#include <QMediaPlayer>
|
||||
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
|
||||
#include <QAudioOutput>
|
||||
#endif
|
||||
#include <libcockatrice/settings/sound_settings.h>
|
||||
|
||||
#define DEFAULT_THEME_NAME "Default"
|
||||
#define TEST_SOUND_FILENAME "player_join"
|
||||
|
|
@ -15,8 +14,10 @@
|
|||
SoundEngine::SoundEngine(QObject *parent) : QObject(parent), audioOutput(nullptr), player(nullptr)
|
||||
{
|
||||
ensureThemeDirectoryExists();
|
||||
connect(&SettingsCache::instance(), &SettingsCache::soundThemeChanged, this, &SoundEngine::themeChangedSlot);
|
||||
connect(&SettingsCache::instance(), &SettingsCache::soundEnabledChanged, this, &SoundEngine::soundEnabledChanged);
|
||||
connect(&SettingsCache::instance().sound(), &SoundSettings::soundThemeChanged, this,
|
||||
&SoundEngine::themeChangedSlot);
|
||||
connect(&SettingsCache::instance().sound(), &SoundSettings::soundEnabledChanged, this,
|
||||
&SoundEngine::soundEnabledChanged);
|
||||
|
||||
soundEnabledChanged();
|
||||
themeChangedSlot();
|
||||
|
|
@ -36,14 +37,12 @@ SoundEngine::~SoundEngine()
|
|||
|
||||
void SoundEngine::soundEnabledChanged()
|
||||
{
|
||||
if (SettingsCache::instance().getSoundEnabled()) {
|
||||
if (SettingsCache::instance().sound().getSoundEnabled()) {
|
||||
qCInfo(SoundEngineLog) << "SoundEngine: enabling sound with" << audioData.size() << "sounds";
|
||||
if (!player) {
|
||||
player = new QMediaPlayer;
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
|
||||
audioOutput = new QAudioOutput(player);
|
||||
player->setAudioOutput(audioOutput);
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
qCInfo(SoundEngineLog) << "SoundEngine: disabling sound";
|
||||
|
|
@ -70,14 +69,9 @@ void SoundEngine::playSound(const QString &fileName)
|
|||
}
|
||||
|
||||
player->stop();
|
||||
int volumeSliderValue = SettingsCache::instance().getMasterVolume();
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
|
||||
int volumeSliderValue = SettingsCache::instance().sound().getMasterVolume();
|
||||
player->audioOutput()->setVolume(qreal(volumeSliderValue) / 100);
|
||||
player->setSource(QUrl::fromLocalFile(audioData[fileName]));
|
||||
#else
|
||||
player->setVolume(volumeSliderValue);
|
||||
player->setMedia(QUrl::fromLocalFile(audioData[fileName]));
|
||||
#endif
|
||||
player->play();
|
||||
}
|
||||
|
||||
|
|
@ -88,10 +82,10 @@ void SoundEngine::testSound()
|
|||
|
||||
void SoundEngine::ensureThemeDirectoryExists()
|
||||
{
|
||||
if (SettingsCache::instance().getSoundThemeName().isEmpty() ||
|
||||
!getAvailableThemes().contains(SettingsCache::instance().getSoundThemeName())) {
|
||||
if (SettingsCache::instance().sound().getSoundThemeName().isEmpty() ||
|
||||
!getAvailableThemes().contains(SettingsCache::instance().sound().getSoundThemeName())) {
|
||||
qCInfo(SoundEngineLog) << "Sounds theme name not set, setting default value";
|
||||
SettingsCache::instance().setSoundThemeName(DEFAULT_THEME_NAME);
|
||||
SettingsCache::instance().sound().setSoundThemeName(DEFAULT_THEME_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -132,7 +126,7 @@ QStringMap &SoundEngine::getAvailableThemes()
|
|||
|
||||
void SoundEngine::themeChangedSlot()
|
||||
{
|
||||
QString themeName = SettingsCache::instance().getSoundThemeName();
|
||||
QString themeName = SettingsCache::instance().sound().getSoundThemeName();
|
||||
qCInfo(SoundEngineLog) << "Sound theme changed:" << themeName;
|
||||
|
||||
QDir dir = getAvailableThemes().value(themeName);
|
||||
|
|
|
|||
69
cockatrice/src/client/url_scheme_event_filter.h
Normal file
69
cockatrice/src/client/url_scheme_event_filter.h
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
#ifndef COCKATRICE_URL_SCHEME_EVENT_FILTER_H
|
||||
#define COCKATRICE_URL_SCHEME_EVENT_FILTER_H
|
||||
|
||||
#include <QEvent>
|
||||
#include <QFileOpenEvent>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
/**
|
||||
* @brief Event filter that catches QFileOpenEvent URLs matching a scheme and
|
||||
* re-emits them as urlReceived().
|
||||
*
|
||||
* On macOS, when the application is registered as a URL scheme handler, the
|
||||
* OS delivers incoming URLs via QFileOpenEvent on the QApplication object.
|
||||
* Install this filter on QApplication to intercept them:
|
||||
*
|
||||
* @code
|
||||
* UrlSchemeEventFilter filter(QStringList{QStringLiteral("cockatrice")});
|
||||
* QObject::connect(&filter, &UrlSchemeEventFilter::urlReceived,
|
||||
* &mainWindow, &MainWindow::handleUrl);
|
||||
* app.installEventFilter(&filter);
|
||||
* @endcode
|
||||
*
|
||||
* Note: the strings are compared against QUrl::scheme(), so they must be
|
||||
* written without the "://" suffix (e.g. "cockatrice", not "cockatrice://").
|
||||
*/
|
||||
class UrlSchemeEventFilter : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit UrlSchemeEventFilter(const QStringList &schemes, QObject *parent = nullptr)
|
||||
: QObject(parent), prefixes(schemes)
|
||||
{
|
||||
}
|
||||
|
||||
signals:
|
||||
void urlReceived(const QString &url);
|
||||
|
||||
public:
|
||||
bool eventFilter(QObject *watched, QEvent *event) override
|
||||
{
|
||||
if (event->type() == QEvent::FileOpen) {
|
||||
auto *fileEvent = static_cast<QFileOpenEvent *>(event);
|
||||
|
||||
const QUrl url = fileEvent->url();
|
||||
|
||||
for (const auto &prefix : prefixes) {
|
||||
if (url.scheme() == prefix) {
|
||||
emit urlReceived(url.toString());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (url.isLocalFile()) {
|
||||
emit urlReceived(url.toLocalFile());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return QObject::eventFilter(watched, event);
|
||||
}
|
||||
|
||||
private:
|
||||
QStringList prefixes;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_URL_SCHEME_EVENT_FILTER_H
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
#ifndef SETTINGS_CARD_DATABASE_PATH_PROVIDER_H
|
||||
#define SETTINGS_CARD_DATABASE_PATH_PROVIDER_H
|
||||
|
||||
#include "../../client/settings/cache_settings.h"
|
||||
|
||||
#include <libcockatrice/interfaces/interface_card_database_path_provider.h>
|
||||
#include <libcockatrice/settings/paths_settings.h>
|
||||
|
||||
class SettingsCardDatabasePathProvider : public ICardDatabasePathProvider
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SettingsCardDatabasePathProvider(QObject *parent = nullptr) : ICardDatabasePathProvider(parent)
|
||||
{
|
||||
connect(&SettingsCache::instance().paths(), &PathsSettings::cardDatabasePathChanged, this,
|
||||
&ICardDatabasePathProvider::cardDatabasePathChanged);
|
||||
}
|
||||
|
||||
[[nodiscard]] QString getCardDatabasePath() const override
|
||||
{
|
||||
return SettingsCache::instance().paths().getCardDatabasePath();
|
||||
}
|
||||
|
||||
[[nodiscard]] QString getCustomCardDatabasePath() const override
|
||||
{
|
||||
return SettingsCache::instance().paths().getCustomCardDatabasePath();
|
||||
}
|
||||
|
||||
[[nodiscard]] QString getTokenDatabasePath() const override
|
||||
{
|
||||
return SettingsCache::instance().paths().getTokenDatabasePath();
|
||||
}
|
||||
|
||||
[[nodiscard]] virtual QString getSpoilerCardDatabasePath() const override
|
||||
{
|
||||
return SettingsCache::instance().paths().getSpoilerCardDatabasePath();
|
||||
}
|
||||
};
|
||||
|
||||
#endif // SETTINGS_CARD_DATABASE_PATH_PROVIDER_H
|
||||
|
|
@ -3,6 +3,8 @@
|
|||
#include "../../client/settings/cache_settings.h"
|
||||
|
||||
#include <libcockatrice/interfaces/interface_card_preference_provider.h>
|
||||
#include <libcockatrice/settings/card_override_settings.h>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
|
||||
class SettingsCardPreferenceProvider : public ICardPreferenceProvider
|
||||
{
|
||||
|
|
@ -14,7 +16,7 @@ public:
|
|||
|
||||
[[nodiscard]] bool getIncludeRebalancedCards() const override
|
||||
{
|
||||
return SettingsCache::instance().getIncludeRebalancedCards();
|
||||
return SettingsCache::instance().cardsDisplay().getIncludeRebalancedCards();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -52,18 +52,14 @@ static void setupParserRules()
|
|||
|
||||
search["Start"] = passthru;
|
||||
search["QueryPartList"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) {
|
||||
auto matchesFilter = [&deck, &info](const std::any &query) {
|
||||
return std::any_cast<DeckFilter>(query)(deck, info);
|
||||
};
|
||||
return [=](const DeckSearchData &data) {
|
||||
auto matchesFilter = [&data](const std::any &query) { return std::any_cast<DeckFilter>(query)(data); };
|
||||
return std::all_of(sv.begin(), sv.end(), matchesFilter);
|
||||
};
|
||||
};
|
||||
search["ComplexQueryPart"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) {
|
||||
auto matchesFilter = [&deck, &info](const std::any &query) {
|
||||
return std::any_cast<DeckFilter>(query)(deck, info);
|
||||
};
|
||||
return [=](const DeckSearchData &data) {
|
||||
auto matchesFilter = [&data](const std::any &query) { return std::any_cast<DeckFilter>(query)(data); };
|
||||
return std::any_of(sv.begin(), sv.end(), matchesFilter);
|
||||
};
|
||||
};
|
||||
|
|
@ -71,9 +67,7 @@ static void setupParserRules()
|
|||
search["QueryPart"] = passthru;
|
||||
search["NotQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||
const auto dependent = std::any_cast<DeckFilter>(sv[0]);
|
||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) -> bool {
|
||||
return !dependent(deck, info);
|
||||
};
|
||||
return [=](const DeckSearchData &data) -> bool { return !dependent(data); };
|
||||
};
|
||||
|
||||
search["String"] = [](const peg::SemanticValues &sv) -> QString {
|
||||
|
|
@ -125,9 +119,9 @@ static void setupParserRules()
|
|||
auto cardFilter = FilterString(std::any_cast<QString>(sv[0]));
|
||||
auto numberMatcher = sv.size() > 1 ? std::any_cast<NumberMatcher>(sv[1]) : [](int count) { return count > 0; };
|
||||
|
||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) -> bool {
|
||||
return [=](const DeckSearchData &data) -> bool {
|
||||
int count = 0;
|
||||
auto cardNodes = deck->deckLoader->getDeck().deckList.getCardNodes();
|
||||
auto cardNodes = data.deck->deckList.getCardNodes();
|
||||
for (auto node : cardNodes) {
|
||||
auto cardInfoPtr = CardDatabaseManager::query()->getCardInfo(node->getName());
|
||||
if (!cardInfoPtr.isNull() && cardFilter.check(cardInfoPtr)) {
|
||||
|
|
@ -146,53 +140,49 @@ static void setupParserRules()
|
|||
|
||||
search["DeckNameQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||
auto name = std::any_cast<QString>(sv[0]);
|
||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) {
|
||||
return deck->deckLoader->getDeck().deckList.getName().contains(name, Qt::CaseInsensitive);
|
||||
return [=](const DeckSearchData &data) {
|
||||
return data.deck->deckList.getName().contains(name, Qt::CaseInsensitive);
|
||||
};
|
||||
};
|
||||
|
||||
search["FileNameQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||
auto name = std::any_cast<QString>(sv[0]);
|
||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) {
|
||||
auto filename = QFileInfo(deck->filePath).fileName();
|
||||
return [=](const DeckSearchData &data) {
|
||||
auto filename = QFileInfo(data.filePath).fileName();
|
||||
return filename.contains(name, Qt::CaseInsensitive);
|
||||
};
|
||||
};
|
||||
|
||||
search["PathQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||
auto name = std::any_cast<QString>(sv[0]);
|
||||
return [=](const DeckPreviewWidget *, const ExtraDeckSearchInfo &info) {
|
||||
return info.relativeFilePath.contains(name, Qt::CaseInsensitive);
|
||||
};
|
||||
return [=](const DeckSearchData &data) { return data.relativeFilePath.contains(name, Qt::CaseInsensitive); };
|
||||
};
|
||||
|
||||
search["FormatQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||
auto format = std::any_cast<QString>(sv[0]);
|
||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) {
|
||||
auto gameFormat = deck->deckLoader->getDeck().deckList.getGameFormat();
|
||||
return [=](const DeckSearchData &data) {
|
||||
auto gameFormat = data.deck->deckList.getGameFormat();
|
||||
return QString::compare(format, gameFormat, Qt::CaseInsensitive) == 0;
|
||||
};
|
||||
};
|
||||
|
||||
search["CommentQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||
auto value = std::any_cast<QString>(sv[0]);
|
||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) {
|
||||
auto comments = deck->deckLoader->getDeck().deckList.getComments();
|
||||
return [=](const DeckSearchData &data) {
|
||||
auto comments = data.deck->deckList.getComments();
|
||||
return comments.contains(value, Qt::CaseInsensitive);
|
||||
};
|
||||
};
|
||||
|
||||
search["GenericQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||
auto name = std::any_cast<QString>(sv[0]);
|
||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) {
|
||||
return deck->getDisplayName().contains(name, Qt::CaseInsensitive);
|
||||
};
|
||||
return [=](const DeckSearchData &data) { return data.displayName.contains(name, Qt::CaseInsensitive); };
|
||||
};
|
||||
}
|
||||
|
||||
DeckFilterString::DeckFilterString()
|
||||
{
|
||||
filter = [](const DeckPreviewWidget *, const ExtraDeckSearchInfo &) { return false; };
|
||||
filter = [](const DeckSearchData &) { return false; };
|
||||
_error = "Not initialized";
|
||||
}
|
||||
|
||||
|
|
@ -205,7 +195,7 @@ DeckFilterString::DeckFilterString(const QString &expr)
|
|||
_error = QString();
|
||||
|
||||
if (ba.isEmpty()) {
|
||||
filter = [](const DeckPreviewWidget *, const ExtraDeckSearchInfo &) { return true; };
|
||||
filter = [](const DeckSearchData &) { return true; };
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -215,6 +205,6 @@ DeckFilterString::DeckFilterString(const QString &expr)
|
|||
|
||||
if (!search.parse(ba.data(), filter)) {
|
||||
qCInfo(DeckFilterStringLog).nospace() << "DeckFilterString error for " << expr << "; " << qPrintable(_error);
|
||||
filter = [](const DeckPreviewWidget *, const ExtraDeckSearchInfo &) { return false; };
|
||||
filter = [](const DeckSearchData &) { return false; };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
#ifndef DECK_FILTER_STRING_H
|
||||
#define DECK_FILTER_STRING_H
|
||||
|
||||
#include "../interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h"
|
||||
#include "../interface/deck_loader/loaded_deck.h"
|
||||
|
||||
#include <QLoggingCategory>
|
||||
#include <QString>
|
||||
|
|
@ -16,26 +16,29 @@
|
|||
inline Q_LOGGING_CATEGORY(DeckFilterStringLog, "deck_filter_string");
|
||||
|
||||
/**
|
||||
* Extra info relevant to filtering that isn't present in the DeckPreviewWidget
|
||||
* The data a deck search expression is evaluated against.
|
||||
*
|
||||
* This is a data view rather than a widget pointer, so the same filter
|
||||
* expression can be evaluated against a model or a live widget.
|
||||
*/
|
||||
struct ExtraDeckSearchInfo
|
||||
struct DeckSearchData
|
||||
{
|
||||
/**
|
||||
* The relative filepath starting from the deck folder
|
||||
*/
|
||||
QString relativeFilePath;
|
||||
const LoadedDeck *deck = nullptr; ///< The loaded deck. Must not be null.
|
||||
QString filePath; ///< Absolute path of the deck file.
|
||||
QString displayName; ///< Deck name, or the file name if the deck has no name.
|
||||
QString relativeFilePath; ///< File path relative to the deck folder.
|
||||
};
|
||||
|
||||
typedef std::function<bool(const DeckPreviewWidget *, const ExtraDeckSearchInfo &)> DeckFilter;
|
||||
typedef std::function<bool(const DeckSearchData &data)> DeckFilter;
|
||||
|
||||
class DeckFilterString
|
||||
{
|
||||
public:
|
||||
DeckFilterString();
|
||||
explicit DeckFilterString(const QString &expr);
|
||||
bool check(const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) const
|
||||
bool check(const DeckSearchData &data) const
|
||||
{
|
||||
return filter(deck, info);
|
||||
return filter(data);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool valid() const
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ AbstractClient *AbstractGame::getClientForPlayer(int playerId) const
|
|||
}
|
||||
}
|
||||
|
||||
void AbstractGame::loadReplay(GameReplay *replay)
|
||||
void AbstractGame::loadReplay(const GameReplay *replay)
|
||||
{
|
||||
gameMetaInfo->setFromProto(replay->game_info());
|
||||
gameMetaInfo->setSpectatorsOmniscient(true);
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ public:
|
|||
|
||||
AbstractClient *getClientForPlayer(int playerId) const;
|
||||
|
||||
void loadReplay(GameReplay *replay);
|
||||
void loadReplay(const GameReplay *replay);
|
||||
|
||||
CardItem *getCard(int playerId, const QString &zoneName, int cardId) const;
|
||||
|
||||
|
|
|
|||
|
|
@ -229,7 +229,11 @@ void GameEventHandler::handleArrowDeletion(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);
|
||||
}
|
||||
}
|
||||
|
|
@ -281,12 +285,19 @@ void GameEventHandler::eventGameStateChanged(const Event_GameStateChanged &event
|
|||
emit playerJoined(prop);
|
||||
}
|
||||
player->processPlayerInfo(playerInfo);
|
||||
// Extract playmat from player properties for opponent display
|
||||
if (prop.has_playmat_params()) {
|
||||
player->setPlaymatFromProperties(prop);
|
||||
}
|
||||
if (player->getPlayerInfo()->getLocal()) {
|
||||
emit localPlayerDeckSelected(player, playerId, playerInfo);
|
||||
} else {
|
||||
if (!game->getGameMetaInfo()->proto().share_decklists_on_load()) {
|
||||
continue;
|
||||
}
|
||||
if (!playerInfo.has_deck_list()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
opponentDecksToDisplay.append(
|
||||
qMakePair(playerId, qMakePair(playerName, QString::fromStdString(playerInfo.deck_list()))));
|
||||
|
|
@ -344,6 +355,11 @@ void GameEventHandler::eventPlayerPropertiesChanged(const Event_PlayerProperties
|
|||
const ServerInfo_PlayerProperties &prop = event.player_properties();
|
||||
emit playerPropertiesChanged(prop, eventPlayerId);
|
||||
|
||||
// Update playmat from player properties
|
||||
if (prop.has_playmat_params()) {
|
||||
player->setPlaymatFromProperties(prop);
|
||||
}
|
||||
|
||||
const auto contextType = static_cast<GameEventContext::ContextType>(getPbExtension(context));
|
||||
switch (contextType) {
|
||||
case GameEventContext::READY_START: {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,16 @@
|
|||
/**
|
||||
* @file game_event_handler.h
|
||||
* @ingroup GameLogic
|
||||
* @brief Game-level command sender and event dispatcher.
|
||||
*
|
||||
* GameEventHandler sends commands initiated by the local client to the server
|
||||
* and processes incoming game-wide events. It bridges the networking layer
|
||||
* (protobuf events received via AbstractClient) with the game model and UI
|
||||
* (GameState, PlayerManager, logging, widgets).
|
||||
*
|
||||
* Player-scoped events are forwarded to PlayerEventHandler instances, while
|
||||
* spectator and global game events are handled directly here.
|
||||
*/
|
||||
//! \todo Document this file.
|
||||
|
||||
#ifndef COCKATRICE_GAME_EVENT_HANDLER_H
|
||||
#define COCKATRICE_GAME_EVENT_HANDLER_H
|
||||
|
|
@ -15,92 +23,310 @@
|
|||
#include <libcockatrice/protocol/pb/serverinfo_player.pb.h>
|
||||
|
||||
class AbstractClient;
|
||||
class Response;
|
||||
class AbstractGame;
|
||||
class CommandContainer;
|
||||
class GameCommand;
|
||||
class GameEventContainer;
|
||||
class GameEventContext;
|
||||
class GameCommand;
|
||||
class GameState;
|
||||
class MessageLogWidget;
|
||||
class CommandContainer;
|
||||
class Event_GameJoined;
|
||||
class PendingCommand;
|
||||
class PlayerLogic;
|
||||
class Response;
|
||||
|
||||
class Event_GameStateChanged;
|
||||
class Event_PlayerPropertiesChanged;
|
||||
class Event_Join;
|
||||
class Event_Leave;
|
||||
class Event_GameHostChanged;
|
||||
class Event_GameClosed;
|
||||
class Event_GameStart;
|
||||
class Event_SetActivePlayer;
|
||||
class Event_SetActivePhase;
|
||||
class Event_Ping;
|
||||
class Event_GameSay;
|
||||
class Event_Kicked;
|
||||
class Event_ReverseTurn;
|
||||
class AbstractGame;
|
||||
class PendingCommand;
|
||||
class PlayerLogic;
|
||||
class Event_Ping;
|
||||
|
||||
inline Q_LOGGING_CATEGORY(GameEventHandlerLog, "game_event_handler");
|
||||
|
||||
/**
|
||||
* @class GameEventHandler
|
||||
* @brief Central dispatcher for game-wide commands and events.
|
||||
*
|
||||
* This class owns no game state itself. Instead, it:
|
||||
* - Sends commands to the server on behalf of local players
|
||||
* - Receives and dispatches server-side game events
|
||||
* - Updates the game model indirectly via Player, GameState, and PlayerManager
|
||||
* - Emits high-level signals for UI updates and logging
|
||||
*/
|
||||
class GameEventHandler : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
/** Pointer to the owning game instance. */
|
||||
AbstractGame *game;
|
||||
|
||||
public:
|
||||
/** @name Construction
|
||||
* Lifecycle and ownership.
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Construct a GameEventHandler.
|
||||
*
|
||||
* The handler is owned by the AbstractGame instance and uses it to
|
||||
* access the game state, players, and network clients.
|
||||
*
|
||||
* @param _game Owning game instance (also used as QObject parent).
|
||||
*/
|
||||
explicit GameEventHandler(AbstractGame *_game);
|
||||
|
||||
/** @} */
|
||||
|
||||
/** @name Outgoing game commands
|
||||
* Commands initiated locally and sent to the server.
|
||||
*
|
||||
* These methods construct and send protobuf commands corresponding
|
||||
* to user actions in the UI.
|
||||
* @{
|
||||
*/
|
||||
|
||||
/** @brief Request advancing the game to the next turn. */
|
||||
void handleNextTurn();
|
||||
|
||||
/** @brief Request reversing the current turn order. */
|
||||
void handleReverseTurn();
|
||||
|
||||
/** @brief Concede the game for the currently active local player. */
|
||||
void handleActiveLocalPlayerConceded();
|
||||
|
||||
/** @brief Undo a previous concede for the active local player. */
|
||||
void handleActiveLocalPlayerUnconceded();
|
||||
|
||||
/**
|
||||
* @brief Set the active phase of the game.
|
||||
*
|
||||
* Typically triggered by the active player selecting a new phase.
|
||||
*
|
||||
* @param phase Phase identifier.
|
||||
*/
|
||||
void handleActivePhaseChanged(int phase);
|
||||
|
||||
/** @brief Leave the current game session. */
|
||||
void handleGameLeft();
|
||||
|
||||
/**
|
||||
* @brief Send a chat message to all players and spectators.
|
||||
*
|
||||
* @param chatMessage Message text.
|
||||
*/
|
||||
void handleChatMessageSent(const QString &chatMessage);
|
||||
|
||||
/**
|
||||
* @brief Delete an existing arrow.
|
||||
*
|
||||
* @param arrowId Unique identifier of the arrow to delete.
|
||||
*/
|
||||
void handleArrowDeletion(int creatorId, int arrowId);
|
||||
void handleArrowDeletionFinished(const Response &response, int creatorId, int arrowId);
|
||||
|
||||
/** @} */
|
||||
|
||||
/** @name Incoming event processing
|
||||
* Entry points for server-sent events.
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Process a container of game events received from the server.
|
||||
*
|
||||
* This is the main dispatch function for incoming game events.
|
||||
* Events are routed to spectator handlers, game-level handlers,
|
||||
* or forwarded to PlayerEventHandler instances as appropriate.
|
||||
*
|
||||
* @param cont Game event container from the server.
|
||||
* @param client Client that received the container.
|
||||
* @param options Processing flags (e.g. silent, replay).
|
||||
*/
|
||||
void
|
||||
processGameEventContainer(const GameEventContainer &cont, AbstractClient *client, EventProcessingOptions options);
|
||||
|
||||
/** @} */
|
||||
|
||||
/** @name Command preparation helpers
|
||||
* Internal helpers for building command containers.
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Wrap a single protobuf command in a PendingCommand.
|
||||
*
|
||||
* @param cmd Protobuf command message.
|
||||
* @return Newly allocated PendingCommand (caller takes ownership).
|
||||
*/
|
||||
PendingCommand *prepareGameCommand(const ::google::protobuf::Message &cmd);
|
||||
|
||||
/**
|
||||
* @brief Wrap multiple protobuf commands in a single PendingCommand.
|
||||
*
|
||||
* Ownership of the messages in cmdList is transferred to the handler.
|
||||
*
|
||||
* @param cmdList List of protobuf command messages.
|
||||
* @return Newly allocated PendingCommand.
|
||||
*/
|
||||
PendingCommand *prepareGameCommand(const QList<const ::google::protobuf::Message *> &cmdList);
|
||||
|
||||
/** @} */
|
||||
|
||||
/** @name Spectator event handlers
|
||||
* Events originating from spectators.
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Handle a spectator chat message.
|
||||
*/
|
||||
void eventSpectatorSay(const Event_GameSay &event, int eventPlayerId, const GameEventContext &context);
|
||||
|
||||
/**
|
||||
* @brief Handle a spectator leaving the game.
|
||||
*/
|
||||
void eventSpectatorLeave(const Event_Leave &event, int eventPlayerId, const GameEventContext &context);
|
||||
|
||||
/** @} */
|
||||
|
||||
/** @name Game state event handlers
|
||||
* Events that affect global game state.
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Handle a full game state update from the server.
|
||||
*
|
||||
* Used during game startup, reconnection, and resynchronization.
|
||||
*/
|
||||
void eventGameStateChanged(const Event_GameStateChanged &event, int eventPlayerId, const GameEventContext &context);
|
||||
|
||||
/**
|
||||
* @brief Update card attachment relationships for all players.
|
||||
*
|
||||
* Called after a game state update to ensure attachments are resolved
|
||||
* consistently across all zones.
|
||||
*/
|
||||
void processCardAttachmentsForPlayers(const Event_GameStateChanged &event);
|
||||
|
||||
/** @brief Handle a change in game host. */
|
||||
void eventGameHostChanged(const Event_GameHostChanged &event, int eventPlayerId, const GameEventContext &context);
|
||||
|
||||
/** @brief Handle the game being closed by the server. */
|
||||
void eventGameClosed(const Event_GameClosed &event, int eventPlayerId, const GameEventContext &context);
|
||||
|
||||
/** @brief Handle a change of the active player. */
|
||||
void eventSetActivePlayer(const Event_SetActivePlayer &event, int eventPlayerId, const GameEventContext &context);
|
||||
|
||||
/** @brief Handle a change of the active phase. */
|
||||
void eventSetActivePhase(const Event_SetActivePhase &event, int eventPlayerId, const GameEventContext &context);
|
||||
|
||||
/** @brief Handle a turn reversal event. */
|
||||
void eventReverseTurn(const Event_ReverseTurn &event, int eventPlayerId, const GameEventContext &context);
|
||||
|
||||
/** @brief Handle ping / latency updates. */
|
||||
void eventPing(const Event_Ping &event, int eventPlayerId, const GameEventContext &context);
|
||||
|
||||
/** @} */
|
||||
|
||||
/** @name Player lifecycle and property handlers
|
||||
* Events related to players joining, leaving, or changing state.
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Handle updates to a player's properties.
|
||||
*
|
||||
* Includes readiness, concede state, deck selection, sideboard lock,
|
||||
* and connection state changes.
|
||||
*/
|
||||
void eventPlayerPropertiesChanged(const Event_PlayerPropertiesChanged &event,
|
||||
int eventPlayerId,
|
||||
const GameEventContext &context);
|
||||
|
||||
/** @brief Handle a player or spectator joining the game. */
|
||||
void eventJoin(const Event_Join &event, int eventPlayerId, const GameEventContext &context);
|
||||
|
||||
/** @brief Handle a player leaving the game. */
|
||||
void eventLeave(const Event_Leave &event, int eventPlayerId, const GameEventContext &context);
|
||||
QString getLeaveReason(Event_Leave::LeaveReason reason);
|
||||
|
||||
/** @brief Handle the local player being kicked from the game. */
|
||||
void eventKicked(const Event_Kicked &event, int eventPlayerId, const GameEventContext &context);
|
||||
void eventGameHostChanged(const Event_GameHostChanged &event, int eventPlayerId, const GameEventContext &context);
|
||||
void eventGameClosed(const Event_GameClosed &event, int eventPlayerId, const GameEventContext &context);
|
||||
|
||||
void eventSetActivePlayer(const Event_SetActivePlayer &event, int eventPlayerId, const GameEventContext &context);
|
||||
void eventSetActivePhase(const Event_SetActivePhase &event, int eventPlayerId, const GameEventContext &context);
|
||||
void eventPing(const Event_Ping &event, int eventPlayerId, const GameEventContext &context);
|
||||
void eventReverseTurn(const Event_ReverseTurn &event, int eventPlayerId, const GameEventContext & /*context*/);
|
||||
/**
|
||||
* @brief Convert a leave reason enum to a human-readable string.
|
||||
*/
|
||||
QString getLeaveReason(Event_Leave::LeaveReason reason);
|
||||
|
||||
void commandFinished(const Response &response);
|
||||
/** @} */
|
||||
|
||||
void
|
||||
processGameEventContainer(const GameEventContainer &cont, AbstractClient *client, EventProcessingOptions options);
|
||||
PendingCommand *prepareGameCommand(const ::google::protobuf::Message &cmd);
|
||||
PendingCommand *prepareGameCommand(const QList<const ::google::protobuf::Message *> &cmdList);
|
||||
public slots:
|
||||
/** @name Command dispatch slots
|
||||
* Low-level command transmission.
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Send a prepared PendingCommand.
|
||||
*
|
||||
* @param pend Pending command to send.
|
||||
* @param playerId Player whose client should send the command.
|
||||
*/
|
||||
void sendGameCommand(PendingCommand *pend, int playerId = -1);
|
||||
|
||||
/**
|
||||
* @brief Send a single protobuf command.
|
||||
*
|
||||
* @param command Protobuf command message.
|
||||
* @param playerId Player whose client should send the command.
|
||||
*/
|
||||
void sendGameCommand(const ::google::protobuf::Message &command, int playerId = -1);
|
||||
|
||||
/**
|
||||
* @brief Called when a PendingCommand finishes execution.
|
||||
*
|
||||
* Used to detect server-side errors such as chat flood protection.
|
||||
*/
|
||||
void commandFinished(const Response &response);
|
||||
|
||||
/** @} */
|
||||
|
||||
signals:
|
||||
/** @name Core state signals
|
||||
* @{
|
||||
*/
|
||||
|
||||
void emitUserEvent();
|
||||
void containerProcessingStarted(GameEventContext context);
|
||||
void containerProcessingDone();
|
||||
void gameFlooded();
|
||||
void setContextJudgeName(QString judgeName);
|
||||
|
||||
/** @} */
|
||||
|
||||
/** @name Player and spectator signals
|
||||
* @{
|
||||
*/
|
||||
|
||||
void addPlayerToAutoCompleteList(QString playerName);
|
||||
void localPlayerDeckSelected(PlayerLogic *localPlayer, int playerId, ServerInfo_Player playerInfo);
|
||||
void remotePlayerDeckSelected(QString deckList, int playerId, QString playerName);
|
||||
void remotePlayersDecksSelected(QVector<QPair<int, QPair<QString, QString>>> opponentDecks);
|
||||
void localPlayerSideboardLocked(int playerId, bool sideboardLocked);
|
||||
void localPlayerReadyStateChanged(int playerId, bool ready);
|
||||
|
||||
/** @} */
|
||||
|
||||
/** @name Game flow signals
|
||||
* @{
|
||||
*/
|
||||
|
||||
void gameStopped();
|
||||
void gameClosed();
|
||||
void playerPropertiesChanged(const ServerInfo_PlayerProperties &prop, int playerId);
|
||||
|
|
@ -109,11 +335,15 @@ signals:
|
|||
void playerKicked();
|
||||
void spectatorJoined(const ServerInfo_PlayerProperties &spectatorInfo);
|
||||
void spectatorLeft(int leavingSpectatorId);
|
||||
void gameFlooded();
|
||||
void containerProcessingStarted(GameEventContext context);
|
||||
void setContextJudgeName(QString judgeName);
|
||||
void containerProcessingDone();
|
||||
void arrowDeleted(int creatorId, int arrowId);
|
||||
|
||||
/** @} */
|
||||
|
||||
/** @name Logging signals
|
||||
* Signals consumed by MessageLogWidget.
|
||||
* @{
|
||||
*/
|
||||
|
||||
void logSpectatorSay(ServerInfo_User userInfo, QString message);
|
||||
void logSpectatorLeave(QString name, QString reason);
|
||||
void logGameStart();
|
||||
|
|
@ -132,6 +362,8 @@ signals:
|
|||
void logActivePhaseChanged(int activePhase);
|
||||
void logConcede(int playerId);
|
||||
void logUnconcede(int playerId);
|
||||
|
||||
/** @} */
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_GAME_EVENT_HANDLER_H
|
||||
|
|
|
|||
|
|
@ -27,8 +27,11 @@
|
|||
#include <libcockatrice/protocol/pb/command_shuffle.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_undo_draw.pb.h>
|
||||
#include <libcockatrice/protocol/pb/context_move_card.pb.h>
|
||||
#include <libcockatrice/settings/card_override_settings.h>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
#include <libcockatrice/utility/clamped_arithmetic.h>
|
||||
#include <libcockatrice/utility/counter_limits.h>
|
||||
#include <libcockatrice/utility/expression.h>
|
||||
#include <libcockatrice/utility/trice_limits.h>
|
||||
#include <libcockatrice/utility/zone_names.h>
|
||||
|
||||
// milliseconds in between triggers of the move top cards until action
|
||||
|
|
@ -66,7 +69,7 @@ void PlayerActions::playCard(CardItem *card, bool faceDown)
|
|||
const CardInfo &info = exactCard.getInfo();
|
||||
|
||||
int tableRow = info.getUiAttributes().tableRow;
|
||||
bool playToStack = SettingsCache::instance().getPlayToStack();
|
||||
bool playToStack = SettingsCache::instance().userInterface().getPlayToStack();
|
||||
QString currentZone = card->getZone()->getName();
|
||||
if (!faceDown && currentZone == ZoneNames::STACK && tableRow == 3) {
|
||||
cmd.set_target_zone(ZoneNames::GRAVE);
|
||||
|
|
@ -309,7 +312,7 @@ void PlayerActions::actDrawCard()
|
|||
|
||||
void PlayerActions::actRequestMulliganDialog()
|
||||
{
|
||||
int startSize = SettingsCache::instance().getStartingHandSize();
|
||||
int startSize = SettingsCache::instance().userInterface().getStartingHandSize();
|
||||
int handSize = player->getHandZone()->getCards().size();
|
||||
int deckSize = player->getDeckZone()->getCards().size() + handSize;
|
||||
|
||||
|
|
@ -325,7 +328,7 @@ void PlayerActions::actMulligan(int number)
|
|||
}
|
||||
|
||||
doMulligan(number);
|
||||
SettingsCache::instance().setStartingHandSize(number);
|
||||
SettingsCache::instance().userInterface().setStartingHandSize(number);
|
||||
}
|
||||
|
||||
void PlayerActions::actMulliganSameSize()
|
||||
|
|
@ -929,13 +932,13 @@ void PlayerActions::setLastTokenInfo(CardInfoPtr cardInfo)
|
|||
return;
|
||||
}
|
||||
|
||||
lastTokenInfo = {.name = cardInfo->getName(),
|
||||
.color = cardInfo->getColors().isEmpty() ? QString() : cardInfo->getColors().left(1).toLower(),
|
||||
.pt = cardInfo->getPowTough(),
|
||||
.annotation = SettingsCache::instance().getAnnotateTokens() ? cardInfo->getText() : "",
|
||||
.destroy = true,
|
||||
.providerId =
|
||||
SettingsCache::instance().cardOverrides().getCardPreferenceOverride(cardInfo->getName())};
|
||||
lastTokenInfo = {
|
||||
.name = cardInfo->getName(),
|
||||
.color = cardInfo->getColors().isEmpty() ? QString() : cardInfo->getColors().left(1).toLower(),
|
||||
.pt = cardInfo->getPowTough(),
|
||||
.annotation = SettingsCache::instance().userInterface().getAnnotateTokens() ? cardInfo->getText() : "",
|
||||
.destroy = true,
|
||||
.providerId = SettingsCache::instance().cardOverrides().getCardPreferenceOverride(cardInfo->getName())};
|
||||
|
||||
lastTokenTableRow = TableZone::tableRowToGridY(cardInfo->getUiAttributes().tableRow);
|
||||
|
||||
|
|
@ -1168,7 +1171,7 @@ void PlayerActions::createCard(const CardItem *sourceCard,
|
|||
}
|
||||
|
||||
cmd.set_pt(cardInfo->getPowTough().toStdString());
|
||||
if (SettingsCache::instance().getAnnotateTokens()) {
|
||||
if (SettingsCache::instance().userInterface().getAnnotateTokens()) {
|
||||
cmd.set_annotation(cardInfo->getText().toStdString());
|
||||
} else {
|
||||
cmd.set_annotation("");
|
||||
|
|
@ -1351,11 +1354,7 @@ void PlayerActions::actSetPT(QList<CardItem *> selectedCards, const QString &pt)
|
|||
const auto oldpt = CardItem::parsePT(card->getPT());
|
||||
int ptIter = 0;
|
||||
for (const auto &_item : ptList) {
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
|
||||
if (_item.typeId() == QMetaType::Type::Int) {
|
||||
#else
|
||||
if (_item.type() == QVariant::Int) {
|
||||
#endif
|
||||
int oldItem = ptIter < oldpt.size() ? oldpt.at(ptIter).toInt() : 0;
|
||||
newpt += '/' + QString::number(oldItem + _item.toInt());
|
||||
} else {
|
||||
|
|
@ -1530,12 +1529,15 @@ void PlayerActions::offsetCardCounter(QList<CardItem *> selectedCards, int count
|
|||
QList<const ::google::protobuf::Message *> commandList;
|
||||
for (auto card : selectedCards) {
|
||||
int oldValue = card->getCounters().value(counterId, 0);
|
||||
int newValue = oldValue + offset;
|
||||
|
||||
// Early exit optimization: server enforces [0, MAX_COUNTERS_ON_CARD].
|
||||
// Compare clamped value to allow recovery from invalid states.
|
||||
int clampedValue = qBound(0, newValue, MAX_COUNTERS_ON_CARD);
|
||||
if (clampedValue != oldValue) {
|
||||
// Overflow-safe clamp to the server-enforced range [0, MAX_COUNTER_VALUE];
|
||||
// a result differing from oldValue also corrects an out-of-range cached value.
|
||||
// Callers only ever pass offset == ±1 (actAddCardCounter / actRemoveCardCounter).
|
||||
// This client-side clamp is a defense-in-depth UX check, consistent with
|
||||
// actSetCardCounter and actIncrementAllCardCounters; the server remains the
|
||||
// authoritative enforcer of the bounds.
|
||||
int newValue = addClamped(oldValue, offset, 0, MAX_COUNTER_VALUE);
|
||||
if (newValue != oldValue) {
|
||||
auto *cmd = new Command_SetCardCounter;
|
||||
cmd->set_zone(card->getZone()->getName().toStdString());
|
||||
cmd->set_card_id(card->getId());
|
||||
|
|
@ -1568,7 +1570,7 @@ void PlayerActions::actSetCardCounter(QList<CardItem *> selectedCards, int count
|
|||
Expression exp(oldValue);
|
||||
double parsed = exp.parse(counterValue);
|
||||
// Clamp in double precision first to avoid UB, then cast
|
||||
int number = static_cast<int>(qBound(0.0, parsed, static_cast<double>(MAX_COUNTERS_ON_CARD)));
|
||||
int number = static_cast<int>(qBound(0.0, parsed, static_cast<double>(MAX_COUNTER_VALUE)));
|
||||
|
||||
auto *cmd = new Command_SetCardCounter;
|
||||
cmd->set_zone(card->getZone()->getName().toStdString());
|
||||
|
|
@ -1598,7 +1600,7 @@ void PlayerActions::actIncrementAllCardCounters(QList<CardItem *> cardsToUpdate)
|
|||
counterIterator.next();
|
||||
int counterId = counterIterator.key();
|
||||
int currentValue = counterIterator.value();
|
||||
if (currentValue >= MAX_COUNTERS_ON_CARD) {
|
||||
if (currentValue >= MAX_COUNTER_VALUE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,24 @@
|
|||
/**
|
||||
* @file player_event_handler.h
|
||||
* @ingroup GameLogicPlayers
|
||||
* @brief Player-scoped game event handler.
|
||||
*
|
||||
* PlayerEventHandler applies game events that affect a single Player’s
|
||||
* board state, zones, cards, counters, arrows, and related UI/log output.
|
||||
*
|
||||
* It is invoked by GameEventHandler after basic routing and validation.
|
||||
* Each instance is bound 1:1 to a Player and must never mutate state
|
||||
* belonging to other players except where explicitly required by events
|
||||
* (e.g. moving cards between players, attaching cards, arrows).
|
||||
*
|
||||
* This class is intentionally stateful and tightly coupled to Player,
|
||||
* PlayerActions, and the board/zones implementation. It performs both
|
||||
* model mutation and UI-side bookkeeping (zone views, arrows, menus).
|
||||
*/
|
||||
//! \todo Document this file.
|
||||
|
||||
#ifndef COCKATRICE_PLAYER_EVENT_HANDLER_H
|
||||
#define COCKATRICE_PLAYER_EVENT_HANDLER_H
|
||||
|
||||
#include "event_processing_options.h"
|
||||
|
||||
#include <QObject>
|
||||
|
|
@ -16,6 +29,7 @@
|
|||
class CardItem;
|
||||
class CardZoneLogic;
|
||||
class PlayerLogic;
|
||||
|
||||
class Event_AttachCard;
|
||||
class Event_ChangeZoneProperties;
|
||||
class Event_CreateArrow;
|
||||
|
|
@ -37,11 +51,176 @@ class Event_SetCounter;
|
|||
class Event_Shuffle;
|
||||
class Event_GameLogNotice;
|
||||
|
||||
/**
|
||||
* @class PlayerEventHandler
|
||||
* @brief Applies player-specific game events and emits corresponding log signals.
|
||||
*
|
||||
* Design notes:
|
||||
* - All event handlers assume events are authoritative and already validated
|
||||
* by the server.
|
||||
* - Most handlers mutate both logical state (CardItem, CardZoneLogic, counters)
|
||||
* and visual/UI state (views, arrows, menus).
|
||||
* - Logging signals are emitted *after* or *during* state mutation, depending
|
||||
* on whether later mutations would invalidate log data.
|
||||
*/
|
||||
class PlayerEventHandler : public QObject
|
||||
{
|
||||
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a PlayerEventHandler bound to a Player.
|
||||
* @param player Owning player instance.
|
||||
*/
|
||||
explicit PlayerEventHandler(PlayerLogic *player);
|
||||
|
||||
/** @name Event dispatch
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Dispatch a generic GameEvent to the appropriate handler.
|
||||
*
|
||||
* This is the single entry point used by GameEventHandler. It extracts
|
||||
* the correct protobuf extension and forwards the event to a typed
|
||||
* handler method.
|
||||
*
|
||||
* @param type Game event type enum.
|
||||
* @param event Generic protobuf container.
|
||||
* @param context Additional context (undo, judge, etc.).
|
||||
* @param options Processing options (UI suppression, reveal behavior).
|
||||
*/
|
||||
void processGameEvent(GameEvent::GameEventType type,
|
||||
const GameEvent &event,
|
||||
const GameEventContext &context,
|
||||
EventProcessingOptions options);
|
||||
|
||||
/** @} */
|
||||
|
||||
/** @name Chat and randomization events
|
||||
* @{
|
||||
*/
|
||||
|
||||
/// Handle in-game chat messages from this player.
|
||||
void eventGameSay(const Event_GameSay &event);
|
||||
|
||||
/// Handle zone shuffle events (typically libraries).
|
||||
void eventShuffle(const Event_Shuffle &event);
|
||||
|
||||
/// Handle die roll events.
|
||||
void eventRollDie(const Event_RollDie &event);
|
||||
|
||||
/** @} */
|
||||
|
||||
/** @name Arrow and targeting events
|
||||
* @{
|
||||
*/
|
||||
|
||||
/// Create a visual arrow between cards or players.
|
||||
void eventCreateArrow(const Event_CreateArrow &event);
|
||||
|
||||
/// Delete an existing arrow.
|
||||
void eventDeleteArrow(const Event_DeleteArrow &event);
|
||||
|
||||
/** @} */
|
||||
|
||||
/** @name Token and card creation
|
||||
* @{
|
||||
*/
|
||||
|
||||
/// Create a token card in a target zone.
|
||||
void eventCreateToken(const Event_CreateToken &event);
|
||||
|
||||
/** @} */
|
||||
|
||||
/** @name Card attribute and counter updates
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Set a card attribute (tapped, PT, annotation, etc.).
|
||||
*
|
||||
* May apply to a single card or all cards in a zone if no card ID
|
||||
* is provided by the event.
|
||||
*/
|
||||
void
|
||||
eventSetCardAttr(const Event_SetCardAttr &event, const GameEventContext &context, EventProcessingOptions options);
|
||||
|
||||
/// Update a counter attached to a card.
|
||||
void eventSetCardCounter(const Event_SetCardCounter &event);
|
||||
|
||||
/// Create a player-level counter.
|
||||
void eventCreateCounter(const Event_CreateCounter &event);
|
||||
|
||||
/// Set a player-level counter value.
|
||||
void eventSetCounter(const Event_SetCounter &event);
|
||||
|
||||
/// Delete a player-level counter.
|
||||
void eventDelCounter(const Event_DelCounter &event);
|
||||
|
||||
/** @} */
|
||||
|
||||
/** @name Zone-level operations
|
||||
* @{
|
||||
*/
|
||||
|
||||
/// Log a zone dump (e.g. reveal graveyard/library contents).
|
||||
void eventDumpZone(const Event_DumpZone &event);
|
||||
|
||||
/**
|
||||
* @brief Move a card between zones and/or players.
|
||||
*
|
||||
* This is one of the most complex handlers:
|
||||
* - Removes the card from the start zone
|
||||
* - Updates card identity and ownership if needed
|
||||
* - Handles attachments and arrows
|
||||
* - Emits appropriate move or undo-draw logs
|
||||
* - Inserts the card into the target zone
|
||||
*/
|
||||
void eventMoveCard(const Event_MoveCard &event, const GameEventContext &context);
|
||||
|
||||
/// Flip a card face up or face down.
|
||||
void eventFlipCard(const Event_FlipCard &event);
|
||||
|
||||
/// Destroy a card and clean up attachments.
|
||||
void eventDestroyCard(const Event_DestroyCard &event);
|
||||
|
||||
/// Attach or detach a card to/from another card.
|
||||
void eventAttachCard(const Event_AttachCard &event);
|
||||
|
||||
/** @} */
|
||||
|
||||
/** @name Draw and reveal operations
|
||||
* @{
|
||||
*/
|
||||
|
||||
/// Draw one or more cards from the deck.
|
||||
void eventDrawCards(const Event_DrawCards &event);
|
||||
|
||||
/**
|
||||
* @brief Reveal cards from a zone.
|
||||
*
|
||||
* Handles peeking, in-place top-card reveals, full reveal windows,
|
||||
* and write-access granting.
|
||||
*/
|
||||
void eventRevealCards(const Event_RevealCards &event, EventProcessingOptions options);
|
||||
|
||||
/** @} */
|
||||
|
||||
/** @name Zone configuration
|
||||
* @{
|
||||
*/
|
||||
|
||||
/// Update zone visibility and reveal behavior.
|
||||
void eventChangeZoneProperties(const Event_ChangeZoneProperties &event);
|
||||
|
||||
/** @} */
|
||||
|
||||
void eventGameLogNotice(const Event_GameLogNotice &event);
|
||||
signals:
|
||||
/** @name Logging signals
|
||||
* @{
|
||||
*/
|
||||
void logSay(PlayerLogic *player, QString message);
|
||||
void logShuffle(PlayerLogic *player, CardZoneLogic *zone, int start, int end);
|
||||
void logRollDie(PlayerLogic *player, int sides, const QList<uint> &rolls);
|
||||
|
|
@ -82,40 +261,13 @@ signals:
|
|||
bool isLentToAnotherPlayer = false);
|
||||
void logAlwaysRevealTopCard(PlayerLogic *player, CardZoneLogic *zone, bool reveal);
|
||||
void logAlwaysLookAtTopCard(PlayerLogic *player, CardZoneLogic *zone, bool reveal);
|
||||
/** @} */
|
||||
|
||||
void cardZoneChanged(CardItem *card, bool sameZone);
|
||||
void requestCardMenuUpdate(const CardItem *card);
|
||||
|
||||
public:
|
||||
PlayerEventHandler(PlayerLogic *player);
|
||||
|
||||
void processGameEvent(GameEvent::GameEventType type,
|
||||
const GameEvent &event,
|
||||
const GameEventContext &context,
|
||||
EventProcessingOptions options);
|
||||
|
||||
void eventGameSay(const Event_GameSay &event);
|
||||
void eventShuffle(const Event_Shuffle &event);
|
||||
void eventRollDie(const Event_RollDie &event);
|
||||
void eventCreateArrow(const Event_CreateArrow &event);
|
||||
void eventDeleteArrow(const Event_DeleteArrow &event);
|
||||
void eventCreateToken(const Event_CreateToken &event);
|
||||
void
|
||||
eventSetCardAttr(const Event_SetCardAttr &event, const GameEventContext &context, EventProcessingOptions options);
|
||||
void eventSetCardCounter(const Event_SetCardCounter &event);
|
||||
void eventCreateCounter(const Event_CreateCounter &event);
|
||||
void eventSetCounter(const Event_SetCounter &event);
|
||||
void eventDelCounter(const Event_DelCounter &event);
|
||||
void eventDumpZone(const Event_DumpZone &event);
|
||||
void eventMoveCard(const Event_MoveCard &event, const GameEventContext &context);
|
||||
void eventFlipCard(const Event_FlipCard &event);
|
||||
void eventDestroyCard(const Event_DestroyCard &event);
|
||||
void eventAttachCard(const Event_AttachCard &event);
|
||||
void eventDrawCards(const Event_DrawCards &event);
|
||||
void eventRevealCards(const Event_RevealCards &event, EventProcessingOptions options);
|
||||
void eventChangeZoneProperties(const Event_ChangeZoneProperties &event);
|
||||
void eventGameLogNotice(const Event_GameLogNotice &event);
|
||||
|
||||
private:
|
||||
/** Owning player instance. */
|
||||
PlayerLogic *player;
|
||||
|
||||
void setCardAttrHelper(const GameEventContext &context,
|
||||
|
|
|
|||
|
|
@ -250,6 +250,22 @@ void PlayerLogic::setDeck(const DeckList &_deck)
|
|||
emit deckChanged();
|
||||
}
|
||||
|
||||
void PlayerLogic::setPlaymatFromProperties(const ServerInfo_PlayerProperties &props)
|
||||
{
|
||||
if (props.has_playmat_params() && !props.playmat_params().card_name().empty()) {
|
||||
const auto &pp = props.playmat_params();
|
||||
remotePlaymatCard = {QString::fromStdString(pp.card_name()), QString::fromStdString(pp.card_provider_id())};
|
||||
remotePlaymatParams = {qBound(0.0, pp.margin_pct_l(), 0.95), qBound(0.0, pp.margin_pct_r(), 0.95),
|
||||
qBound(0.0, pp.vertical_offset(), 1.0), qBound(0.1, pp.zoom(), 4.0)};
|
||||
hasRemotePlaymat = true;
|
||||
} else {
|
||||
remotePlaymatCard = CardRef{};
|
||||
remotePlaymatParams = PlaymatParams{};
|
||||
hasRemotePlaymat = false;
|
||||
}
|
||||
emit playmatChanged();
|
||||
}
|
||||
|
||||
CounterState *PlayerLogic::addCounter(const ServerInfo_Counter &counter)
|
||||
{
|
||||
return addCounter(counter.id(), QString::fromStdString(counter.name()),
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
#include "../zones/table_zone_logic.h"
|
||||
#include "player_event_handler.h"
|
||||
#include "player_info.h"
|
||||
#include "player_manager.h"
|
||||
|
||||
#include <QInputDialog>
|
||||
#include <QLoggingCategory>
|
||||
|
|
@ -72,6 +73,8 @@ signals:
|
|||
const QList<const ServerInfo_Card *> &cardList,
|
||||
bool withWritePermission);
|
||||
void deckChanged();
|
||||
/** @brief Emitted when the remote playmat (card/params) is updated from player properties. */
|
||||
void playmatChanged();
|
||||
void newCardAdded(AbstractCardItem *card);
|
||||
void requestCardMenuUpdate(const CardItem *card);
|
||||
void counterAdded(CounterState *state);
|
||||
|
|
@ -226,6 +229,20 @@ public:
|
|||
|
||||
void setZoneId(int _zoneId);
|
||||
|
||||
void setPlaymatFromProperties(const ServerInfo_PlayerProperties &props);
|
||||
const CardRef &getRemotePlaymatCard() const
|
||||
{
|
||||
return remotePlaymatCard;
|
||||
}
|
||||
const PlaymatParams &getRemotePlaymatParams() const
|
||||
{
|
||||
return remotePlaymatParams;
|
||||
}
|
||||
bool getHasRemotePlaymat() const
|
||||
{
|
||||
return hasRemotePlaymat;
|
||||
}
|
||||
|
||||
private:
|
||||
AbstractGame *game;
|
||||
PlayerInfo *playerInfo;
|
||||
|
|
@ -243,6 +260,11 @@ private:
|
|||
|
||||
bool dialogSemaphore;
|
||||
QList<CardItem *> cardsToDelete;
|
||||
|
||||
// Playmat from player properties (for opponent display)
|
||||
CardRef remotePlaymatCard;
|
||||
PlaymatParams remotePlaymatParams;
|
||||
bool hasRemotePlaymat = false;
|
||||
};
|
||||
|
||||
class AnnotationDialog : public QInputDialog
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
#include "../interface/widgets/tabs/tab_game.h"
|
||||
|
||||
Replay::Replay(QObject *_parent, GameReplay *_replay, bool isLocalGame) : AbstractGame(_parent)
|
||||
Replay::Replay(QObject *_parent, const GameReplay *_replay, bool isLocalGame) : AbstractGame(_parent)
|
||||
{
|
||||
gameState = new GameState(this, 0, -1, isLocalGame, {}, false, false, -1, false);
|
||||
connect(gameMetaInfo, &GameMetaInfo::startedChanged, gameState, &GameState::onStartedChanged);
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class Replay : public AbstractGame
|
|||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit Replay(QObject *_parent, GameReplay *_replay, bool isLocalGame);
|
||||
explicit Replay(QObject *_parent, const GameReplay *_replay, bool isLocalGame);
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_REPLAY_H
|
||||
|
|
|
|||
|
|
@ -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 "../../game_graphics/board/card_item.h"
|
||||
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
/**
|
||||
* @param _player the player that the cards are revealed to.
|
||||
* @param _origZone the zone the cards were revealed from.
|
||||
|
|
@ -57,7 +58,7 @@ bool ZoneViewZoneLogic::prepareAddCard(int x)
|
|||
|
||||
// autoclose check is done both here and in removeCard
|
||||
|
||||
if (cards.isEmpty() && !doInsert && SettingsCache::instance().getCloseEmptyCardView()) {
|
||||
if (cards.isEmpty() && !doInsert && SettingsCache::instance().userInterface().getCloseEmptyCardView()) {
|
||||
emit closeView();
|
||||
}
|
||||
|
||||
|
|
@ -144,7 +145,7 @@ void ZoneViewZoneLogic::removeCard(int position, bool toNewZone)
|
|||
// card gets dragged within the view.
|
||||
// Another autoclose check is done in prepareAddCard so that the view autocloses if the last card was moved to an
|
||||
// unrevealed portion of the same zone.
|
||||
if (cards.isEmpty() && SettingsCache::instance().getCloseEmptyCardView() && toNewZone) {
|
||||
if (cards.isEmpty() && SettingsCache::instance().userInterface().getCloseEmptyCardView() && toNewZone) {
|
||||
emit closeView();
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
26
cockatrice/src/game_graphics/animated_item.h
Normal file
26
cockatrice/src/game_graphics/animated_item.h
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
#ifndef ANIMATED_ITEM_H
|
||||
#define ANIMATED_ITEM_H
|
||||
|
||||
/**
|
||||
* @file animated_item.h
|
||||
* @ingroup GameGraphics
|
||||
* @brief Interface for scene items driven by GameScene's shared animation timer.
|
||||
*
|
||||
* Items that want per-tick animation while a single QBasicTimer runs (instead of
|
||||
* owning their own QTimer) implement this interface and register with the scene
|
||||
* via GameScene::registerAnimationItem.
|
||||
*/
|
||||
|
||||
class IAnimatedItem
|
||||
{
|
||||
public:
|
||||
virtual ~IAnimatedItem() = default;
|
||||
|
||||
/**
|
||||
* @brief Advances the item's animation by one timer tick.
|
||||
* @return true while the animation is still running, false once it has finished.
|
||||
*/
|
||||
virtual bool animationEvent() = 0;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
#include <QDebug>
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
|
||||
const QColor GHOST_MASK = QColor(255, 255, 255, 50);
|
||||
|
||||
|
|
@ -34,12 +35,13 @@ AbstractCardDragItem::AbstractCardDragItem(AbstractCardItem *_item,
|
|||
|
||||
setCacheMode(DeviceCoordinateCache);
|
||||
|
||||
connect(&SettingsCache::instance(), &SettingsCache::roundCardCornersChanged, this, [this](bool _roundCardCorners) {
|
||||
Q_UNUSED(_roundCardCorners);
|
||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::roundCardCornersChanged, this,
|
||||
[this](bool _roundCardCorners) {
|
||||
Q_UNUSED(_roundCardCorners);
|
||||
|
||||
prepareGeometryChange();
|
||||
update();
|
||||
});
|
||||
prepareGeometryChange();
|
||||
update();
|
||||
});
|
||||
|
||||
connect(item, &QObject::destroyed, this, &AbstractCardDragItem::deleteLater);
|
||||
}
|
||||
|
|
@ -47,7 +49,8 @@ AbstractCardDragItem::AbstractCardDragItem(AbstractCardItem *_item,
|
|||
QPainterPath AbstractCardDragItem::shape() const
|
||||
{
|
||||
QPainterPath shape;
|
||||
qreal cardCornerRadius = SettingsCache::instance().getRoundCardCorners() ? 0.05 * CardDimensions::WIDTH_F : 0.0;
|
||||
qreal cardCornerRadius =
|
||||
SettingsCache::instance().cardsDisplay().getRoundCardCorners() ? 0.05 * CardDimensions::WIDTH_F : 0.0;
|
||||
shape.addRoundedRect(boundingRect(), cardCornerRadius, cardCornerRadius);
|
||||
return shape;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@
|
|||
#include <algorithm>
|
||||
#include <libcockatrice/card/database/card_database.h>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/settings/appearance_settings.h>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
#include <libcockatrice/settings/debug_settings.h>
|
||||
|
||||
AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef, PlayerLogic *_owner, int _id)
|
||||
: ArrowTarget(_owner, parent), id(_id), cardRef(cardRef), tapped(false), facedown(false), tapAngle(0),
|
||||
|
|
@ -21,15 +24,17 @@ AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef
|
|||
setFlag(ItemIsSelectable);
|
||||
setCacheMode(DeviceCoordinateCache);
|
||||
|
||||
connect(&SettingsCache::instance(), &SettingsCache::displayCardNamesChanged, this, [this] { update(); });
|
||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::displayCardNamesChanged, this,
|
||||
[this] { update(); });
|
||||
refreshCardInfo();
|
||||
|
||||
connect(&SettingsCache::instance(), &SettingsCache::roundCardCornersChanged, this, [this](bool _roundCardCorners) {
|
||||
Q_UNUSED(_roundCardCorners);
|
||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::roundCardCornersChanged, this,
|
||||
[this](bool _roundCardCorners) {
|
||||
Q_UNUSED(_roundCardCorners);
|
||||
|
||||
prepareGeometryChange();
|
||||
update();
|
||||
});
|
||||
prepareGeometryChange();
|
||||
update();
|
||||
});
|
||||
}
|
||||
|
||||
AbstractCardItem::~AbstractCardItem()
|
||||
|
|
@ -45,7 +50,8 @@ QRectF AbstractCardItem::boundingRect() const
|
|||
QPainterPath AbstractCardItem::shape() const
|
||||
{
|
||||
QPainterPath shape;
|
||||
qreal cardCornerRadius = SettingsCache::instance().getRoundCardCorners() ? 0.05 * CardDimensions::WIDTH_F : 0.0;
|
||||
qreal cardCornerRadius =
|
||||
SettingsCache::instance().cardsDisplay().getRoundCardCorners() ? 0.05 * CardDimensions::WIDTH_F : 0.0;
|
||||
shape.addRoundedRect(boundingRect(), cardCornerRadius, cardCornerRadius);
|
||||
return shape;
|
||||
}
|
||||
|
|
@ -101,7 +107,7 @@ QSizeF AbstractCardItem::getTranslatedSize(QPainter *painter) const
|
|||
|
||||
void AbstractCardItem::transformPainter(QPainter *painter, const QSizeF &translatedSize, int angle)
|
||||
{
|
||||
const int MAX_FONT_SIZE = SettingsCache::instance().getMaxFontSize();
|
||||
const int MAX_FONT_SIZE = SettingsCache::instance().appearance().getMaxFontSize();
|
||||
const int fontSize = std::max(9, MAX_FONT_SIZE);
|
||||
|
||||
QRectF totalBoundingRect = painter->combinedTransform().mapRect(boundingRect());
|
||||
|
|
@ -151,7 +157,7 @@ void AbstractCardItem::paintPicture(QPainter *painter, const QSizeF &translatedS
|
|||
painter->drawPath(shape());
|
||||
}
|
||||
|
||||
if (translatedPixmap.isNull() || SettingsCache::instance().getDisplayCardNames() || facedown) {
|
||||
if (translatedPixmap.isNull() || SettingsCache::instance().cardsDisplay().getDisplayCardNames() || facedown) {
|
||||
painter->save();
|
||||
transformPainter(painter, translatedSize, angle);
|
||||
painter->setPen(Qt::white);
|
||||
|
|
@ -234,7 +240,7 @@ void AbstractCardItem::setHovered(bool _hovered)
|
|||
|
||||
isHovered = _hovered;
|
||||
setZValue(_hovered ? ZValues::HOVERED_CARD : realZValue);
|
||||
setScale(_hovered && SettingsCache::instance().getScaleCards() ? 1.1 : 1);
|
||||
setScale(_hovered && SettingsCache::instance().cardsDisplay().getScaleCards() ? 1.1 : 1);
|
||||
setTransformOriginPoint(_hovered ? CardDimensions::WIDTH_HALF_F : 0, _hovered ? CardDimensions::HEIGHT_HALF_F : 0);
|
||||
update();
|
||||
}
|
||||
|
|
@ -287,7 +293,7 @@ void AbstractCardItem::setTapped(bool _tapped, bool canAnimate)
|
|||
}
|
||||
|
||||
tapped = _tapped;
|
||||
if (SettingsCache::instance().getTapAnimation() && canAnimate) {
|
||||
if (SettingsCache::instance().cardsDisplay().getTapAnimation() && canAnimate) {
|
||||
static_cast<GameScene *>(scene())->registerAnimationItem(this);
|
||||
} else {
|
||||
tapAngle = tapped ? 90 : 0;
|
||||
|
|
@ -299,6 +305,11 @@ void AbstractCardItem::setTapped(bool _tapped, bool canAnimate)
|
|||
}
|
||||
}
|
||||
|
||||
bool AbstractCardItem::animationEvent()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
void AbstractCardItem::setFaceDown(bool _facedown)
|
||||
{
|
||||
facedown = _facedown;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#ifndef ABSTRACTCARDITEM_H
|
||||
#define ABSTRACTCARDITEM_H
|
||||
|
||||
#include "../animated_item.h"
|
||||
#include "../card_dimensions.h"
|
||||
#include "arrow_target.h"
|
||||
#include "graphics_item_type.h"
|
||||
|
|
@ -16,7 +17,7 @@
|
|||
|
||||
class PlayerLogic;
|
||||
|
||||
class AbstractCardItem : public ArrowTarget
|
||||
class AbstractCardItem : public ArrowTarget, public IAnimatedItem
|
||||
{
|
||||
Q_OBJECT
|
||||
protected:
|
||||
|
|
@ -126,6 +127,9 @@ public:
|
|||
emit deleteCardInfoPopup(cardRef.name);
|
||||
}
|
||||
|
||||
/** @brief Default: no per-tick animation. Subclasses override to animate. */
|
||||
bool animationEvent() override;
|
||||
|
||||
protected:
|
||||
void transformPainter(QPainter *painter, const QSizeF &translatedSize, int angle);
|
||||
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "abstract_counter.h"
|
||||
|
||||
#include "../../client/settings/cache_settings.h"
|
||||
#include "../../client/settings/shortcuts_settings.h"
|
||||
#include "../../game/player/player_actions.h"
|
||||
#include "../../game/player/player_logic.h"
|
||||
#include "../../game_graphics/board/translate_counter_name.h"
|
||||
|
|
@ -28,8 +29,9 @@ AbstractCounter::AbstractCounter(CounterState *state,
|
|||
{
|
||||
setAcceptHoverEvents(true);
|
||||
|
||||
connect(state, &CounterState::valueChanged, this, [this](int, int newValue) {
|
||||
connect(state, &CounterState::valueChanged, this, [this](int oldValue, int newValue) {
|
||||
value = newValue;
|
||||
onValueChanged(oldValue, newValue);
|
||||
update();
|
||||
});
|
||||
|
||||
|
|
@ -227,3 +229,9 @@ void AbstractCounterDialog::changeValue(int diff)
|
|||
curValue += diff;
|
||||
setTextValue(QString::number(curValue));
|
||||
}
|
||||
|
||||
void AbstractCounter::onValueChanged(int /*oldValue*/, int /*newValue*/)
|
||||
{
|
||||
// Default: no feedback. Subclasses such as PlayerCounter override this to
|
||||
// flash the counter on meaningful changes (life gain/loss).
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,13 @@ protected:
|
|||
bool hovered = false;
|
||||
bool useNameForShortcut;
|
||||
|
||||
/**
|
||||
* @brief Hook for subclasses that need per-value-change feedback (e.g. life-total flash).
|
||||
*
|
||||
* Called whenever the counter's value changes, before the item repaints.
|
||||
*/
|
||||
virtual void onValueChanged(int oldValue, int newValue);
|
||||
|
||||
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
|
||||
void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override;
|
||||
void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override;
|
||||
|
|
|
|||
|
|
@ -4,12 +4,14 @@
|
|||
#include "../../client/settings/cache_settings.h"
|
||||
#include "../../game/player/player_actions.h"
|
||||
#include "../../game/player/player_logic.h"
|
||||
#include "../game_scene.h"
|
||||
#include "../player/player_target.h"
|
||||
#include "../z_values.h"
|
||||
#include "../zones/card_zone.h"
|
||||
#include "card_item.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QElapsedTimer>
|
||||
#include <QGraphicsScene>
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
#include <QPainter>
|
||||
|
|
@ -18,9 +20,27 @@
|
|||
#include <libcockatrice/protocol/pb/command_attach_card.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_create_arrow.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_delete_arrow.pb.h>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
#include <libcockatrice/utility/color.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)
|
||||
: 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()
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -90,16 +125,21 @@ void ArrowItem::updatePath(const QPointF &endPoint)
|
|||
prepareGeometryChange();
|
||||
if (lineLength < 30) {
|
||||
path = QPainterPath();
|
||||
bodyPath = QPainterPath();
|
||||
headPath = QPainterPath();
|
||||
shaftOutlinePath = QPainterPath();
|
||||
centerLine = QPainterPath();
|
||||
headBaseFraction = 1.0;
|
||||
} else {
|
||||
QPointF c(lineLength / 2, qTan(phi * M_PI / 180) * lineLength);
|
||||
|
||||
QPainterPath centerLine;
|
||||
centerLine = QPainterPath();
|
||||
centerLine.moveTo(0, 0);
|
||||
centerLine.quadTo(c, QPointF(lineLength, 0));
|
||||
|
||||
double percentage = 1 - headLength / lineLength;
|
||||
QPointF arrowBodyEndPoint = centerLine.pointAtPercent(percentage);
|
||||
QLineF testLine(arrowBodyEndPoint, centerLine.pointAtPercent(percentage + 0.001));
|
||||
headBaseFraction = 1 - headLength / lineLength;
|
||||
QPointF arrowBodyEndPoint = centerLine.pointAtPercent(headBaseFraction);
|
||||
QLineF testLine(arrowBodyEndPoint, centerLine.pointAtPercent(headBaseFraction + 0.001));
|
||||
qreal alpha = testLine.angle() - 90;
|
||||
QPointF endPoint1 =
|
||||
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 =
|
||||
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.lineTo(point1);
|
||||
path.lineTo(QPointF(lineLength, 0));
|
||||
path.lineTo(point2);
|
||||
path.lineTo(endPoint2);
|
||||
path.quadTo(c, arrowWidth / 2 * QPointF(qCos((phi - 90) * M_PI / 180), qSin((phi - 90) * M_PI / 180)));
|
||||
path.lineTo(-arrowWidth / 2 * QPointF(qCos((phi - 90) * M_PI / 180), qSin((phi - 90) * M_PI / 180)));
|
||||
path.quadTo(c, start2);
|
||||
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);
|
||||
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*/)
|
||||
{
|
||||
QColor paintColor(data->color);
|
||||
|
|
@ -132,8 +241,66 @@ void ArrowItem::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*opti
|
|||
} else {
|
||||
paintColor.setAlpha(150);
|
||||
}
|
||||
|
||||
painter->save();
|
||||
const QPen outlinePen = painter->pen();
|
||||
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)
|
||||
|
|
@ -225,6 +392,12 @@ void ArrowDragItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
|
|||
void ArrowDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -261,7 +434,7 @@ void ArrowDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
|||
if (startZone->getName() == ZoneNames::HAND) {
|
||||
startCard->playCard(false);
|
||||
CardInfoPtr ci = startCard->getCard().getCardPtr();
|
||||
bool playToStack = SettingsCache::instance().getPlayToStack();
|
||||
bool playToStack = SettingsCache::instance().userInterface().getPlayToStack();
|
||||
if (ci && ((!playToStack && ci->getUiAttributes().tableRow == 3) ||
|
||||
(playToStack && ci->getUiAttributes().tableRow != 0 &&
|
||||
startCard->getZone()->getName() != ZoneNames::STACK))) {
|
||||
|
|
@ -348,6 +521,12 @@ void ArrowAttachItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
|
|||
void ArrowAttachItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,13 @@
|
|||
#define ARROWITEM_H
|
||||
|
||||
#include "../../game/board/arrow_data.h"
|
||||
#include "../animated_item.h"
|
||||
#include "arrow_target.h"
|
||||
#include "graphics_item_type.h"
|
||||
|
||||
#include <QElapsedTimer>
|
||||
#include <QGraphicsItem>
|
||||
#include <QPainterPath>
|
||||
#include <QPointer>
|
||||
#include <QSharedPointer>
|
||||
|
||||
|
|
@ -12,7 +16,7 @@ class CardItem;
|
|||
class QGraphicsSceneMouseEvent;
|
||||
class PlayerLogic;
|
||||
|
||||
class ArrowItem : public QObject, public QGraphicsItem
|
||||
class ArrowItem : public QObject, public QGraphicsItem, public IAnimatedItem
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_INTERFACES(QGraphicsItem)
|
||||
|
|
@ -21,6 +25,19 @@ signals:
|
|||
|
||||
private:
|
||||
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:
|
||||
QSharedPointer<const ArrowData> data;
|
||||
|
|
@ -32,17 +49,28 @@ protected:
|
|||
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
|
||||
|
||||
public:
|
||||
enum
|
||||
{
|
||||
Type = typeArrow
|
||||
};
|
||||
[[nodiscard]] int type() const override
|
||||
{
|
||||
return Type;
|
||||
}
|
||||
ArrowItem(QSharedPointer<const ArrowData> _data, ArrowTarget *_startItem, ArrowTarget *_targetItem);
|
||||
~ArrowItem() override;
|
||||
|
||||
void onTargetDestroyed();
|
||||
void delArrow();
|
||||
void updatePath();
|
||||
void updatePath(const QPointF &endPoint);
|
||||
void startDrawAnimation();
|
||||
bool animationEvent() override;
|
||||
|
||||
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
|
||||
[[nodiscard]] QRectF boundingRect() const override
|
||||
{
|
||||
return path.boundingRect();
|
||||
return path.boundingRect().adjusted(-glowExtent, -glowExtent, glowExtent, glowExtent);
|
||||
}
|
||||
[[nodiscard]] QPainterPath shape() const override
|
||||
{
|
||||
|
|
@ -106,4 +134,4 @@ protected:
|
|||
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;
|
||||
};
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "card_item.h"
|
||||
|
||||
#include "../../client/settings/cache_settings.h"
|
||||
#include "../../client/settings/card_counter_settings.h"
|
||||
#include "../../game/phase.h"
|
||||
#include "../../game/player/player_actions.h"
|
||||
#include "../../game/player/player_logic.h"
|
||||
|
|
@ -19,6 +20,7 @@
|
|||
#include <QPainter>
|
||||
#include <libcockatrice/card/card_info.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_card.pb.h>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
|
||||
CardItem::CardItem(PlayerLogic *_owner,
|
||||
QGraphicsItem *parent,
|
||||
|
|
@ -279,7 +281,7 @@ void CardItem::drawArrow(const QColor &arrowColor)
|
|||
auto *game = owner->getGame();
|
||||
PlayerLogic *arrowOwner = game->getPlayerManager()->getActiveLocalPlayer(game->getGameState()->getActivePlayer());
|
||||
int phase = 0; // 0 means to not set the phase
|
||||
if (SettingsCache::instance().getDoNotDeleteArrowsInSubPhases()) {
|
||||
if (SettingsCache::instance().userInterface().getDoNotDeleteArrowsInSubPhases()) {
|
||||
int currentPhase = game->getGameState()->getCurrentPhase();
|
||||
phase = Phases::getLastSubphase(currentPhase) + 1;
|
||||
}
|
||||
|
|
@ -398,7 +400,7 @@ void CardItem::playCard(bool faceDown)
|
|||
if (tz) {
|
||||
emit tz->toggleTapped();
|
||||
} else {
|
||||
if (SettingsCache::instance().getClickPlaysAllSelected()) {
|
||||
if (SettingsCache::instance().userInterface().getClickPlaysAllSelected()) {
|
||||
if (faceDown) {
|
||||
emit playSelectedFaceDown(this);
|
||||
} else {
|
||||
|
|
@ -462,7 +464,7 @@ static bool isUnwritableRevealZone(CardZoneLogic *zone)
|
|||
void CardItem::handleClickedToPlay(bool shiftHeld)
|
||||
{
|
||||
if (isUnwritableRevealZone(state->getZone())) {
|
||||
if (SettingsCache::instance().getClickPlaysAllSelected()) {
|
||||
if (SettingsCache::instance().userInterface().getClickPlaysAllSelected()) {
|
||||
emit hideSelected(this);
|
||||
} else {
|
||||
state->getZone()->removeCard(this);
|
||||
|
|
@ -479,7 +481,7 @@ void CardItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
|||
return;
|
||||
}
|
||||
if ((event->modifiers() != Qt::AltModifier) && (event->button() == Qt::LeftButton) &&
|
||||
(!SettingsCache::instance().getDoubleClickToPlay())) {
|
||||
(!SettingsCache::instance().userInterface().getDoubleClickToPlay())) {
|
||||
handleClickedToPlay(event->modifiers().testFlag(Qt::ShiftModifier));
|
||||
}
|
||||
if (owner != nullptr) {
|
||||
|
|
@ -491,7 +493,7 @@ void CardItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
|||
void CardItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
|
||||
{
|
||||
if ((event->modifiers() != Qt::AltModifier) && (event->buttons() == Qt::LeftButton) &&
|
||||
(SettingsCache::instance().getDoubleClickToPlay())) {
|
||||
(SettingsCache::instance().userInterface().getDoubleClickToPlay())) {
|
||||
handleClickedToPlay(event->modifiers().testFlag(Qt::ShiftModifier));
|
||||
}
|
||||
event->accept();
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@
|
|||
#include "abstract_card_item.h"
|
||||
|
||||
#include <libcockatrice/network/server/remote/game/server_card.h>
|
||||
#include <libcockatrice/utility/trice_limits.h>
|
||||
|
||||
class CardDatabase;
|
||||
class CardDragItem;
|
||||
|
|
@ -138,7 +137,7 @@ public:
|
|||
void resetState(bool keepAnnotations = false);
|
||||
void processCardInfo(const ServerInfo_Card &_info);
|
||||
|
||||
bool animationEvent();
|
||||
bool animationEvent() override;
|
||||
CardDragItem *createDragItem(int _id, const QPointF &_pos, const QPointF &_scenePos, bool forceFaceDown);
|
||||
void deleteDragItem();
|
||||
void drawArrow(const QColor &arrowColor);
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ enum GraphicsItemType
|
|||
typeZone = QGraphicsItem::UserType + 3,
|
||||
typePlayerTarget = QGraphicsItem::UserType + 4,
|
||||
typeDeckViewCardContainer = QGraphicsItem::UserType + 5,
|
||||
typeOther = QGraphicsItem::UserType + 6
|
||||
typeOther = QGraphicsItem::UserType + 6,
|
||||
typeArrow = QGraphicsItem::UserType + 7
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_GRAPHICS_ITEM_TYPE_H
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
#include <libcockatrice/card/card_info.h>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
|
||||
DeckViewCardDragItem::DeckViewCardDragItem(DeckViewCard *_item,
|
||||
const QPointF &_hotSpot,
|
||||
|
|
@ -77,11 +78,12 @@ DeckViewCard::DeckViewCard(QGraphicsItem *parent, const CardRef &cardRef, const
|
|||
{
|
||||
setAcceptHoverEvents(true);
|
||||
|
||||
connect(&SettingsCache::instance(), &SettingsCache::roundCardCornersChanged, this, [this](bool _roundCardCorners) {
|
||||
Q_UNUSED(_roundCardCorners);
|
||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::roundCardCornersChanged, this,
|
||||
[this](bool _roundCardCorners) {
|
||||
Q_UNUSED(_roundCardCorners);
|
||||
|
||||
update();
|
||||
});
|
||||
update();
|
||||
});
|
||||
}
|
||||
|
||||
DeckViewCard::~DeckViewCard()
|
||||
|
|
@ -99,7 +101,8 @@ void DeckViewCard::paint(QPainter *painter, const QStyleOptionGraphicsItem *opti
|
|||
pen.setJoinStyle(Qt::MiterJoin);
|
||||
pen.setColor(originZone == DECK_ZONE_MAIN ? Qt::green : Qt::red);
|
||||
painter->setPen(pen);
|
||||
qreal cardRadius = SettingsCache::instance().getRoundCardCorners() ? 0.05 * (CardDimensions::WIDTH_F - 3) : 0.0;
|
||||
qreal cardRadius =
|
||||
SettingsCache::instance().cardsDisplay().getRoundCardCorners() ? 0.05 * (CardDimensions::WIDTH_F - 3) : 0.0;
|
||||
painter->drawRoundedRect(QRectF(1.5, 1.5, CardDimensions::WIDTH_F - 3, CardDimensions::HEIGHT_F - 3), cardRadius,
|
||||
cardRadius);
|
||||
painter->restore();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "deck_view_container.h"
|
||||
|
||||
#include "../../client/settings/cache_settings.h"
|
||||
#include "../../client/settings/shortcuts_settings.h"
|
||||
#include "../../interface/card_picture_loader/card_picture_loader.h"
|
||||
#include "../../interface/deck_loader/deck_loader.h"
|
||||
#include "../../interface/widgets/dialogs/dlg_load_deck.h"
|
||||
|
|
@ -13,13 +14,17 @@
|
|||
#include <QMessageBox>
|
||||
#include <libcockatrice/card/database/card_database.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_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_plan.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_deck_download.pb.h>
|
||||
#include <libcockatrice/protocol/pending_command.h>
|
||||
#include <libcockatrice/utility/trice_limits.h>
|
||||
#include <libcockatrice/settings/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)
|
||||
{
|
||||
|
|
@ -95,8 +100,11 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent)
|
|||
&DeckViewContainer::refreshShortcuts);
|
||||
refreshShortcuts();
|
||||
|
||||
connect(&SettingsCache::instance(), &SettingsCache::visualDeckStorageInGameChanged, this,
|
||||
&DeckViewContainer::setVisualDeckStorageExists);
|
||||
connect(&SettingsCache::instance().visualDeckStorage(), &VisualDeckStorageSettings::visualDeckStorageInGameChanged,
|
||||
this, &DeckViewContainer::setVisualDeckStorageExists);
|
||||
|
||||
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::playmatSettingsChanged, this,
|
||||
&DeckViewContainer::onPlaymatSettingsChanged);
|
||||
|
||||
switchToDeckSelectView();
|
||||
}
|
||||
|
|
@ -138,7 +146,7 @@ static void setVisibility(QPushButton *button, bool visible)
|
|||
|
||||
void DeckViewContainer::switchToDeckSelectView()
|
||||
{
|
||||
if (SettingsCache::instance().getVisualDeckStorageInGame()) {
|
||||
if (SettingsCache::instance().visualDeckStorage().getVisualDeckStorageInGame()) {
|
||||
deckView->setHidden(true);
|
||||
|
||||
tryCreateVisualDeckStorageWidget();
|
||||
|
|
@ -275,6 +283,8 @@ void DeckViewContainer::loadDeckFromFile(const QString &filePath)
|
|||
|
||||
void DeckViewContainer::loadDeckFromDeckList(const DeckList &deck)
|
||||
{
|
||||
currentDeck = deck;
|
||||
|
||||
QString deckString = deck.writeToString_Native();
|
||||
|
||||
if (deckString.length() > MAX_FILE_LENGTH) {
|
||||
|
|
@ -287,6 +297,52 @@ void DeckViewContainer::loadDeckFromDeckList(const DeckList &deck)
|
|||
PendingCommand *pend = parentGame->getGame()->getGameEventHandler()->prepareGameCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &DeckViewContainer::deckSelectFinished);
|
||||
parentGame->getGame()->getGameEventHandler()->sendGameCommand(pend, playerId);
|
||||
|
||||
resolveAndSendPlaymat();
|
||||
}
|
||||
|
||||
void DeckViewContainer::resolveAndSendPlaymat()
|
||||
{
|
||||
if (currentDeck.getCardRefList().isEmpty() && currentDeck.getPlaymat().card.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &settings = SettingsCache::instance().userInterface();
|
||||
const auto fallbackBehavior = static_cast<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()
|
||||
|
|
@ -377,6 +433,10 @@ void DeckViewContainer::sideboardPlanChanged()
|
|||
*/
|
||||
void DeckViewContainer::sendReadyStartCommand(bool ready)
|
||||
{
|
||||
if (ready) {
|
||||
resolveAndSendPlaymat();
|
||||
}
|
||||
|
||||
Command_ReadyStart cmd;
|
||||
cmd.set_ready(ready);
|
||||
parentGame->getGame()->getGameEventHandler()->sendGameCommand(cmd, playerId);
|
||||
|
|
@ -414,6 +474,7 @@ void DeckViewContainer::setSideboardLocked(bool locked)
|
|||
|
||||
void DeckViewContainer::setDeck(const DeckList &deck)
|
||||
{
|
||||
currentDeck = deck;
|
||||
deckView->setDeck(deck);
|
||||
switchToDeckLoadedView();
|
||||
}
|
||||
|
|
@ -57,6 +57,9 @@ private:
|
|||
VisualDeckStorageWidget *visualDeckStorageWidget;
|
||||
TabGame *parentGame;
|
||||
int playerId;
|
||||
int playmatRotationIndex = 0; ///< Per-match cursor for round-robin playmat mode.
|
||||
DeckList currentDeck; ///< Cached deck for live settings re-resolution.
|
||||
PlaymatInfo lastResolvedPlaymat; ///< Tracks last sent playmat to avoid repeats in random mode.
|
||||
|
||||
void tryCreateVisualDeckStorageWidget();
|
||||
void sendReadyStartCommand(bool ready);
|
||||
|
|
@ -75,6 +78,7 @@ private slots:
|
|||
void sideboardLockButtonClicked();
|
||||
void updateSideboardLockButtonText();
|
||||
void refreshShortcuts();
|
||||
void onPlaymatSettingsChanged();
|
||||
signals:
|
||||
void newCardAdded(AbstractCardItem *card);
|
||||
void notIdle();
|
||||
|
|
@ -87,6 +91,8 @@ public:
|
|||
void setSideboardLocked(bool locked);
|
||||
void setDeck(const DeckList &deck);
|
||||
void setVisualDeckStorageExists(bool exists);
|
||||
void advancePlaymatRotation();
|
||||
void resolveAndSendPlaymat();
|
||||
|
||||
public slots:
|
||||
void loadDeckFromFile(const QString &filePath);
|
||||
|
|
|
|||
|
|
@ -20,7 +20,10 @@
|
|||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
#include <libcockatrice/models/database/card_database_model.h>
|
||||
#include <libcockatrice/models/database/token/token_display_model.h>
|
||||
#include <libcockatrice/utility/trice_limits.h>
|
||||
#include <libcockatrice/settings/card_override_settings.h>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
#include <libcockatrice/settings/layouts_settings.h>
|
||||
#include <libcockatrice/utility/string_limits.h>
|
||||
|
||||
DlgCreateToken::DlgCreateToken(const QStringList &_predefinedTokens, QWidget *parent)
|
||||
: QDialog(parent), predefinedTokens(_predefinedTokens)
|
||||
|
|
@ -186,7 +189,7 @@ void DlgCreateToken::tokenSelectionChanged(const QModelIndex ¤t, const QMo
|
|||
const QChar cardColor = cardInfo->getColorChar();
|
||||
colorEdit->setCurrentIndex(colorEdit->findData(cardColor, Qt::UserRole, Qt::MatchFixedString));
|
||||
ptEdit->setText(cardInfo->getPowTough());
|
||||
if (SettingsCache::instance().getAnnotateTokens()) {
|
||||
if (SettingsCache::instance().userInterface().getAnnotateTokens()) {
|
||||
annotationEdit->setText(cardInfo->getText());
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
#include <QSpinBox>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
#include <libcockatrice/utility/trice_limits.h>
|
||||
#include <libcockatrice/utility/dice_limits.h>
|
||||
|
||||
DlgRollDice::DlgRollDice(QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@
|
|||
#include <QDebug>
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
#include <QGraphicsView>
|
||||
#include <QSet>
|
||||
#include <QtMath>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
#include <libcockatrice/utility/zone_names.h>
|
||||
#include <numeric>
|
||||
|
||||
|
|
@ -36,7 +36,7 @@ GameScene::GameScene(PhasesToolbar *_phasesToolbar, QObject *parent)
|
|||
{
|
||||
animationTimer = new QBasicTimer;
|
||||
addItem(phasesToolbar);
|
||||
connect(&SettingsCache::instance(), &SettingsCache::minPlayersForMultiColumnLayoutChanged, this,
|
||||
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::minPlayersForMultiColumnLayoutChanged, this,
|
||||
&GameScene::rearrange);
|
||||
|
||||
rearrange();
|
||||
|
|
@ -44,7 +44,25 @@ GameScene::GameScene(PhasesToolbar *_phasesToolbar, QObject *parent)
|
|||
|
||||
GameScene::~GameScene()
|
||||
{
|
||||
// Sever all incoming connections (animated item destroy-tracking) before the
|
||||
// members below are destroyed: the base QGraphicsScene destructor destroys the
|
||||
// remaining items, and their destroyed() signals must not reach slots that
|
||||
// reference members that no longer exist.
|
||||
disconnect(this);
|
||||
|
||||
delete animationTimer;
|
||||
animationTimer = nullptr;
|
||||
|
||||
// Delete all ArrowItems before QGraphicsScene's base destructor runs.
|
||||
// QGraphicsScene::~QGraphicsScene() destroys items in arbitrary order.
|
||||
// If a PlayerTarget is destroyed before an ArrowItem pointing to it,
|
||||
// ArrowItem::onTargetDestroyed fires and emits on the partially-destroyed
|
||||
// GameScene, causing a segfault.
|
||||
for (auto *item : items()) {
|
||||
if (auto *arrow = qgraphicsitem_cast<ArrowItem *>(item)) {
|
||||
delete arrow;
|
||||
}
|
||||
}
|
||||
|
||||
// DO NOT call clearViews() here
|
||||
// clearViews calls close() on the zoneViews, which sends signals; sending signals in destructors leads to segfaults
|
||||
|
|
@ -234,17 +252,27 @@ void GameScene::adjustPlayerRotation(int rotationAdjustment)
|
|||
*/
|
||||
void GameScene::rearrange()
|
||||
{
|
||||
int firstPlayerIndex = 0;
|
||||
auto playersPlaying = collectActivePlayers(firstPlayerIndex);
|
||||
playersPlaying = rotatePlayers(playersPlaying, firstPlayerIndex);
|
||||
if (rearranging) {
|
||||
needsReArrange = true;
|
||||
return;
|
||||
}
|
||||
rearranging = true;
|
||||
do {
|
||||
needsReArrange = false;
|
||||
|
||||
int columns = determineColumnCount(playersPlaying.size());
|
||||
QSizeF sceneSize = computeSceneSizeAndPlayerLayout(playersPlaying, columns);
|
||||
int firstPlayerIndex = 0;
|
||||
auto playersPlaying = collectActivePlayers(firstPlayerIndex);
|
||||
playersPlaying = rotatePlayers(playersPlaying, firstPlayerIndex);
|
||||
|
||||
phasesToolbar->setHeight(sceneSize.height());
|
||||
setSceneRect(0, 0, sceneSize.width(), sceneSize.height());
|
||||
int columns = determineColumnCount(playersPlaying.size());
|
||||
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 ----------
|
||||
|
|
@ -324,7 +352,7 @@ QList<PlayerLogic *> GameScene::rotatePlayers(const QList<PlayerLogic *> &active
|
|||
|
||||
int GameScene::determineColumnCount(int playerCount)
|
||||
{
|
||||
return playerCount < SettingsCache::instance().getMinPlayersForMultiColumnLayout() ? 1 : 2;
|
||||
return playerCount < SettingsCache::instance().userInterface().getMinPlayersForMultiColumnLayout() ? 1 : 2;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -441,8 +469,14 @@ void GameScene::resizeColumnsAndPlayers(const QList<qreal> &minWidthByColumn, qr
|
|||
qreal extraWidthPerColumn = (newWidth - minWidth) / playersByColumn.size();
|
||||
qreal newx = phasesToolbar->getWidth();
|
||||
|
||||
for (int col = 0; col < playersByColumn.size(); ++col) {
|
||||
for (PlayerGraphicsItem *player : playersByColumn[col]) {
|
||||
// Snapshot the columns: resizing a player's table can synchronously trigger
|
||||
// GameScene::rearrange (table width -> sizeChanged -> updateBoundingRect ->
|
||||
// sizeChanged -> rearrange), and rearrange rebuilds playersByColumn. Iterating
|
||||
// the live container across that re-entrant call would use invalidated iterators.
|
||||
const QList<QList<PlayerGraphicsItem *>> columns = playersByColumn;
|
||||
|
||||
for (int col = 0; col < columns.size(); ++col) {
|
||||
for (PlayerGraphicsItem *player : columns[col]) {
|
||||
player->processSceneSizeChange(minWidthByColumn[col] + extraWidthPerColumn);
|
||||
player->setPos(newx, player->y());
|
||||
}
|
||||
|
|
@ -484,6 +518,7 @@ void GameScene::addArrow(QSharedPointer<ArrowData> data)
|
|||
|
||||
auto *arrow = new ArrowItem(data, startCard, targetItem);
|
||||
addItem(arrow);
|
||||
arrow->startDrawAnimation();
|
||||
arrowRegistry.insert(data, arrow);
|
||||
connect(arrow, &ArrowItem::requestDeletion, this, &GameScene::requestArrowDeletion);
|
||||
}
|
||||
|
|
@ -529,7 +564,9 @@ void GameScene::clearArrowsForPlayer(int playerId)
|
|||
void GameScene::clearArrowsForPlayerLocally(int playerId)
|
||||
{
|
||||
for (int arrowId : arrowRegistry.idsForPlayer(playerId)) {
|
||||
arrowRegistry.take(playerId, arrowId)->delArrow();
|
||||
if (auto *arrow = arrowRegistry.take(playerId, arrowId)) {
|
||||
arrow->delArrow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -722,30 +759,45 @@ bool GameScene::event(QEvent *event)
|
|||
|
||||
void GameScene::timerEvent(QTimerEvent * /*event*/)
|
||||
{
|
||||
QMutableSetIterator<CardItem *> i(cardsToAnimate);
|
||||
QMutableHashIterator<QObject *, IAnimatedItem *> i(animatedItems);
|
||||
while (i.hasNext()) {
|
||||
i.next();
|
||||
if (!i.value()->animationEvent()) {
|
||||
i.remove();
|
||||
}
|
||||
}
|
||||
if (cardsToAnimate.isEmpty()) {
|
||||
if (animatedItems.isEmpty()) {
|
||||
animationTimer->stop();
|
||||
}
|
||||
}
|
||||
|
||||
void GameScene::registerAnimationItem(AbstractCardItem *card)
|
||||
void GameScene::registerAnimationItem(IAnimatedItem *item)
|
||||
{
|
||||
cardsToAnimate.insert(static_cast<CardItem *>(card));
|
||||
if (!animationTimer->isActive()) {
|
||||
auto *object = dynamic_cast<QObject *>(item);
|
||||
if (!object) {
|
||||
return;
|
||||
}
|
||||
if (!animatedItems.contains(object)) {
|
||||
connect(object, &QObject::destroyed, this, &GameScene::removeAnimatedItem);
|
||||
}
|
||||
animatedItems.insert(object, item);
|
||||
if (animationTimer && !animationTimer->isActive()) {
|
||||
animationTimer->start(10, this);
|
||||
}
|
||||
}
|
||||
|
||||
void GameScene::unregisterAnimationItem(AbstractCardItem *card)
|
||||
void GameScene::unregisterAnimationItem(IAnimatedItem *item)
|
||||
{
|
||||
cardsToAnimate.remove(static_cast<CardItem *>(card));
|
||||
if (cardsToAnimate.isEmpty()) {
|
||||
animatedItems.remove(dynamic_cast<QObject *>(item));
|
||||
if (animationTimer && animatedItems.isEmpty()) {
|
||||
animationTimer->stop();
|
||||
}
|
||||
}
|
||||
|
||||
void GameScene::removeAnimatedItem(QObject *item)
|
||||
{
|
||||
animatedItems.remove(item);
|
||||
if (animationTimer && animatedItems.isEmpty()) {
|
||||
animationTimer->stop();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,13 +4,14 @@
|
|||
#include "../game/arrow_registry.h"
|
||||
#include "../game/board/arrow_data.h"
|
||||
#include "../game/zones/card_zone_logic.h"
|
||||
#include "animated_item.h"
|
||||
#include "board/arrow_item.h"
|
||||
|
||||
#include <QGraphicsScene>
|
||||
#include <QHash>
|
||||
#include <QList>
|
||||
#include <QLoggingCategory>
|
||||
#include <QPointer>
|
||||
#include <QSet>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(GameSceneLog, "game_scene");
|
||||
inline Q_LOGGING_CATEGORY(GameScenePlayerAdditionRemovalLog, "game_scene.player_addition_removal");
|
||||
|
|
@ -24,6 +25,7 @@ class CardItem;
|
|||
class ServerInfo_Card;
|
||||
class PhasesToolbar;
|
||||
class QBasicTimer;
|
||||
class QObject;
|
||||
|
||||
/**
|
||||
* @class GameScene
|
||||
|
|
@ -50,9 +52,11 @@ private:
|
|||
QList<ZoneViewWidget *> zoneViews; ///< Active zone view widgets
|
||||
QSize viewSize; ///< Current view size
|
||||
QPointer<CardItem> hoveredCard; ///< Currently hovered card
|
||||
QBasicTimer *animationTimer; ///< Timer for card animations
|
||||
QSet<CardItem *> cardsToAnimate; ///< Cards currently animating
|
||||
QBasicTimer *animationTimer; ///< Timer for scene animations
|
||||
QHash<QObject *, IAnimatedItem *> animatedItems; ///< Items currently animating
|
||||
int playerRotation; ///< Rotation offset for player layout
|
||||
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.
|
||||
|
|
@ -182,15 +186,24 @@ public:
|
|||
/** @brief Updates hovered card highlighting. */
|
||||
void updateHoveredCard(CardItem *newCard);
|
||||
|
||||
/** @brief Registers a card for animation updates. */
|
||||
void registerAnimationItem(AbstractCardItem *card);
|
||||
/**
|
||||
* @brief Registers an item for animation updates with the shared scene timer.
|
||||
*
|
||||
* The item must inherit QObject; it is unregistered automatically when it is
|
||||
* destroyed, so it may be deleted mid-animation without a dangling pointer.
|
||||
*/
|
||||
void registerAnimationItem(IAnimatedItem *item);
|
||||
|
||||
/** @brief Unregisters a card from animation updates. */
|
||||
void unregisterAnimationItem(AbstractCardItem *card);
|
||||
/** @brief Unregisters an item from animation updates. */
|
||||
void unregisterAnimationItem(IAnimatedItem *item);
|
||||
void startRubberBand(const QPointF &selectionOrigin);
|
||||
void resizeRubberBand(const QPointF &cursorPoint, int selectedCount);
|
||||
void stopRubberBand();
|
||||
|
||||
private slots:
|
||||
/** @brief Removes a destroyed item from the animation set. */
|
||||
void removeAnimatedItem(QObject *item);
|
||||
|
||||
public slots:
|
||||
void onCardSelectionChanged(AbstractCardItem *card, bool selected);
|
||||
void onCardRightClicked(AbstractCardItem *card, QPoint screenPos);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#include "game_view.h"
|
||||
|
||||
#include "../client/settings/cache_settings.h"
|
||||
#include "../game/selection_subtype_tally.h"
|
||||
#include "../client/settings/shortcuts_settings.h"
|
||||
#include "game_scene.h"
|
||||
|
||||
#include <QAction>
|
||||
|
|
@ -10,6 +10,7 @@
|
|||
#include <QLayout>
|
||||
#include <QResizeEvent>
|
||||
#include <QRubberBand>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
#include <libcockatrice/utility/qt_utils.h>
|
||||
|
||||
// QRubberBand calls raise() in showEvent() and changeEvent() to stay on top of siblings.
|
||||
|
|
@ -46,9 +47,12 @@ GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, par
|
|||
connect(scene, &GameScene::sigResizeRubberBand, this, &GameView::resizeRubberBand);
|
||||
connect(scene, &GameScene::sigStopRubberBand, this, &GameView::stopRubberBand);
|
||||
connect(scene, &QGraphicsScene::selectionChanged, this, [this]() { updateTotalSelectionCount(); });
|
||||
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::tallyTypeChanged, this,
|
||||
[this] { updateTotalSelectionCount(); });
|
||||
|
||||
setFocusDisabled(SettingsCache::instance().getKeepGameChatFocus());
|
||||
connect(&SettingsCache::instance(), &SettingsCache::keepGameChatFocusChanged, this, &GameView::setFocusDisabled);
|
||||
setFocusDisabled(SettingsCache::instance().userInterface().getKeepGameChatFocus());
|
||||
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::keepGameChatFocusChanged, this,
|
||||
&GameView::setFocusDisabled);
|
||||
|
||||
aCloseMostRecentZoneView = new QAction(this);
|
||||
|
||||
|
|
@ -79,12 +83,12 @@ GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, par
|
|||
totalCountLabel->setStyleSheet(totalCountLabelStyle);
|
||||
totalCountLabel->hide();
|
||||
|
||||
subtypeTallyContainer = new QWidget(this);
|
||||
subtypeTallyContainer->setStyleSheet(subtypeTallyLabelStyle);
|
||||
subtypeTallyLayout = new QGridLayout(subtypeTallyContainer);
|
||||
subtypeTallyLayout->setContentsMargins(2, 2, 2, 2);
|
||||
subtypeTallyLayout->setSpacing(2);
|
||||
subtypeTallyContainer->hide();
|
||||
tallyContainer = new QWidget(this);
|
||||
tallyContainer->setStyleSheet(subtypeTallyLabelStyle);
|
||||
tallyLayout = new QGridLayout(tallyContainer);
|
||||
tallyLayout->setContentsMargins(2, 2, 2, 2);
|
||||
tallyLayout->setSpacing(2);
|
||||
tallyContainer->hide();
|
||||
}
|
||||
|
||||
void GameView::resizeEvent(QResizeEvent *event)
|
||||
|
|
@ -110,6 +114,7 @@ void GameView::startRubberBand(const QPointF &_selectionOrigin)
|
|||
}
|
||||
|
||||
selectionOrigin = _selectionOrigin;
|
||||
previousBandRect = QRect();
|
||||
rubberBand->setGeometry(QRect(mapFromScene(selectionOrigin), QSize(0, 0)));
|
||||
rubberBand->show();
|
||||
}
|
||||
|
|
@ -124,9 +129,19 @@ void GameView::resizeRubberBand(const QPointF &cursorPoint, int selectedCount)
|
|||
|
||||
QPoint cursor = cursorPoint.toPoint();
|
||||
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();
|
||||
return;
|
||||
}
|
||||
|
|
@ -167,7 +182,13 @@ void GameView::stopRubberBand()
|
|||
return;
|
||||
}
|
||||
|
||||
// Same rationale as resizeRubberBand: repaint the last known band area
|
||||
// since hiding a child widget doesn't reliably invalidate its region.
|
||||
rubberBand->hide();
|
||||
if (viewport() && !previousBandRect.isNull()) {
|
||||
viewport()->update(previousBandRect.adjusted(-1, -1, 1, 1));
|
||||
previousBandRect = QRect();
|
||||
}
|
||||
dragCountLabel->hide();
|
||||
}
|
||||
|
||||
|
|
@ -177,14 +198,14 @@ void GameView::refreshShortcuts()
|
|||
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 countStyle =
|
||||
|
|
@ -195,16 +216,16 @@ QSize GameView::rebuildSubtypeLabels(const QList<SubtypeEntry> &entries)
|
|||
int maxCountWidth = 0;
|
||||
|
||||
int row = 0;
|
||||
for (const SubtypeEntry &entry : entries) {
|
||||
auto *nameLabel = new QLabel(entry.name, subtypeTallyContainer);
|
||||
for (const TallyRow &entry : entries) {
|
||||
auto *nameLabel = new QLabel(entry.name, tallyContainer);
|
||||
nameLabel->setStyleSheet(nameStyle);
|
||||
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->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||||
subtypeTallyLayout->addWidget(countLabel, row, 1);
|
||||
tallyLayout->addWidget(countLabel, row, 1);
|
||||
|
||||
QSize nameSize = nameLabel->sizeHint();
|
||||
QSize countSize = countLabel->sizeHint();
|
||||
|
|
@ -215,9 +236,9 @@ QSize GameView::rebuildSubtypeLabels(const QList<SubtypeEntry> &entries)
|
|||
++row;
|
||||
}
|
||||
|
||||
int spacing = subtypeTallyLayout->spacing();
|
||||
int margins = subtypeTallyLayout->contentsMargins().left() + subtypeTallyLayout->contentsMargins().right();
|
||||
int verticalMargins = subtypeTallyLayout->contentsMargins().top() + subtypeTallyLayout->contentsMargins().bottom();
|
||||
int spacing = tallyLayout->spacing();
|
||||
int margins = tallyLayout->contentsMargins().left() + tallyLayout->contentsMargins().right();
|
||||
int verticalMargins = tallyLayout->contentsMargins().top() + tallyLayout->contentsMargins().bottom();
|
||||
|
||||
int width = maxNameWidth + spacing + maxCountWidth + margins;
|
||||
int height = totalHeight + (row - 1) * spacing + verticalMargins;
|
||||
|
|
@ -235,7 +256,7 @@ void GameView::updateTotalSelectionCount(const QSize &viewSize)
|
|||
|
||||
int count = scene()->selectedItems().count();
|
||||
|
||||
if (!SettingsCache::instance().getShowTotalSelectionCount() || count <= 1) {
|
||||
if (!SettingsCache::instance().userInterface().getShowTotalSelectionCount() || count <= 1) {
|
||||
totalCountLabel->hide();
|
||||
} else {
|
||||
totalCountLabel->setText(QString::number(count));
|
||||
|
|
@ -247,29 +268,25 @@ void GameView::updateTotalSelectionCount(const QSize &viewSize)
|
|||
totalCountLabel->show();
|
||||
}
|
||||
|
||||
if (!SettingsCache::instance().getShowSubtypeSelectionTally() || count <= 1) {
|
||||
subtypeTallyContainer->hide();
|
||||
cachedSubtypeEntries.clear();
|
||||
return;
|
||||
}
|
||||
TallyType tallyType = Tally::intToType(SettingsCache::instance().userInterface().getTallyType());
|
||||
|
||||
GameScene *gameScene = static_cast<GameScene *>(scene());
|
||||
QList<SubtypeEntry> entries = SelectionSubtypeTally::countSubtypes(gameScene->selectedCards());
|
||||
QList<TallyRow> entries = Tally::compute(gameScene->selectedCards(), tallyType);
|
||||
|
||||
if (entries.isEmpty()) {
|
||||
subtypeTallyContainer->hide();
|
||||
cachedSubtypeEntries.clear();
|
||||
tallyContainer->hide();
|
||||
cachedTallyRows.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// Only rebuild labels if entries changed
|
||||
QSize containerSize;
|
||||
if (entries != cachedSubtypeEntries) {
|
||||
cachedSubtypeEntries = entries;
|
||||
containerSize = rebuildSubtypeLabels(entries);
|
||||
subtypeTallyContainer->resize(containerSize);
|
||||
if (entries != cachedTallyRows) {
|
||||
cachedTallyRows = entries;
|
||||
containerSize = rebuildTallyLabels(entries);
|
||||
tallyContainer->resize(containerSize);
|
||||
} else {
|
||||
containerSize = subtypeTallyContainer->size();
|
||||
containerSize = tallyContainer->size();
|
||||
}
|
||||
|
||||
int x = availableWidth - containerSize.width() - kMarginInPixels;
|
||||
|
|
@ -283,8 +300,8 @@ void GameView::updateTotalSelectionCount(const QSize &viewSize)
|
|||
|
||||
y = qMax(kMarginInPixels, y);
|
||||
|
||||
subtypeTallyContainer->move(x, y);
|
||||
subtypeTallyContainer->show();
|
||||
tallyContainer->move(x, y);
|
||||
tallyContainer->show();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
#ifndef GAMEVIEW_H
|
||||
#define GAMEVIEW_H
|
||||
|
||||
#include "../game/selection_subtype_tally.h"
|
||||
#include "tally/tally.h"
|
||||
|
||||
#include <QGraphicsView>
|
||||
|
||||
|
|
@ -24,13 +24,14 @@ private:
|
|||
QRubberBand *rubberBand;
|
||||
QLabel *dragCountLabel;
|
||||
QLabel *totalCountLabel;
|
||||
QWidget *subtypeTallyContainer;
|
||||
QGridLayout *subtypeTallyLayout;
|
||||
QWidget *tallyContainer;
|
||||
QGridLayout *tallyLayout;
|
||||
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);
|
||||
void clearSubtypeLabels();
|
||||
QSize rebuildTallyLabels(const QList<TallyRow> &entries);
|
||||
void clearTallyLabels();
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "card_menu.h"
|
||||
|
||||
#include "../../../client/settings/card_counter_settings.h"
|
||||
#include "../../../client/settings/shortcuts_settings.h"
|
||||
#include "../../../interface/widgets/tabs/tab_game.h"
|
||||
#include "../../board/card_item.h"
|
||||
#include "../../game/player/player_actions.h"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "grave_menu.h"
|
||||
|
||||
#include "../../../client/settings/shortcuts_settings.h"
|
||||
#include "../../game/abstract_game.h"
|
||||
#include "../../game/player/player_actions.h"
|
||||
#include "../../game/player/player_logic.h"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "move_menu.h"
|
||||
|
||||
#include "../../../client/settings/shortcuts_settings.h"
|
||||
#include "../../game/player/player_actions.h"
|
||||
#include "../../game/player/player_logic.h"
|
||||
#include "../card_menu_action_type.h"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "player_menu.h"
|
||||
|
||||
#include "../../../client/settings/shortcuts_settings.h"
|
||||
#include "../../../game_graphics/zones/hand_zone.h"
|
||||
#include "../../../game_graphics/zones/pile_zone.h"
|
||||
#include "../../../game_graphics/zones/table_zone.h"
|
||||
|
|
@ -44,6 +45,8 @@ PlayerMenu::PlayerMenu(PlayerGraphicsItem *_player) : QObject(_player), player(_
|
|||
utilityMenu = nullptr;
|
||||
}
|
||||
|
||||
tallyMenu = addManagedMenu<TallyMenu>();
|
||||
|
||||
if (player->getLogic()->getPlayerInfo()->getLocal()) {
|
||||
sayMenu = addManagedMenu<SayMenu>(player);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -15,11 +15,13 @@
|
|||
#include "rfg_menu.h"
|
||||
#include "say_menu.h"
|
||||
#include "sideboard_menu.h"
|
||||
#include "tally_menu.h"
|
||||
#include "utility_menu.h"
|
||||
|
||||
#include <QList>
|
||||
#include <QMenu>
|
||||
#include <QObject>
|
||||
#include <libcockatrice/utility/card_ref.h>
|
||||
|
||||
class CardItem;
|
||||
class CardMenu;
|
||||
|
|
@ -87,6 +89,7 @@ private:
|
|||
GraveyardMenu *graveMenu;
|
||||
RfgMenu *rfgMenu;
|
||||
UtilityMenu *utilityMenu;
|
||||
TallyMenu *tallyMenu;
|
||||
SayMenu *sayMenu;
|
||||
CustomZoneMenu *customZonesMenu;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "pt_menu.h"
|
||||
|
||||
#include "../../../client/settings/shortcuts_settings.h"
|
||||
#include "../../game/player/player_actions.h"
|
||||
#include "../../game/player/player_logic.h"
|
||||
#include "../player_graphics_item.h"
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
#include "../../game/player/player_logic.h"
|
||||
#include "../player_graphics_item.h"
|
||||
|
||||
#include <libcockatrice/settings/message_settings.h>
|
||||
SayMenu::SayMenu(PlayerGraphicsItem *_player) : player(_player)
|
||||
{
|
||||
connect(&SettingsCache::instance().messages(), &MessageSettings::messageMacrosChanged, this, &SayMenu::initSayMenu);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "sideboard_menu.h"
|
||||
|
||||
#include "../../../client/settings/shortcuts_settings.h"
|
||||
#include "../../game/player/player_actions.h"
|
||||
#include "../../game/player/player_logic.h"
|
||||
#include "../player_graphics_item.h"
|
||||
|
|
|
|||
57
cockatrice/src/game_graphics/player/menu/tally_menu.cpp
Normal file
57
cockatrice/src/game_graphics/player/menu/tally_menu.cpp
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
#include "tally_menu.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
|
||||
#include <QActionGroup>
|
||||
|
||||
TallyMenu::TallyMenu()
|
||||
{
|
||||
actionGroup = new QActionGroup(this);
|
||||
actionGroup->setExclusive(true);
|
||||
|
||||
aTallyNone = createTallyAction(TallyType::None);
|
||||
aTallySubtypes = createTallyAction(TallyType::Subtypes);
|
||||
aTallyTotalPower = createTallyAction(TallyType::TotalPower);
|
||||
|
||||
addAction(aTallyNone);
|
||||
addSeparator();
|
||||
addAction(aTallySubtypes);
|
||||
addAction(aTallyTotalPower);
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
QAction *TallyMenu::createTallyAction(TallyType tallyType)
|
||||
{
|
||||
TallyType currentType = Tally::intToType(SettingsCache::instance().userInterface().getTallyType());
|
||||
|
||||
QAction *action = new QAction(this);
|
||||
action->setCheckable(true);
|
||||
action->setChecked(tallyType == currentType);
|
||||
|
||||
connect(action, &QAction::triggered, &SettingsCache::instance().userInterface(),
|
||||
[tallyType] { SettingsCache::instance().userInterface().setTallyType(static_cast<int>(tallyType)); });
|
||||
|
||||
actionGroup->addAction(action);
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
void TallyMenu::setShortcutsActive()
|
||||
{
|
||||
// no-op because we haven't decided if we're adding shortcuts for tally types
|
||||
}
|
||||
|
||||
void TallyMenu::setShortcutsInactive()
|
||||
{
|
||||
// no-op because we haven't decided if we're adding shortcuts for tally types
|
||||
}
|
||||
|
||||
void TallyMenu::retranslateUi()
|
||||
{
|
||||
setTitle(tr("Tally"));
|
||||
|
||||
aTallyNone->setText(tr("None"));
|
||||
aTallySubtypes->setText(tr("Subtypes"));
|
||||
aTallyTotalPower->setText(tr("Total Power"));
|
||||
}
|
||||
31
cockatrice/src/game_graphics/player/menu/tally_menu.h
Normal file
31
cockatrice/src/game_graphics/player/menu/tally_menu.h
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
#ifndef COCKATRICE_TALLY_MENU_H
|
||||
#define COCKATRICE_TALLY_MENU_H
|
||||
|
||||
#include "../../../interface/widgets/menus/tearoff_menu.h"
|
||||
#include "../../tally/tally.h"
|
||||
#include "abstract_player_component.h"
|
||||
|
||||
#include <QMenu>
|
||||
|
||||
class TallyMenu : public TearOffMenu, public AbstractPlayerComponent
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
TallyMenu();
|
||||
|
||||
void setShortcutsActive() override;
|
||||
void setShortcutsInactive() override;
|
||||
void retranslateUi() override;
|
||||
|
||||
private:
|
||||
QActionGroup *actionGroup = nullptr;
|
||||
|
||||
QAction *aTallyNone = nullptr;
|
||||
QAction *aTallySubtypes = nullptr;
|
||||
QAction *aTallyTotalPower = nullptr;
|
||||
|
||||
QAction *createTallyAction(TallyType tallyType);
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_TALLY_MENU_H
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
#include "utility_menu.h"
|
||||
|
||||
#include "../../../client/settings/shortcuts_settings.h"
|
||||
#include "../../../interface/deck_loader/deck_loader.h"
|
||||
#include "../../game/player/player_actions.h"
|
||||
#include "../../game/player/player_logic.h"
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
#include <QInputDialog>
|
||||
#include <libcockatrice/card/relation/card_relation.h>
|
||||
#include <libcockatrice/utility/string_limits.h>
|
||||
|
||||
PlayerDialogs::PlayerDialogs(PlayerGraphicsItem *_player, PlayerActions *_playerActions)
|
||||
: QObject(_player), player(_player), playerActions(_playerActions)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
#include "player_graphics_item.h"
|
||||
|
||||
#include "../../game/player/player_actions.h"
|
||||
#include "../../interface/card_picture_loader/card_picture_loader.h"
|
||||
#include "../../interface/widgets/cards/art_crop_attribution.h"
|
||||
#include "../../interface/widgets/playmat/playmat_utils.h"
|
||||
#include "../../interface/widgets/tabs/tab_game.h"
|
||||
#include "../board/abstract_card_item.h"
|
||||
#include "../board/counter_general.h"
|
||||
|
|
@ -13,12 +16,16 @@
|
|||
#include "player_dialogs.h"
|
||||
|
||||
#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)
|
||||
{
|
||||
connect(&SettingsCache::instance(), &SettingsCache::horizontalHandChanged, this,
|
||||
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::horizontalHandChanged, this,
|
||||
&PlayerGraphicsItem::rearrangeZones);
|
||||
connect(&SettingsCache::instance(), &SettingsCache::handJustificationChanged, this,
|
||||
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::handJustificationChanged, this,
|
||||
&PlayerGraphicsItem::rearrangeZones);
|
||||
connect(player, &PlayerLogic::rearrangeCounters, this, &PlayerGraphicsItem::rearrangeCounters);
|
||||
connect(player, &PlayerLogic::activeChanged, this, &PlayerGraphicsItem::onPlayerActiveChanged);
|
||||
|
|
@ -27,6 +34,10 @@ PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player)
|
|||
|
||||
connect(player, &PlayerLogic::counterAdded, this, &PlayerGraphicsItem::onCounterAdded);
|
||||
connect(player, &PlayerLogic::counterRemoved, this, &PlayerGraphicsItem::onCounterRemoved);
|
||||
connect(player, &PlayerLogic::deckChanged, this, &PlayerGraphicsItem::updatePlaymat);
|
||||
connect(player, &PlayerLogic::playmatChanged, this, &PlayerGraphicsItem::updatePlaymat);
|
||||
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::playmatVisibilityChanged, this,
|
||||
[this](int) { updatePlaymat(); });
|
||||
|
||||
playerMenu = new PlayerMenu(this);
|
||||
|
||||
|
|
@ -66,6 +77,9 @@ PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player)
|
|||
|
||||
connect(tableZoneGraphicsItem, &TableZone::sizeChanged, this, &PlayerGraphicsItem::updateBoundingRect);
|
||||
|
||||
connect(this, &PlayerGraphicsItem::playmatChanged, tableZoneGraphicsItem, &TableZone::onPlaymatChanged);
|
||||
connect(this, &PlayerGraphicsItem::playmatChanged, stackZoneGraphicsItem, &StackZone::onPlaymatChanged);
|
||||
|
||||
updateBoundingRect();
|
||||
|
||||
rearrangeZones();
|
||||
|
|
@ -111,7 +125,6 @@ void PlayerGraphicsItem::initializeZones()
|
|||
rfgZoneGraphicsItem->setPos(base + QPointF(0, 2 * h + h2 + 10));
|
||||
|
||||
tableZoneGraphicsItem = new TableZone(player->getTableZone(), mirrored, this);
|
||||
connect(tableZoneGraphicsItem, &TableZone::sizeChanged, this, &PlayerGraphicsItem::updateBoundingRect);
|
||||
connect(this, &PlayerGraphicsItem::mirroredChanged, tableZoneGraphicsItem, &TableZone::setMirrored);
|
||||
|
||||
stackZoneGraphicsItem =
|
||||
|
|
@ -148,16 +161,67 @@ qreal PlayerGraphicsItem::getMinimumWidth() const
|
|||
{
|
||||
qreal result = tableZoneGraphicsItem->getMinimumWidth() + CardDimensions::HEIGHT_F + 15 + counterAreaWidth +
|
||||
stackZoneGraphicsItem->boundingRect().width();
|
||||
if (!SettingsCache::instance().getHorizontalHand()) {
|
||||
if (!SettingsCache::instance().userInterface().getHorizontalHand()) {
|
||||
result += handZoneGraphicsItem->boundingRect().width();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void PlayerGraphicsItem::paint(QPainter * /*painter*/,
|
||||
const QStyleOptionGraphicsItem * /*option*/,
|
||||
QWidget * /*widget*/)
|
||||
void PlayerGraphicsItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *)
|
||||
{
|
||||
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)
|
||||
|
|
@ -165,7 +229,7 @@ void PlayerGraphicsItem::processSceneSizeChange(int newPlayerWidth)
|
|||
// Extend table (and hand, if horizontal) to accommodate the new player width.
|
||||
qreal tableWidth = newPlayerWidth - CardDimensions::HEIGHT_F - 15 - counterAreaWidth -
|
||||
stackZoneGraphicsItem->boundingRect().width();
|
||||
if (!SettingsCache::instance().getHorizontalHand()) {
|
||||
if (!SettingsCache::instance().userInterface().getHorizontalHand()) {
|
||||
tableWidth -= handZoneGraphicsItem->boundingRect().width();
|
||||
}
|
||||
|
||||
|
|
@ -187,6 +251,11 @@ void PlayerGraphicsItem::onCounterAdded(CounterState *state)
|
|||
AbstractCounter *widget;
|
||||
if (state->getName() == "life") {
|
||||
widget = playerTarget->addCounter(state);
|
||||
connect(state, &CounterState::valueChanged, this, [this](int oldValue, int newValue) {
|
||||
if (newValue < oldValue) {
|
||||
tableZoneGraphicsItem->triggerDamageShimmer();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
widget = new GeneralCounter(state, player, true, this);
|
||||
}
|
||||
|
|
@ -233,7 +302,7 @@ void PlayerGraphicsItem::rearrangeCounters()
|
|||
void PlayerGraphicsItem::rearrangeZones()
|
||||
{
|
||||
auto base = QPointF(CardDimensions::HEIGHT_F + counterAreaWidth + 15, 0);
|
||||
if (SettingsCache::instance().getHorizontalHand()) {
|
||||
if (SettingsCache::instance().userInterface().getHorizontalHand()) {
|
||||
if (mirrored) {
|
||||
if (player->getHandZone()->contentsKnown()) {
|
||||
handVisible = true;
|
||||
|
|
@ -284,7 +353,7 @@ void PlayerGraphicsItem::updateBoundingRect()
|
|||
{
|
||||
prepareGeometryChange();
|
||||
qreal width = CardDimensions::HEIGHT_F + 15 + counterAreaWidth + stackZoneGraphicsItem->boundingRect().width();
|
||||
if (SettingsCache::instance().getHorizontalHand()) {
|
||||
if (SettingsCache::instance().userInterface().getHorizontalHand()) {
|
||||
qreal handHeight = handVisible ? handZoneGraphicsItem->boundingRect().height() : 0;
|
||||
bRect = QRectF(0, 0, width + tableZoneGraphicsItem->boundingRect().width(),
|
||||
tableZoneGraphicsItem->boundingRect().height() + handHeight);
|
||||
|
|
@ -297,3 +366,100 @@ void PlayerGraphicsItem::updateBoundingRect()
|
|||
|
||||
emit sizeChanged();
|
||||
}
|
||||
|
||||
void PlayerGraphicsItem::updatePlaymat()
|
||||
{
|
||||
int visibility = SettingsCache::instance().userInterface().getPlaymatVisibility();
|
||||
|
||||
// "Don't use playmats" — never show
|
||||
if (visibility == PlaymatVisibilityNone) {
|
||||
clearPlaymat();
|
||||
return;
|
||||
}
|
||||
|
||||
// "Show own playmat only" — hide playmats for remote players
|
||||
if (visibility == PlaymatVisibilityOwnOnly && !player->getPlayerInfo()->getLocal()) {
|
||||
clearPlaymat();
|
||||
return;
|
||||
}
|
||||
|
||||
CardRef playmatCard;
|
||||
PlaymatParams params;
|
||||
|
||||
if (player->getHasRemotePlaymat()) {
|
||||
// Prefer the server-confirmed playmat (updated by Command_SetPlaymat).
|
||||
playmatCard = player->getRemotePlaymatCard();
|
||||
params = player->getRemotePlaymatParams();
|
||||
} else if (player->getPlayerInfo()->getLocal()) {
|
||||
// Local player without a server broadcast yet: apply the full
|
||||
// settings-based resolution chain (mode, fallback list, behavior).
|
||||
const auto &settings = SettingsCache::instance().userInterface();
|
||||
const PlaymatInfo resolved = resolvePlaymatForDeck(
|
||||
player->getDeck(), settings.getPlaymatFallbackList(), static_cast<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 <QGraphicsObject>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
|
||||
class HandZone;
|
||||
class PileZone;
|
||||
|
|
@ -126,6 +127,7 @@ signals:
|
|||
void playerCountChanged();
|
||||
void mirroredChanged(bool isMirrored);
|
||||
void cardInfoRequested(const CardRef &cardRef);
|
||||
void playmatChanged(bool hasPlaymat);
|
||||
|
||||
private:
|
||||
PlayerLogic *player;
|
||||
|
|
@ -146,9 +148,23 @@ private:
|
|||
bool mirrored;
|
||||
bool handVisible = false;
|
||||
|
||||
QPixmap playmatPixmap;
|
||||
QPixmap scaledPlaymatPixmap; // down-scaled copy of playmatPixmap for the current render size
|
||||
QSize scaledPlaymatKey; // size bucket scaledPlaymatPixmap was rendered for
|
||||
PlaymatParams playmatParams;
|
||||
QString playmatAttribution;
|
||||
bool hasPlaymat = false;
|
||||
QMetaObject::Connection playmatPixmapConnection;
|
||||
|
||||
private slots:
|
||||
void updateBoundingRect();
|
||||
void rearrangeZones();
|
||||
void clearPlaymat();
|
||||
void updatePlaymat();
|
||||
void onPlaymatPixmapReady();
|
||||
|
||||
private:
|
||||
QPixmap scaledPlaymatFor(const QRectF &srcRect, const QSizeF &deviceDstSize);
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_PLAYER_GRAPHICS_ITEM_H
|
||||
|
|
|
|||
|
|
@ -25,11 +25,7 @@ bool PlayerListItemDelegate::editorEvent(QEvent *event,
|
|||
if ((event->type() == QEvent::MouseButtonPress) && index.isValid()) {
|
||||
auto *const mouseEvent = static_cast<QMouseEvent *>(event);
|
||||
if (mouseEvent->button() == Qt::RightButton) {
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
|
||||
static_cast<PlayerListWidget *>(parent())->showContextMenu(mouseEvent->globalPosition().toPoint(), index);
|
||||
#else
|
||||
static_cast<PlayerListWidget *>(parent())->showContextMenu(mouseEvent->globalPos(), index);
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
#include "player_target.h"
|
||||
|
||||
#include "../../client/settings/cache_settings.h"
|
||||
#include "../../game/player/player_logic.h"
|
||||
#include "../../interface/pixel_map_generator.h"
|
||||
#include "../game_scene.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QDebug>
|
||||
#include <QPainter>
|
||||
#include <QPixmapCache>
|
||||
|
|
@ -21,17 +24,24 @@ QRectF PlayerCounter::boundingRect() const
|
|||
|
||||
void PlayerCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
|
||||
{
|
||||
const int radius = 8;
|
||||
const qreal border = 1;
|
||||
QPainterPath path(QPointF(50 - border / 2, border / 2));
|
||||
path.lineTo(radius, border / 2);
|
||||
path.arcTo(border / 2, border / 2, 2 * radius, 2 * radius, 90, 90);
|
||||
path.lineTo(border / 2, 30 - border / 2);
|
||||
path.lineTo(50 - border / 2, 30 - border / 2);
|
||||
path.closeSubpath();
|
||||
const int radius = 15;
|
||||
const qreal border = 1.5;
|
||||
// The box is drawn with a border-wide stroke straddling the path, so the
|
||||
// visible outline spans [inset, inset + border]. Fills that must not cover
|
||||
// the outline (e.g. the life-change flash) use a path inset by `border`.
|
||||
const auto makePath = [](qreal inset) {
|
||||
QPainterPath path(QPointF(50 - inset, inset));
|
||||
path.lineTo(radius, inset);
|
||||
path.arcTo(inset, inset, 2 * radius, 2 * radius, 90, 90);
|
||||
path.lineTo(inset, 30 - inset);
|
||||
path.lineTo(50 - inset, 30 - inset);
|
||||
path.closeSubpath();
|
||||
return path;
|
||||
};
|
||||
QPainterPath path = makePath(border / 2);
|
||||
|
||||
QPen pen(QColor(100, 100, 100));
|
||||
pen.setWidth(border);
|
||||
pen.setWidthF(border);
|
||||
painter->setPen(pen);
|
||||
painter->setBrush(hovered ? QColor(50, 50, 50, 160) : QColor(0, 0, 0, 160));
|
||||
|
||||
|
|
@ -45,6 +55,48 @@ void PlayerCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*
|
|||
painter->setFont(font);
|
||||
painter->setPen(Qt::white);
|
||||
painter->drawText(translatedRect, Qt::AlignCenter, QString::number(value));
|
||||
|
||||
// Life-change flash: emerald on gain, red on loss, decaying over a few ticks.
|
||||
if (flashAlpha > 0) {
|
||||
painter->save();
|
||||
QColor flashColor = flashDelta > 0 ? QColor(52, 224, 122) : QColor(239, 68, 68);
|
||||
flashColor.setAlphaF(0.45 * flashAlpha);
|
||||
painter->setPen(Qt::NoPen);
|
||||
painter->setBrush(flashColor);
|
||||
painter->setOpacity(0.85);
|
||||
painter->drawPath(makePath(border));
|
||||
painter->restore();
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerCounter::onValueChanged(int oldValue, int newValue)
|
||||
{
|
||||
flashDelta = newValue - oldValue;
|
||||
if (flashDelta == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!SettingsCache::instance().userInterface().getLifeCounterAnimationsEnabled()) {
|
||||
flashAlpha = 0.0;
|
||||
return;
|
||||
}
|
||||
|
||||
flashAlpha = 1.0;
|
||||
flashClock.start();
|
||||
if (scene()) {
|
||||
static_cast<GameScene *>(scene())->registerAnimationItem(this);
|
||||
}
|
||||
}
|
||||
|
||||
bool PlayerCounter::animationEvent()
|
||||
{
|
||||
flashAlpha = 1.0 - flashClock.elapsed() / flashDurationMs;
|
||||
if (flashAlpha <= 0.0) {
|
||||
flashAlpha = 0.0;
|
||||
return false;
|
||||
}
|
||||
update();
|
||||
return true;
|
||||
}
|
||||
|
||||
PlayerTarget::PlayerTarget(PlayerLogic *_owner, QGraphicsItem *parentItem)
|
||||
|
|
|
|||
|
|
@ -7,21 +7,34 @@
|
|||
#ifndef PLAYERTARGET_H
|
||||
#define PLAYERTARGET_H
|
||||
|
||||
#include "../animated_item.h"
|
||||
#include "../board/abstract_counter.h"
|
||||
#include "../board/arrow_target.h"
|
||||
#include "../board/graphics_item_type.h"
|
||||
|
||||
#include <QElapsedTimer>
|
||||
#include <QPixmap>
|
||||
|
||||
class PlayerLogic;
|
||||
|
||||
class PlayerCounter : public AbstractCounter
|
||||
class PlayerCounter : public AbstractCounter, public IAnimatedItem
|
||||
{
|
||||
Q_OBJECT
|
||||
protected:
|
||||
void onValueChanged(int oldValue, int newValue) override;
|
||||
|
||||
private:
|
||||
static constexpr qreal flashDurationMs = 450.0;
|
||||
|
||||
QElapsedTimer flashClock;
|
||||
qreal flashAlpha = 0.0;
|
||||
int flashDelta = 0;
|
||||
|
||||
public:
|
||||
PlayerCounter(CounterState *state, PlayerLogic *player, QGraphicsItem *parent);
|
||||
QRectF boundingRect() const override;
|
||||
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
|
||||
bool animationEvent() override;
|
||||
};
|
||||
|
||||
class PlayerTarget : public ArrowTarget
|
||||
|
|
|
|||
36
cockatrice/src/game_graphics/tally/stats_tally.cpp
Normal file
36
cockatrice/src/game_graphics/tally/stats_tally.cpp
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
#include "stats_tally.h"
|
||||
|
||||
#include "../board/card_item.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QList>
|
||||
#include <algorithm>
|
||||
|
||||
static int sumPowers(const QList<CardItem *> &cards)
|
||||
{
|
||||
// calculate total power;
|
||||
int total = 0;
|
||||
for (auto card : cards) {
|
||||
QVariantList parsed = CardItem::parsePT(card->getPT());
|
||||
if (!parsed.isEmpty()) {
|
||||
int power = parsed.first().toInt(); // toInt will default to 0 if it's not an int
|
||||
total += qMax(power, 0);
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
QList<TallyRow> StatsTally::computeTotalPower(const QList<CardItem *> &cards)
|
||||
{
|
||||
// don't bother if none of the cards have pt
|
||||
bool hasPT =
|
||||
std::any_of(cards.cbegin(), cards.cend(), [](const CardItem *card) { return !card->getPT().isEmpty(); });
|
||||
if (!hasPT) {
|
||||
return {};
|
||||
}
|
||||
|
||||
int total = sumPowers(cards);
|
||||
|
||||
QString name = QCoreApplication::translate("StatsTally", "Total Power");
|
||||
return {TallyRow{name, QString::number(total)}};
|
||||
}
|
||||
21
cockatrice/src/game_graphics/tally/stats_tally.h
Normal file
21
cockatrice/src/game_graphics/tally/stats_tally.h
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
#ifndef COCKATRICE_STATS_TALLY_H
|
||||
#define COCKATRICE_STATS_TALLY_H
|
||||
#include "tally.h"
|
||||
|
||||
/**
|
||||
* @brief Extracts and tallies stats from selected cards.
|
||||
*/
|
||||
namespace StatsTally
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Sums the power of all selected cards
|
||||
*
|
||||
* @param cards The list of selected card items to analyze.
|
||||
* @return A single row containing the total, or an empty list if none of the cards have pt
|
||||
*/
|
||||
QList<TallyRow> computeTotalPower(const QList<CardItem *> &cards);
|
||||
|
||||
} // namespace StatsTally
|
||||
|
||||
#endif // COCKATRICE_STATS_TALLY_H
|
||||
|
|
@ -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 <algorithm>
|
||||
|
|
@ -19,12 +19,19 @@ QStringList extractSubtypesFromFace(const QString &faceType)
|
|||
return {};
|
||||
}
|
||||
|
||||
/** @brief A single subtype (e.g., "Goblin", "Warrior") with its occurrence count. */
|
||||
struct SubtypeEntry
|
||||
{
|
||||
QString name;
|
||||
int count;
|
||||
};
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
namespace SelectionSubtypeTally
|
||||
namespace SubtypeTally
|
||||
{
|
||||
|
||||
QList<SubtypeEntry> countSubtypes(const QList<CardItem *> &cards)
|
||||
QList<TallyRow> countSubtypes(const QList<CardItem *> &cards)
|
||||
{
|
||||
QMap<QString, int> subtypeCounts;
|
||||
|
||||
|
|
@ -58,7 +65,13 @@ QList<SubtypeEntry> countSubtypes(const QList<CardItem *> &cards)
|
|||
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 <libcockatrice/protocol/pb/command_move_card.pb.h>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
|
||||
HandZone::HandZone(HandZoneLogic *_logic, int _zoneHeight, QGraphicsItem *parent)
|
||||
: SelectZone(_logic, parent), zoneHeight(_zoneHeight)
|
||||
|
|
@ -33,7 +34,7 @@ void HandZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
|
|||
|
||||
QPoint point = dropPoint + scenePos().toPoint();
|
||||
int x = -1;
|
||||
if (SettingsCache::instance().getHorizontalHand()) {
|
||||
if (SettingsCache::instance().userInterface().getHorizontalHand()) {
|
||||
for (x = 0; x < getLogic()->getCards().size(); x++) {
|
||||
if (point.x() < static_cast<CardItem *>(getLogic()->getCards().at(x))->scenePos().x()) {
|
||||
break;
|
||||
|
|
@ -60,7 +61,7 @@ void HandZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
|
|||
|
||||
QRectF HandZone::boundingRect() const
|
||||
{
|
||||
if (SettingsCache::instance().getHorizontalHand()) {
|
||||
if (SettingsCache::instance().userInterface().getHorizontalHand()) {
|
||||
return QRectF(0, 0, width, CardDimensions::HEIGHT_F + 10);
|
||||
} else {
|
||||
return QRectF(0, 0, CardDimensions::WIDTH_F * 1.5, zoneHeight);
|
||||
|
|
@ -77,8 +78,8 @@ void HandZone::reorganizeCards()
|
|||
{
|
||||
if (!getLogic()->getCards().isEmpty()) {
|
||||
const int cardCount = getLogic()->getCards().size();
|
||||
if (SettingsCache::instance().getHorizontalHand()) {
|
||||
bool leftJustified = SettingsCache::instance().getLeftJustified();
|
||||
if (SettingsCache::instance().userInterface().getHorizontalHand()) {
|
||||
bool leftJustified = SettingsCache::instance().userInterface().getLeftJustified();
|
||||
qreal cardWidth = getLogic()->getCards().at(0)->boundingRect().width();
|
||||
const int xPadding = leftJustified ? cardWidth * 1.4 : 5;
|
||||
qreal totalWidth =
|
||||
|
|
@ -126,7 +127,7 @@ void HandZone::sortHand(const QList<CardList::SortOption> &options)
|
|||
|
||||
void HandZone::setWidth(qreal _width)
|
||||
{
|
||||
if (SettingsCache::instance().getHorizontalHand()) {
|
||||
if (SettingsCache::instance().userInterface().getHorizontalHand()) {
|
||||
prepareGeometryChange();
|
||||
width = _width;
|
||||
reorganizeCards();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "pile_zone.h"
|
||||
|
||||
#include "../../client/settings/cache_settings.h"
|
||||
#include "../../game/player/player_actions.h"
|
||||
#include "../../game/player/player_logic.h"
|
||||
#include "../../game/zones/pile_zone_logic.h"
|
||||
|
|
@ -11,6 +12,7 @@
|
|||
#include <QGraphicsSceneMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <libcockatrice/protocol/pb/command_move_card.pb.h>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
|
||||
PileZone::PileZone(PileZoneLogic *_logic, QGraphicsItem *parent) : CardZone(_logic, parent)
|
||||
{
|
||||
|
|
@ -23,12 +25,13 @@ PileZone::PileZone(PileZoneLogic *_logic, QGraphicsItem *parent) : CardZone(_log
|
|||
.rotate(90)
|
||||
.translate(-CardDimensions::WIDTH_HALF_F, -CardDimensions::HEIGHT_HALF_F));
|
||||
|
||||
connect(&SettingsCache::instance(), &SettingsCache::roundCardCornersChanged, this, [this](bool _roundCardCorners) {
|
||||
Q_UNUSED(_roundCardCorners);
|
||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::roundCardCornersChanged, this,
|
||||
[this](bool _roundCardCorners) {
|
||||
Q_UNUSED(_roundCardCorners);
|
||||
|
||||
prepareGeometryChange();
|
||||
update();
|
||||
});
|
||||
prepareGeometryChange();
|
||||
update();
|
||||
});
|
||||
}
|
||||
|
||||
QRectF PileZone::boundingRect() const
|
||||
|
|
@ -39,7 +42,8 @@ QRectF PileZone::boundingRect() const
|
|||
QPainterPath PileZone::shape() const
|
||||
{
|
||||
QPainterPath shape;
|
||||
qreal cardCornerRadius = SettingsCache::instance().getRoundCardCorners() ? 0.05 * CardDimensions::WIDTH_F : 0.0;
|
||||
qreal cardCornerRadius =
|
||||
SettingsCache::instance().cardsDisplay().getRoundCardCorners() ? 0.05 * CardDimensions::WIDTH_F : 0.0;
|
||||
shape.addRoundedRect(boundingRect(), cardCornerRadius, cardCornerRadius);
|
||||
return shape;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@
|
|||
#include <QGraphicsRectItem>
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
#include <QtMath>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
|
||||
static qreal stackingOffset(qreal cardHeight)
|
||||
{
|
||||
const qreal overlapPercent = SettingsCache::instance().getStackCardOverlapPercent();
|
||||
const qreal overlapPercent = SettingsCache::instance().cardsDisplay().getStackCardOverlapPercent();
|
||||
return cardHeight * (100.0 - overlapPercent) / 100.0;
|
||||
}
|
||||
|
||||
|
|
@ -21,40 +22,32 @@ SelectZone::ZoneLayout SelectZone::computeZoneLayout(const StackLayoutParams &pa
|
|||
}
|
||||
qreal effectiveOffset = params.desiredOffset;
|
||||
if (params.cardCount > 1) {
|
||||
qreal fitOffset;
|
||||
if (params.totalHeight < params.cardHeight && params.minOffset > 0.0) {
|
||||
// Zone is shorter than a card (e.g. minimized). Compress offsets so
|
||||
// every card has at least minOffset pixels of its top visible.
|
||||
fitOffset = (params.totalHeight - params.minOffset) / (params.cardCount - 1);
|
||||
effectiveOffset = qMax(0.0, qMin(params.desiredOffset, fitOffset));
|
||||
qreal reservedForBottomCard;
|
||||
if (params.allowBottomOverflow) {
|
||||
// Allow the bottom card to partially overflow in tight zones, scaling the
|
||||
// overflow allowance by sqrt(cardCount-1) so offsets decrease smoothly
|
||||
// as cards are added rather than dropping by 1/(n-1) each time.
|
||||
// The 0.75 ratio was tuned experimentally to balance card visibility vs. overflow.
|
||||
constexpr qreal bottomCardZoneRatio = 0.75;
|
||||
const qreal adjustedRatio = bottomCardZoneRatio / qSqrt(static_cast<qreal>(params.cardCount - 1));
|
||||
reservedForBottomCard = qMin(params.cardHeight, params.totalHeight * adjustedRatio);
|
||||
} else {
|
||||
qreal reservedForBottomCard;
|
||||
if (params.allowBottomOverflow) {
|
||||
// Allow the bottom card to partially overflow in tight zones, scaling the
|
||||
// overflow allowance by sqrt(cardCount-1) so offsets decrease smoothly
|
||||
// as cards are added rather than dropping by 1/(n-1) each time.
|
||||
// The 0.75 ratio was tuned experimentally to balance card visibility vs. overflow.
|
||||
constexpr qreal bottomCardZoneRatio = 0.75;
|
||||
const qreal adjustedRatio = bottomCardZoneRatio / qSqrt(static_cast<qreal>(params.cardCount - 1));
|
||||
reservedForBottomCard = qMin(params.cardHeight, params.totalHeight * adjustedRatio);
|
||||
} else {
|
||||
// No overflow: reserve full card height for the bottom card
|
||||
reservedForBottomCard = params.cardHeight;
|
||||
}
|
||||
fitOffset = (params.totalHeight - reservedForBottomCard) / (params.cardCount - 1);
|
||||
// No overflow: reserve full card height for the bottom card
|
||||
reservedForBottomCard = params.cardHeight;
|
||||
}
|
||||
qreal fitOffset = (params.totalHeight - reservedForBottomCard) / (params.cardCount - 1);
|
||||
|
||||
if (!params.allowBottomOverflow) {
|
||||
// Constrain offset so all card tops remain within zone bounds.
|
||||
// With start=0, last card top at (cardCount-1) * effectiveOffset must be < totalHeight.
|
||||
qreal maxOffsetForTops = params.totalHeight / (params.cardCount - 1);
|
||||
fitOffset = qMin(fitOffset, maxOffsetForTops);
|
||||
}
|
||||
if (!params.allowBottomOverflow) {
|
||||
// Constrain offset so all card tops remain within zone bounds.
|
||||
// With start=0, last card top at (cardCount-1) * effectiveOffset must be < totalHeight.
|
||||
qreal maxOffsetForTops = params.totalHeight / (params.cardCount - 1);
|
||||
fitOffset = qMin(fitOffset, maxOffsetForTops);
|
||||
}
|
||||
|
||||
// Apply minOffset only if it fits; otherwise compress further to keep all card tops visible.
|
||||
effectiveOffset = qMin(params.desiredOffset, fitOffset);
|
||||
if (fitOffset >= params.minOffset) {
|
||||
effectiveOffset = qMax(params.minOffset, effectiveOffset);
|
||||
}
|
||||
// Apply minOffset only if it fits; otherwise compress further to keep all card tops visible.
|
||||
effectiveOffset = qMin(params.desiredOffset, fitOffset);
|
||||
if (fitOffset >= params.minOffset) {
|
||||
effectiveOffset = qMax(params.minOffset, effectiveOffset);
|
||||
}
|
||||
}
|
||||
qreal stackHeight = (params.cardCount - 1) * effectiveOffset + params.cardHeight;
|
||||
|
|
|
|||
|
|
@ -31,8 +31,22 @@ QRectF StackZone::boundingRect() const
|
|||
|
||||
void StackZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
|
||||
{
|
||||
QBrush brush = themeManager->getExtraBgBrush(ThemeManager::Stack, getLogic()->getPlayer()->getZoneId());
|
||||
painter->fillRect(boundingRect(), brush);
|
||||
if (playmatActive) {
|
||||
// Subtle overlay to distinguish stack zone from table zone (slightly darker)
|
||||
painter->fillRect(boundingRect(), QColor(0, 0, 0, 80));
|
||||
} else {
|
||||
QBrush brush = themeManager->getExtraBgBrush(ThemeManager::Stack, getLogic()->getPlayer()->getZoneId());
|
||||
painter->fillRect(boundingRect(), brush);
|
||||
}
|
||||
}
|
||||
|
||||
void StackZone::onPlaymatChanged(bool active)
|
||||
{
|
||||
playmatActive = active;
|
||||
// See TableZone::onPlaymatChanged for the rationale. Translucent overlay
|
||||
// over a dynamic playmat should not be held in the device cache.
|
||||
setCacheMode(active ? QGraphicsItem::NoCache : QGraphicsItem::DeviceCoordinateCache);
|
||||
update();
|
||||
}
|
||||
|
||||
void StackZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
|
||||
|
|
@ -43,12 +57,18 @@ void StackZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
|
|||
return;
|
||||
}
|
||||
|
||||
int index = calcDropIndexFromY(dropPoint.y(), MIN_CARD_VISIBLE);
|
||||
|
||||
// Same-zone no-op: don't move a card onto itself
|
||||
const auto &cards = getLogic()->getCards();
|
||||
if (!cards.isEmpty() && startZone == getLogic() && cards.at(index)->getId() == dragItems.at(0)->getId()) {
|
||||
return;
|
||||
int index;
|
||||
if (startZone == getLogic()) {
|
||||
// Reordering within the zone: use drop position
|
||||
index = calcDropIndexFromY(dropPoint.y(), MIN_CARD_VISIBLE);
|
||||
// Same-zone no-op: don't move a card onto itself
|
||||
if (!cards.isEmpty() && cards.at(index)->getId() == dragItems.at(0)->getId()) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Coming from another zone: append at end (top of stack, rendered on top)
|
||||
index = static_cast<int>(cards.size());
|
||||
}
|
||||
|
||||
Command_MoveCard cmd;
|
||||
|
|
|
|||
|
|
@ -15,9 +15,13 @@ class StackZone : public SelectZone
|
|||
Q_OBJECT
|
||||
private:
|
||||
qreal zoneHeight;
|
||||
bool playmatActive = false;
|
||||
private slots:
|
||||
void updateBg();
|
||||
|
||||
public slots:
|
||||
void onPlaymatChanged(bool active);
|
||||
|
||||
public:
|
||||
StackZone(StackZoneLogic *_logic, int _zoneHeight, QGraphicsItem *parent);
|
||||
/** @brief Resizes the stack zone height, e.g. when sharing vertical space with the command zone. */
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
#include "../board/arrow_item.h"
|
||||
#include "../board/card_drag_item.h"
|
||||
#include "../board/card_item.h"
|
||||
#include "../game_scene.h"
|
||||
#include "../z_values.h"
|
||||
|
||||
#include <QGraphicsScene>
|
||||
|
|
@ -15,6 +16,7 @@
|
|||
#include <libcockatrice/card/card_info.h>
|
||||
#include <libcockatrice/protocol/pb/command_move_card.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_card_attr.pb.h>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
#include <libcockatrice/utility/zone_names.h>
|
||||
|
||||
const QColor TableZone::BACKGROUND_COLOR = QColor(100, 100, 100);
|
||||
|
|
@ -28,7 +30,7 @@ TableZone::TableZone(TableZoneLogic *_logic, bool _mirrored, QGraphicsItem *pare
|
|||
connect(_logic, &TableZoneLogic::contentSizeChanged, this, &TableZone::resizeToContents);
|
||||
connect(_logic, &TableZoneLogic::toggleTapped, this, &TableZone::toggleTapped);
|
||||
connect(themeManager, &ThemeManager::themeChanged, this, &TableZone::updateBg);
|
||||
connect(&SettingsCache::instance(), &SettingsCache::invertVerticalCoordinateChanged, this,
|
||||
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::invertVerticalCoordinateChanged, this,
|
||||
&TableZone::reorganizeCards);
|
||||
|
||||
updateBg();
|
||||
|
|
@ -46,6 +48,31 @@ void TableZone::updateBg()
|
|||
update();
|
||||
}
|
||||
|
||||
void TableZone::triggerDamageShimmer()
|
||||
{
|
||||
if (!SettingsCache::instance().userInterface().getBattlefieldFlashEnabled()) {
|
||||
damageShimmerAlpha = 0.0;
|
||||
return;
|
||||
}
|
||||
|
||||
damageShimmerAlpha = 1.0;
|
||||
shimmerClock.start();
|
||||
if (scene()) {
|
||||
static_cast<GameScene *>(scene())->registerAnimationItem(this);
|
||||
}
|
||||
}
|
||||
|
||||
bool TableZone::animationEvent()
|
||||
{
|
||||
damageShimmerAlpha = 1.0 - shimmerClock.elapsed() / shimmerDurationMs;
|
||||
if (damageShimmerAlpha <= 0.0) {
|
||||
damageShimmerAlpha = 0.0;
|
||||
return false;
|
||||
}
|
||||
update();
|
||||
return true;
|
||||
}
|
||||
|
||||
QRectF TableZone::boundingRect() const
|
||||
{
|
||||
return QRectF(0, 0, width, height);
|
||||
|
|
@ -59,26 +86,49 @@ void TableZone::setMirrored(bool isMirrored)
|
|||
|
||||
bool TableZone::isInverted() const
|
||||
{
|
||||
return ((mirrored && !SettingsCache::instance().getInvertVerticalCoordinate()) ||
|
||||
(!mirrored && SettingsCache::instance().getInvertVerticalCoordinate()));
|
||||
return ((mirrored && !SettingsCache::instance().userInterface().getInvertVerticalCoordinate()) ||
|
||||
(!mirrored && SettingsCache::instance().userInterface().getInvertVerticalCoordinate()));
|
||||
}
|
||||
|
||||
void TableZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
|
||||
{
|
||||
QBrush brush = themeManager->getExtraBgBrush(ThemeManager::Table, getLogic()->getPlayer()->getZoneId());
|
||||
painter->fillRect(boundingRect(), brush);
|
||||
if (playmatActive) {
|
||||
// Subtle overlay to distinguish table zone from stack zone
|
||||
painter->fillRect(boundingRect(), QColor(0, 0, 0, 60));
|
||||
} else {
|
||||
QBrush brush = themeManager->getExtraBgBrush(ThemeManager::Table, getLogic()->getPlayer()->getZoneId());
|
||||
painter->fillRect(boundingRect(), brush);
|
||||
}
|
||||
|
||||
if (active) {
|
||||
paintZoneOutline(painter);
|
||||
} else {
|
||||
// inactive player gets a darker table zone with a semi transparent black mask
|
||||
// this means if the user provides a custom background it will fade
|
||||
// this means if the user provides a custom background or playmat it will fade
|
||||
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);
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
|
|
|
|||
|
|
@ -8,16 +8,19 @@
|
|||
#define TABLEZONE_H
|
||||
|
||||
#include "../../game/zones/table_zone_logic.h"
|
||||
#include "../animated_item.h"
|
||||
#include "../board/abstract_card_item.h"
|
||||
#include "select_zone.h"
|
||||
|
||||
#include <QElapsedTimer>
|
||||
|
||||
/**
|
||||
* @brief TableZone is the grid based rect where CardItems may be placed.
|
||||
*
|
||||
* It is the main play zone and can be customized with background images.
|
||||
*/
|
||||
//! \todo Refactor methods to make more readable, extract logic to private methods (especially reorganizeCards()).
|
||||
class TableZone : public SelectZone
|
||||
class TableZone : public SelectZone, public IAnimatedItem
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
|
|
@ -83,6 +86,7 @@ private:
|
|||
*/
|
||||
bool active = false;
|
||||
bool mirrored = false;
|
||||
bool playmatActive = false;
|
||||
|
||||
[[nodiscard]] bool isInverted() const;
|
||||
|
||||
|
|
@ -92,6 +96,9 @@ private slots:
|
|||
*/
|
||||
void updateBg();
|
||||
|
||||
public slots:
|
||||
void onPlaymatChanged(bool active);
|
||||
|
||||
public slots:
|
||||
/**
|
||||
Reorganizes CardItems in the TableZone
|
||||
|
|
@ -121,6 +128,16 @@ public:
|
|||
*/
|
||||
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
|
||||
|
||||
/**
|
||||
Flashes the table surface after a player loses life.
|
||||
|
||||
Wired up through the life counter so the battlefield glows when life drops.
|
||||
*/
|
||||
void triggerDamageShimmer();
|
||||
|
||||
/** @brief Decays the damage shimmer by one timer tick. */
|
||||
bool animationEvent() override;
|
||||
|
||||
/**
|
||||
Toggles the selected items as tapped.
|
||||
*/
|
||||
|
|
@ -171,8 +188,17 @@ public:
|
|||
}
|
||||
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();
|
||||
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
|
||||
{
|
||||
|
|
@ -185,6 +211,11 @@ public:
|
|||
}
|
||||
|
||||
private:
|
||||
static constexpr qreal shimmerDurationMs = 450.0;
|
||||
|
||||
QElapsedTimer shimmerClock;
|
||||
qreal damageShimmerAlpha = 0.0;
|
||||
|
||||
void paintZoneOutline(QPainter *painter);
|
||||
void paintLandDivider(QPainter *painter);
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
#include <QStyle>
|
||||
#include <QStyleOption>
|
||||
#include <libcockatrice/protocol/pb/command_shuffle.pb.h>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
|
@ -65,7 +66,7 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player,
|
|||
|
||||
connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(&searchEdit); });
|
||||
|
||||
if (SettingsCache::instance().getFocusCardViewSearchBar()) {
|
||||
if (SettingsCache::instance().userInterface().getFocusCardViewSearchBar()) {
|
||||
this->setActive(true);
|
||||
searchEdit.setFocus();
|
||||
}
|
||||
|
|
@ -76,9 +77,9 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player,
|
|||
vbox->addItem(searchEditProxy);
|
||||
|
||||
// hide search bar if chat autofocus setting is enabled, since typing into it will no longer work anyway
|
||||
searchEditProxy->setVisible(!SettingsCache::instance().getKeepGameChatFocus());
|
||||
connect(&SettingsCache::instance(), &SettingsCache::keepGameChatFocusChanged, searchEditProxy,
|
||||
[searchEditProxy](bool keepFocus) { searchEditProxy->setVisible(!keepFocus); });
|
||||
searchEditProxy->setVisible(!SettingsCache::instance().userInterface().getKeepGameChatFocus());
|
||||
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::keepGameChatFocusChanged,
|
||||
searchEditProxy, [searchEditProxy](bool keepFocus) { searchEditProxy->setVisible(!keepFocus); });
|
||||
|
||||
// top row
|
||||
QGraphicsLinearLayout *hTopRow = new QGraphicsLinearLayout(Qt::Horizontal);
|
||||
|
|
@ -158,9 +159,9 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player,
|
|||
connect(&sortBySelector, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this,
|
||||
&ZoneViewWidget::processSortBy);
|
||||
connect(&pileViewCheckBox, &QCheckBox::QT_STATE_CHANGED, this, &ZoneViewWidget::processSetPileView);
|
||||
groupBySelector.setCurrentIndex(SettingsCache::instance().getZoneViewGroupByIndex());
|
||||
sortBySelector.setCurrentIndex(SettingsCache::instance().getZoneViewSortByIndex());
|
||||
pileViewCheckBox.setChecked(SettingsCache::instance().getZoneViewPileView());
|
||||
groupBySelector.setCurrentIndex(SettingsCache::instance().userInterface().getZoneViewGroupByIndex());
|
||||
sortBySelector.setCurrentIndex(SettingsCache::instance().userInterface().getZoneViewSortByIndex());
|
||||
pileViewCheckBox.setChecked(SettingsCache::instance().userInterface().getZoneViewPileView());
|
||||
|
||||
if (CardList::NoSort == static_cast<CardList::SortOption>(groupBySelector.currentData().toInt())) {
|
||||
pileViewCheckBox.setEnabled(false);
|
||||
|
|
@ -190,7 +191,7 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player,
|
|||
void ZoneViewWidget::processGroupBy(int index)
|
||||
{
|
||||
auto option = static_cast<CardList::SortOption>(groupBySelector.itemData(index).toInt());
|
||||
SettingsCache::instance().setZoneViewGroupByIndex(index);
|
||||
SettingsCache::instance().userInterface().setZoneViewGroupByIndex(index);
|
||||
zone->setGroupBy(option);
|
||||
|
||||
// disable pile view checkbox if we're not grouping by anything
|
||||
|
|
@ -214,13 +215,13 @@ void ZoneViewWidget::processSortBy(int index)
|
|||
return;
|
||||
}
|
||||
|
||||
SettingsCache::instance().setZoneViewSortByIndex(index);
|
||||
SettingsCache::instance().userInterface().setZoneViewSortByIndex(index);
|
||||
zone->setSortBy(option);
|
||||
}
|
||||
|
||||
void ZoneViewWidget::processSetPileView(QT_STATE_CHANGED_T value)
|
||||
{
|
||||
SettingsCache::instance().setZoneViewPileView(value);
|
||||
SettingsCache::instance().userInterface().setZoneViewPileView(value);
|
||||
zone->setPileView(value);
|
||||
}
|
||||
|
||||
|
|
@ -477,7 +478,7 @@ static qreal rowsToHeight(int rows)
|
|||
**/
|
||||
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()
|
||||
{
|
||||
qreal maxInitialHeight = calcMaxInitialHeight();
|
||||
qreal maxExpandedHeight = rowsToHeight(SettingsCache::instance().getCardViewExpandedRowsMax());
|
||||
qreal maxExpandedHeight = rowsToHeight(SettingsCache::instance().userInterface().getCardViewExpandedRowsMax());
|
||||
qreal height = rect().height() - extraHeight - 10;
|
||||
qreal maxHeight = maximumHeight() - extraHeight - 10;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
#include "card_picture_loader.h"
|
||||
|
||||
#include "../../client/settings/cache_settings.h"
|
||||
#include "card_picture_loader_cache_method.h"
|
||||
#include "card_picture_loader_local_schemes.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QBuffer>
|
||||
|
|
@ -16,16 +18,23 @@
|
|||
#include <QStatusBar>
|
||||
#include <QThread>
|
||||
#include <algorithm>
|
||||
#include <libcockatrice/settings/cache_storage_settings.h>
|
||||
#include <libcockatrice/settings/download_settings.h>
|
||||
#include <libcockatrice/settings/paths_settings.h>
|
||||
#include <utility>
|
||||
|
||||
// never cache more than 300 cards at once for a single deck
|
||||
#define CACHED_CARD_PER_DECK_MAX 300
|
||||
|
||||
// wait at least this long before retrying a card whose picture failed to load
|
||||
static constexpr int RETRY_FAILED_CARDS_SECS = 300;
|
||||
|
||||
CardPictureLoader::CardPictureLoader() : QObject(nullptr)
|
||||
{
|
||||
worker = new CardPictureLoaderWorker;
|
||||
connect(&SettingsCache::instance(), &SettingsCache::picsPathChanged, this, &CardPictureLoader::picsPathChanged);
|
||||
connect(&SettingsCache::instance(), &SettingsCache::picDownloadChanged, this,
|
||||
connect(&SettingsCache::instance().paths(), &PathsSettings::picsPathChanged, this,
|
||||
&CardPictureLoader::picsPathChanged);
|
||||
connect(&SettingsCache::instance().downloads(), &DownloadSettings::picDownloadChanged, this,
|
||||
&CardPictureLoader::picDownloadChanged);
|
||||
|
||||
qRegisterMetaType<ExactCard>();
|
||||
|
|
@ -129,7 +138,14 @@ void CardPictureLoader::getPixmap(QPixmap &pixmap, const ExactCard &card, QSize
|
|||
QPixmap bigPixmap;
|
||||
if (QPixmapCache::find(key, &bigPixmap)) {
|
||||
if (bigPixmap.isNull()) {
|
||||
qCDebug(CardPictureLoaderLog) << "Cached pixmap for key" << key << "is NULL!";
|
||||
getCardBackLoadingFailedPixmap(pixmap, size);
|
||||
QDateTime failedAtTime = getInstance().failedAt.value(key);
|
||||
if (!failedAtTime.isValid() ||
|
||||
failedAtTime.addSecs(RETRY_FAILED_CARDS_SECS) < QDateTime::currentDateTime()) {
|
||||
getInstance().failedAt.remove(key);
|
||||
QPixmapCache::remove(key);
|
||||
getInstance().worker->enqueueImageLoad(card);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -153,8 +169,10 @@ void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image)
|
|||
QPixmap finalPixmap;
|
||||
|
||||
if (image.isNull()) {
|
||||
getInstance().failedAt.insert(card.getPixmapCacheKey(), QDateTime::currentDateTime());
|
||||
qCDebug(CardPictureLoaderLog) << "Caching NULL pixmap for" << card.getName();
|
||||
} else {
|
||||
getInstance().failedAt.remove(card.getPixmapCacheKey());
|
||||
if (card.getInfo().getUiAttributes().upsideDownArt) {
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 9, 0))
|
||||
QImage mirrorImage = image.flipped(Qt::Horizontal | Qt::Vertical);
|
||||
|
|
@ -169,7 +187,8 @@ void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image)
|
|||
|
||||
QPixmapCache::insert(card.getPixmapCacheKey(), finalPixmap);
|
||||
|
||||
if (SettingsCache::instance().getCardPictureLoaderCacheMethod() ==
|
||||
if (static_cast<CardPictureLoaderCacheMethod::CacheMethod>(
|
||||
SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) ==
|
||||
CardPictureLoaderCacheMethod::CacheMethod::FILESYSTEM_CACHE) {
|
||||
saveCardImageToLocalStorage(card, finalPixmap);
|
||||
}
|
||||
|
|
@ -177,8 +196,10 @@ void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image)
|
|||
// imageLoaded should only be reached if the exactCard isn't already in cache.
|
||||
// (plus there's a deduplication mechanism in CardPictureLoaderWorker)
|
||||
// It should be safe to connect the CardInfo here without worrying about redundant connections.
|
||||
connect(card.getCardPtr().data(), &QObject::destroyed, this,
|
||||
[cacheKey = card.getPixmapCacheKey()] { QPixmapCache::remove(cacheKey); });
|
||||
connect(card.getCardPtr().data(), &QObject::destroyed, this, [cacheKey = card.getPixmapCacheKey()] {
|
||||
QPixmapCache::remove(cacheKey);
|
||||
getInstance().failedAt.remove(cacheKey);
|
||||
});
|
||||
|
||||
card.emitPixmapUpdated();
|
||||
}
|
||||
|
|
@ -189,9 +210,9 @@ void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card, const
|
|||
return;
|
||||
}
|
||||
|
||||
const QString picsRoot = SettingsCache::instance().getPicsPath();
|
||||
CardPictureLoaderLocalSchemes::NamingScheme scheme =
|
||||
SettingsCache::instance().getLocalCardImageStorageNamingScheme();
|
||||
const QString picsRoot = SettingsCache::instance().paths().getPicsPath();
|
||||
CardPictureLoaderLocalSchemes::NamingScheme scheme = static_cast<CardPictureLoaderLocalSchemes::NamingScheme>(
|
||||
SettingsCache::instance().cacheStorage().getLocalCardImageStorageNamingScheme());
|
||||
|
||||
QString pattern;
|
||||
|
||||
|
|
@ -306,7 +327,7 @@ void CardPictureLoader::picsPathChanged()
|
|||
|
||||
bool CardPictureLoader::hasCustomArt()
|
||||
{
|
||||
auto picsPath = SettingsCache::instance().getPicsPath();
|
||||
auto picsPath = SettingsCache::instance().paths().getPicsPath();
|
||||
QDirIterator it(picsPath, QDir::Dirs | QDir::NoDotAndDotDot);
|
||||
|
||||
// Check if there is at least one non-directory file in the pics path, other
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
#include "card_picture_loader_status_bar.h"
|
||||
#include "card_picture_loader_worker.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QHash>
|
||||
#include <QLoggingCategory>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(CardPictureLoaderLog, "card_picture_loader");
|
||||
|
|
@ -56,6 +58,7 @@ private:
|
|||
|
||||
CardPictureLoaderWorker *worker; ///< Worker thread for async image loading
|
||||
CardPictureLoaderStatusBar *statusBar; ///< Status bar widget showing load progress
|
||||
QHash<QString, QDateTime> failedAt; ///< Timestamp of the last failed load attempt per pixmap cache key
|
||||
|
||||
public:
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -7,15 +7,16 @@
|
|||
#include <QDirIterator>
|
||||
#include <QMovie>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/settings/paths_settings.h>
|
||||
|
||||
static constexpr int REFRESH_INTERVAL_MS = 10 * 1000;
|
||||
|
||||
CardPictureLoaderLocal::CardPictureLoaderLocal(QObject *parent)
|
||||
: QObject(parent), picsPath(SettingsCache::instance().getPicsPath()),
|
||||
customPicsPath(SettingsCache::instance().getCustomPicsPath())
|
||||
: QObject(parent), picsPath(SettingsCache::instance().paths().getPicsPath()),
|
||||
customPicsPath(SettingsCache::instance().paths().getCustomPicsPath())
|
||||
{
|
||||
// Hook up signals to settings
|
||||
connect(&SettingsCache::instance(), &SettingsCache::picsPathChanged, this,
|
||||
connect(&SettingsCache::instance().paths(), &PathsSettings::picsPathChanged, this,
|
||||
&CardPictureLoaderLocal::picsPathChanged);
|
||||
|
||||
refreshIndex();
|
||||
|
|
@ -127,6 +128,6 @@ QImage CardPictureLoaderLocal::tryLoadCardImageFromDisk(const QString &setName,
|
|||
|
||||
void CardPictureLoaderLocal::picsPathChanged()
|
||||
{
|
||||
picsPath = SettingsCache::instance().getPicsPath();
|
||||
customPicsPath = SettingsCache::instance().getCustomPicsPath();
|
||||
picsPath = SettingsCache::instance().paths().getPicsPath();
|
||||
customPicsPath = SettingsCache::instance().paths().getCustomPicsPath();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "card_picture_loader_worker.h"
|
||||
|
||||
#include "../../client/settings/cache_settings.h"
|
||||
#include "card_picture_loader_cache_method.h"
|
||||
#include "card_picture_loader_local.h"
|
||||
#include "card_picture_loader_worker_work.h"
|
||||
|
||||
|
|
@ -9,13 +10,19 @@
|
|||
#include <QNetworkDiskCache>
|
||||
#include <QNetworkReply>
|
||||
#include <QThread>
|
||||
#include <libcockatrice/settings/cache_storage_settings.h>
|
||||
#include <libcockatrice/settings/download_settings.h>
|
||||
#include <libcockatrice/settings/paths_settings.h>
|
||||
#include <utility>
|
||||
#include <version_string.h>
|
||||
|
||||
static constexpr int MAX_REQUESTS_PER_SEC = 10;
|
||||
static constexpr int MIN_HOST_QUOTA = 1; ///< Floor for the per-host request allowance
|
||||
static constexpr qint64 QUOTA_RECOVER_MS = 60000; ///< Idle time before a reduced quota starts recovering
|
||||
|
||||
CardPictureLoaderWorker::CardPictureLoaderWorker()
|
||||
: QObject(nullptr), picDownload(SettingsCache::instance().getPicDownload()), requestQuota(MAX_REQUESTS_PER_SEC)
|
||||
: QObject(nullptr), picDownload(SettingsCache::instance().downloads().getPicDownload()),
|
||||
requestQuota(MAX_REQUESTS_PER_SEC)
|
||||
{
|
||||
networkManager = new QNetworkAccessManager(this);
|
||||
// We need a timeout to ensure requests don't hang indefinitely in case of
|
||||
|
|
@ -25,13 +32,14 @@ CardPictureLoaderWorker::CardPictureLoaderWorker()
|
|||
cache = new QNetworkDiskCache(this);
|
||||
cache->setCacheDirectory(SettingsCache::instance().getNetworkCachePath());
|
||||
cache->setMaximumCacheSize(1024L * 1024L *
|
||||
static_cast<qint64>(SettingsCache::instance().getNetworkCacheSizeInMB()));
|
||||
static_cast<qint64>(SettingsCache::instance().cacheStorage().getNetworkCacheSizeInMB()));
|
||||
|
||||
connect(&SettingsCache::instance(), &SettingsCache::networkCacheSizeChanged, cache, [this](int newSizeInMB) {
|
||||
if (cache) {
|
||||
cache->setMaximumCacheSize(1024L * 1024L * static_cast<qint64>(newSizeInMB));
|
||||
}
|
||||
});
|
||||
connect(&SettingsCache::instance().cacheStorage(), &CacheStorageSettings::networkCacheSizeChanged, cache,
|
||||
[this](int newSizeInMB) {
|
||||
if (cache) {
|
||||
cache->setMaximumCacheSize(1024L * 1024L * static_cast<qint64>(newSizeInMB));
|
||||
}
|
||||
});
|
||||
|
||||
networkManager->setCache(cache);
|
||||
|
||||
|
|
@ -39,7 +47,7 @@ CardPictureLoaderWorker::CardPictureLoaderWorker()
|
|||
// We can't use NoLessSafeRedirectPolicy because it is not applied with AlwaysCache
|
||||
networkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy);
|
||||
|
||||
cacheFilePath = SettingsCache::instance().getRedirectCachePath() + REDIRECT_CACHE_FILENAME;
|
||||
cacheFilePath = SettingsCache::instance().paths().getRedirectCachePath() + REDIRECT_CACHE_FILENAME;
|
||||
loadRedirectCache();
|
||||
cleanStaleEntries();
|
||||
|
||||
|
|
@ -72,7 +80,8 @@ void CardPictureLoaderWorker::queueRequest(const QUrl &url, CardPictureLoaderWor
|
|||
queueRequest(cachedRedirect, worker);
|
||||
return;
|
||||
}
|
||||
if (SettingsCache::instance().getCardPictureLoaderCacheMethod() ==
|
||||
if (static_cast<CardPictureLoaderCacheMethod::CacheMethod>(
|
||||
SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) ==
|
||||
CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE &&
|
||||
cache->metaData(url).isValid()) {
|
||||
// If we hit a cached url, we get to make the request for free, since it won't contribute towards the
|
||||
|
|
@ -98,8 +107,10 @@ QNetworkReply *CardPictureLoaderWorker::makeRequest(const QUrl &url, CardPicture
|
|||
req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
||||
req.setRawHeader("Accept", "image/avif,image/webp,image/apng,image/,/*;q=0.8");
|
||||
|
||||
bool useNetworkCache = !picDownload && SettingsCache::instance().getCardPictureLoaderCacheMethod() ==
|
||||
CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE;
|
||||
bool useNetworkCache =
|
||||
!picDownload && static_cast<CardPictureLoaderCacheMethod::CacheMethod>(
|
||||
SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) ==
|
||||
CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE;
|
||||
|
||||
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute,
|
||||
useNetworkCache ? QNetworkRequest::AlwaysCache : QNetworkRequest::AlwaysNetwork);
|
||||
|
|
@ -115,6 +126,19 @@ QNetworkReply *CardPictureLoaderWorker::makeRequest(const QUrl &url, CardPicture
|
|||
void CardPictureLoaderWorker::resetRequestQuota()
|
||||
{
|
||||
requestQuota = MAX_REQUESTS_PER_SEC;
|
||||
|
||||
QDateTime now = QDateTime::currentDateTime();
|
||||
for (auto it = hostRequestQuota.begin(); it != hostRequestQuota.end(); ++it) {
|
||||
if (!hostLast429.contains(it.key()) || now.msecsTo(hostLast429.value(it.key())) < -QUOTA_RECOVER_MS) {
|
||||
it.value() = qMin(MAX_REQUESTS_PER_SEC, it.value() + 1);
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &request : requestLoadQueue) {
|
||||
const QString host = request.first.host();
|
||||
hostQuotaRemaining.insert(host, hostRequestQuota.value(host, MAX_REQUESTS_PER_SEC));
|
||||
}
|
||||
|
||||
processQueuedRequests();
|
||||
}
|
||||
|
||||
|
|
@ -127,14 +151,26 @@ void CardPictureLoaderWorker::processQueuedRequests()
|
|||
|
||||
bool CardPictureLoaderWorker::processSingleRequest()
|
||||
{
|
||||
if (!requestLoadQueue.isEmpty()) {
|
||||
auto request = requestLoadQueue.takeFirst();
|
||||
makeRequest(request.first, request.second);
|
||||
return true;
|
||||
for (int i = 0; i < requestLoadQueue.size(); ++i) {
|
||||
const auto &request = requestLoadQueue.at(i);
|
||||
QString host = request.first.host();
|
||||
int allowance = hostQuotaRemaining.value(host, MAX_REQUESTS_PER_SEC);
|
||||
if (allowance > 0) {
|
||||
hostQuotaRemaining.insert(host, allowance - 1);
|
||||
makeRequest(request.first, request.second);
|
||||
requestLoadQueue.removeAt(i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void CardPictureLoaderWorker::onHostRateLimited(const QString &host)
|
||||
{
|
||||
hostRequestQuota.insert(host, qMax(MIN_HOST_QUOTA, hostRequestQuota.value(host, MAX_REQUESTS_PER_SEC) / 2));
|
||||
hostLast429.insert(host, QDateTime::currentDateTime());
|
||||
}
|
||||
|
||||
void CardPictureLoaderWorker::enqueueImageLoad(const ExactCard &card)
|
||||
{
|
||||
// Send call through a connection to ensure the handling is run on the pictureLoader thread
|
||||
|
|
@ -229,7 +265,7 @@ void CardPictureLoaderWorker::cleanStaleEntries()
|
|||
|
||||
auto it = redirectCache.begin();
|
||||
while (it != redirectCache.end()) {
|
||||
if (it.value().second.addDays(SettingsCache::instance().getRedirectCacheTtl()) < now) {
|
||||
if (it.value().second.addDays(SettingsCache::instance().cacheStorage().getRedirectCacheTtl()) < now) {
|
||||
it = redirectCache.erase(it); // Remove stale entry
|
||||
} else {
|
||||
++it;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@
|
|||
#include "card_picture_loader_worker_work.h"
|
||||
#include "card_picture_to_load.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QHash>
|
||||
#include <QLoggingCategory>
|
||||
#include <QMutex>
|
||||
#include <QNetworkAccessManager>
|
||||
|
|
@ -66,6 +68,12 @@ public:
|
|||
*/
|
||||
void queueRequest(const QUrl &url, CardPictureLoaderWorkerWork *worker);
|
||||
|
||||
/**
|
||||
* @brief Handles a server returning HTTP 429 by reducing that host's request quota.
|
||||
* @param host The host that returned 429
|
||||
*/
|
||||
void onHostRateLimited(const QString &host);
|
||||
|
||||
/** @brief Clears the network cache and redirect cache. */
|
||||
void clearNetworkCache();
|
||||
|
||||
|
|
@ -110,8 +118,11 @@ private:
|
|||
bool picDownload; ///< Whether downloading images from network is enabled
|
||||
QQueue<QPair<QUrl, CardPictureLoaderWorkerWork *>> requestLoadQueue; ///< Queue of pending network requests
|
||||
|
||||
int requestQuota; ///< Remaining requests allowed per second
|
||||
QTimer requestTimer; ///< Timer to reset the request quota
|
||||
int requestQuota; ///< Remaining requests allowed per second
|
||||
QTimer requestTimer; ///< Timer to reset the request quota
|
||||
QHash<QString, int> hostRequestQuota; ///< Sustained per-host request allowance
|
||||
QHash<QString, int> hostQuotaRemaining; ///< Per-host allowance left in the current second
|
||||
QHash<QString, QDateTime> hostLast429; ///< When each host was last rate limited
|
||||
|
||||
CardPictureLoaderLocal *localLoader; ///< Loader for local images
|
||||
QSet<QString> currentlyLoading; ///< Deduplication: contains pixmapCacheKey currently being loaded
|
||||
|
|
|
|||
|
|
@ -8,8 +8,13 @@
|
|||
#include <QLoggingCategory>
|
||||
#include <QMovie>
|
||||
#include <QNetworkReply>
|
||||
#include <QRandomGenerator>
|
||||
#include <QThread>
|
||||
#include <QThreadPool>
|
||||
#include <QTimer>
|
||||
|
||||
ServerRateLimiter CardPictureLoaderWorkerWork::s_rateLimiter;
|
||||
#include <libcockatrice/settings/download_settings.h>
|
||||
|
||||
// Card back returned by gatherer when card is not found
|
||||
static const QStringList MD5_BLACKLIST = {
|
||||
|
|
@ -19,7 +24,7 @@ static const QStringList MD5_BLACKLIST = {
|
|||
|
||||
CardPictureLoaderWorkerWork::CardPictureLoaderWorkerWork(const CardPictureLoaderWorker *worker, const ExactCard &toLoad)
|
||||
: QObject(nullptr), cardToDownload(CardPictureToLoad(toLoad)),
|
||||
picDownload(SettingsCache::instance().getPicDownload())
|
||||
picDownload(SettingsCache::instance().downloads().getPicDownload())
|
||||
{
|
||||
// Hook up signals to the orchestrator
|
||||
connect(this, &CardPictureLoaderWorkerWork::requestImageDownload, worker, &CardPictureLoaderWorker::queueRequest);
|
||||
|
|
@ -29,19 +34,48 @@ CardPictureLoaderWorkerWork::CardPictureLoaderWorkerWork(const CardPictureLoader
|
|||
connect(this, &CardPictureLoaderWorkerWork::imageLoaded, worker, &CardPictureLoaderWorker::handleImageLoaded);
|
||||
connect(this, &CardPictureLoaderWorkerWork::requestSucceeded, worker,
|
||||
&CardPictureLoaderWorker::imageRequestSucceeded);
|
||||
connect(this, &CardPictureLoaderWorkerWork::rateLimited, worker, &CardPictureLoaderWorker::onHostRateLimited);
|
||||
|
||||
// Hook up signals to settings
|
||||
connect(&SettingsCache::instance(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged()));
|
||||
connect(&SettingsCache::instance().downloads(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged()));
|
||||
|
||||
startNextPicDownload();
|
||||
}
|
||||
|
||||
void CardPictureLoaderWorkerWork::startNextPicDownload()
|
||||
{
|
||||
QDateTime now = QDateTime::currentDateTime();
|
||||
while (!cardToDownload.getCurrentUrl().isEmpty() &&
|
||||
s_rateLimiter.isRateLimited(QUrl(cardToDownload.getCurrentUrl()).host(), now)) {
|
||||
QString host = QUrl(cardToDownload.getCurrentUrl()).host();
|
||||
if (s_rateLimiter.rounds(host) == 1) {
|
||||
// First 429 round for this server: wait out the backoff and give it
|
||||
// one more chance instead of immediately falling through to a worse
|
||||
// source. A second 429 makes us fall through instead.
|
||||
qCDebug(CardPictureLoaderWorkerWorkLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardToDownload.getCard().getInfo().getCorrectedName()
|
||||
<< " set: " << cardToDownload.getSetName() << "]: Waiting out backoff for " << host << " to retry "
|
||||
<< cardToDownload.getCurrentUrl();
|
||||
scheduleDeferredRetry();
|
||||
return;
|
||||
}
|
||||
|
||||
// The server has already 429'd us at least twice, so further retries are
|
||||
// unlikely to succeed: move on to the other configured sources.
|
||||
qCDebug(CardPictureLoaderWorkerWorkLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardToDownload.getCard().getInfo().getCorrectedName()
|
||||
<< " set: " << cardToDownload.getSetName() << "]: Skipping rate-limited URL "
|
||||
<< cardToDownload.getCurrentUrl() << " (server " << host << " still rate limiting)";
|
||||
if (!cardToDownload.nextUrl() && !cardToDownload.nextSet()) {
|
||||
scheduleDeferredRetry();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
QString picUrl = cardToDownload.getCurrentUrl();
|
||||
|
||||
if (picUrl.isEmpty()) {
|
||||
picDownloadFailed();
|
||||
scheduleDeferredRetry();
|
||||
} else {
|
||||
QUrl url(picUrl);
|
||||
qCDebug(CardPictureLoaderWorkerWorkLog).nospace()
|
||||
|
|
@ -107,7 +141,41 @@ static bool imageIsBlackListed(const QByteArray &picData)
|
|||
void CardPictureLoaderWorkerWork::handleFailedReply(const QNetworkReply *reply)
|
||||
{
|
||||
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 429) {
|
||||
qCWarning(CardPictureLoaderWorkerWorkLog) << "Too many requests.";
|
||||
QString host = reply->url().host();
|
||||
QDateTime now = QDateTime::currentDateTime();
|
||||
|
||||
qint64 retryAfterMs = 0;
|
||||
const QByteArray retryAfterHeader = reply->rawHeader("Retry-After");
|
||||
if (!retryAfterHeader.isEmpty()) {
|
||||
bool ok = false;
|
||||
int seconds = retryAfterHeader.toInt(&ok);
|
||||
if (ok && seconds > 0) {
|
||||
retryAfterMs = static_cast<qint64>(seconds) * 1000;
|
||||
} else {
|
||||
QDateTime retryAfterDate =
|
||||
QDateTime::fromString(QString::fromLatin1(retryAfterHeader), Qt::RFC2822Date);
|
||||
if (retryAfterDate.isValid()) {
|
||||
retryAfterMs = qMax<qint64>(0, now.msecsTo(retryAfterDate));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QDateTime backoffUntil = s_rateLimiter.on429(host, now, retryAfterMs);
|
||||
emit rateLimited(host);
|
||||
|
||||
if (s_rateLimiter.rounds(host) == 1) {
|
||||
qCWarning(CardPictureLoaderWorkerWorkLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardToDownload.getCard().getName()
|
||||
<< " set: " << cardToDownload.getSetName() << "]: Too many requests from " << host
|
||||
<< ", backing off until " << backoffUntil.toString(Qt::ISODate) << ", retrying the same url";
|
||||
scheduleDeferredRetry();
|
||||
} else {
|
||||
qCWarning(CardPictureLoaderWorkerWorkLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardToDownload.getCard().getName()
|
||||
<< " set: " << cardToDownload.getSetName() << "]: Too many requests from " << host
|
||||
<< ", retry already attempted, falling through to other sources";
|
||||
picDownloadFailed();
|
||||
}
|
||||
} else {
|
||||
bool isFromCache = reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool();
|
||||
|
||||
|
|
@ -148,6 +216,9 @@ void CardPictureLoaderWorkerWork::handleSuccessfulReply(QNetworkReply *reply)
|
|||
return;
|
||||
}
|
||||
|
||||
// A non-redirect successful response means the server is not rate limiting us anymore.
|
||||
s_rateLimiter.onSuccess(reply->url().host());
|
||||
|
||||
// peek is used to keep the data in the buffer for use by QImageReader
|
||||
const QByteArray &picData = reply->peek(reply->size());
|
||||
|
||||
|
|
@ -202,6 +273,42 @@ QImage CardPictureLoaderWorkerWork::tryLoadImageFromReply(QNetworkReply *reply)
|
|||
return imgReader.read();
|
||||
}
|
||||
|
||||
void CardPictureLoaderWorkerWork::scheduleDeferredRetry()
|
||||
{
|
||||
QDateTime now = QDateTime::currentDateTime();
|
||||
|
||||
// Prefer waiting on the current URL's server so we retry the same source.
|
||||
QString currentHost = QUrl(cardToDownload.getCurrentUrl()).host();
|
||||
QDateTime backoffUntil = s_rateLimiter.deadline(currentHost);
|
||||
if (!s_rateLimiter.isRateLimited(currentHost, now)) {
|
||||
backoffUntil = s_rateLimiter.earliestDeadline(now);
|
||||
}
|
||||
|
||||
if (!backoffUntil.isValid()) {
|
||||
qCWarning(CardPictureLoaderWorkerWorkLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardToDownload.getCard().getInfo().getCorrectedName()
|
||||
<< " set: " << cardToDownload.getSetName() << "]: All URLs exhausted, no servers in backoff: BAILING OUT";
|
||||
concludeImageLoad(QImage());
|
||||
return;
|
||||
}
|
||||
|
||||
qint64 waitMs = qMax<qint64>(0, now.msecsTo(backoffUntil));
|
||||
// Add some jitter to desynchronize concurrent retries and avoid a thundering herd.
|
||||
waitMs += QRandomGenerator::global()->bounded(5000);
|
||||
|
||||
qCDebug(CardPictureLoaderWorkerWorkLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardToDownload.getCard().getInfo().getCorrectedName()
|
||||
<< " set: " << cardToDownload.getSetName() << "]: All URLs exhausted, scheduling deferred retry in " << waitMs
|
||||
<< "ms";
|
||||
|
||||
QTimer::singleShot(waitMs, this, [this] {
|
||||
s_rateLimiter.clearExpired(QDateTime::currentDateTime());
|
||||
|
||||
cardToDownload.resetIndices();
|
||||
startNextPicDownload();
|
||||
});
|
||||
}
|
||||
|
||||
void CardPictureLoaderWorkerWork::concludeImageLoad(const QImage &image)
|
||||
{
|
||||
emit imageLoaded(cardToDownload.getCard(), image);
|
||||
|
|
@ -210,5 +317,5 @@ void CardPictureLoaderWorkerWork::concludeImageLoad(const QImage &image)
|
|||
|
||||
void CardPictureLoaderWorkerWork::picDownloadChanged()
|
||||
{
|
||||
picDownload = SettingsCache::instance().getPicDownload();
|
||||
picDownload = SettingsCache::instance().downloads().getPicDownload();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,15 @@
|
|||
#include "card_picture_loader_worker.h"
|
||||
#include "card_picture_to_load.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QLoggingCategory>
|
||||
#include <QMutex>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QObject>
|
||||
#include <QRandomGenerator>
|
||||
#include <QThread>
|
||||
#include <libcockatrice/card/database/card_database.h>
|
||||
#include <libcockatrice/utility/server_rate_limiter.h>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(CardPictureLoaderWorkerWorkLog, "card_picture_loader.worker");
|
||||
|
||||
|
|
@ -50,6 +53,8 @@ public slots:
|
|||
private:
|
||||
bool picDownload; ///< Whether network downloading is enabled
|
||||
|
||||
static ServerRateLimiter s_rateLimiter; ///< Shared per-server 429 backoff state
|
||||
|
||||
/** @brief Starts downloading the next URL for this card. */
|
||||
void startNextPicDownload();
|
||||
|
||||
|
|
@ -77,6 +82,16 @@ private:
|
|||
*/
|
||||
void concludeImageLoad(const QImage &image);
|
||||
|
||||
/**
|
||||
* @brief Schedules a deferred retry after the relevant server backoff expires.
|
||||
*
|
||||
* Waits on the current URL's server when it is the reason we are blocked,
|
||||
* otherwise on the earliest active backoff. If no servers are in backoff,
|
||||
* concludes with failure. Otherwise resets the CardPictureToLoad indices and
|
||||
* retries after the backoff period.
|
||||
*/
|
||||
void scheduleDeferredRetry();
|
||||
|
||||
private slots:
|
||||
/** @brief Updates the picDownload setting when it changes. */
|
||||
void picDownloadChanged();
|
||||
|
|
@ -100,6 +115,9 @@ signals:
|
|||
/** @brief Emitted when a URL has been redirected. */
|
||||
void urlRedirected(const QUrl &originalUrl, const QUrl &redirectUrl);
|
||||
|
||||
/** @brief Emitted when a server returned HTTP 429. */
|
||||
void rateLimited(const QString &host);
|
||||
|
||||
/** @brief Emitted when a cached URL is invalid and must be removed. */
|
||||
void cachedUrlInvalidated(const QUrl &url);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,14 +9,17 @@
|
|||
#include <algorithm>
|
||||
#include <libcockatrice/card/set/card_set_comparator.h>
|
||||
#include <libcockatrice/interfaces/noop_card_set_priority_controller.h>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
#include <libcockatrice/settings/download_settings.h>
|
||||
|
||||
CardPictureToLoad::CardPictureToLoad(const ExactCard &_card)
|
||||
: card(_card), urlTemplates(SettingsCache::instance().downloads().getAllURLs())
|
||||
{
|
||||
if (card) {
|
||||
sortedSets = extractSetsSorted(card);
|
||||
// The first time called, nextSet will also populate the Urls for the first set.
|
||||
nextSet();
|
||||
currentSetIndex = 0;
|
||||
currentSet = sortedSets.first();
|
||||
populateSetUrls();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -34,7 +37,7 @@ QList<CardSetPtr> CardPictureToLoad::extractSetsSorted(const ExactCard &card)
|
|||
std::sort(sortedSets.begin(), sortedSets.end(), SetPriorityComparator());
|
||||
|
||||
// If the user hasn't disabled arts other than their personal preference...
|
||||
if (!SettingsCache::instance().getOverrideAllCardArtWithPersonalPreference()) {
|
||||
if (!SettingsCache::instance().cardsDisplay().getOverrideAllCardArtWithPersonalPreference()) {
|
||||
// If the pixmapCacheKey corresponds to a specific set, we have to try to load it first.
|
||||
qsizetype setIndex = sortedSets.indexOf(card.getPrinting().getSet());
|
||||
if (setIndex > 0) { // we don't need to move the set if it's already first
|
||||
|
|
@ -99,15 +102,19 @@ void CardPictureToLoad::populateSetUrls()
|
|||
}
|
||||
}
|
||||
|
||||
/* Call nextUrl to make sure currentUrl is up-to-date
|
||||
but we don't need the result here. */
|
||||
(void)nextUrl();
|
||||
currentUrlIndex = 0;
|
||||
if (!currentSetUrls.isEmpty()) {
|
||||
currentUrl = currentSetUrls.first();
|
||||
} else {
|
||||
currentUrl = QString();
|
||||
}
|
||||
}
|
||||
|
||||
bool CardPictureToLoad::nextSet()
|
||||
{
|
||||
if (!sortedSets.isEmpty()) {
|
||||
currentSet = sortedSets.takeFirst();
|
||||
currentSetIndex++;
|
||||
if (currentSetIndex < sortedSets.size()) {
|
||||
currentSet = sortedSets.at(currentSetIndex);
|
||||
populateSetUrls();
|
||||
return true;
|
||||
}
|
||||
|
|
@ -117,8 +124,9 @@ bool CardPictureToLoad::nextSet()
|
|||
|
||||
bool CardPictureToLoad::nextUrl()
|
||||
{
|
||||
if (!currentSetUrls.isEmpty()) {
|
||||
currentUrl = currentSetUrls.takeFirst();
|
||||
currentUrlIndex++;
|
||||
if (currentUrlIndex < currentSetUrls.size()) {
|
||||
currentUrl = currentSetUrls.at(currentUrlIndex);
|
||||
return true;
|
||||
}
|
||||
currentUrl = QString();
|
||||
|
|
@ -134,6 +142,28 @@ QString CardPictureToLoad::getSetName() const
|
|||
}
|
||||
}
|
||||
|
||||
QString CardPictureToLoad::peekNextUrl() const
|
||||
{
|
||||
int nextIndex = currentUrlIndex + 1;
|
||||
if (nextIndex < currentSetUrls.size()) {
|
||||
return currentSetUrls.at(nextIndex);
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
void CardPictureToLoad::resetIndices()
|
||||
{
|
||||
currentSetIndex = 0;
|
||||
if (!sortedSets.isEmpty()) {
|
||||
currentSet = sortedSets.first();
|
||||
populateSetUrls();
|
||||
} else {
|
||||
currentSet = {};
|
||||
currentSetUrls.clear();
|
||||
currentUrl = QString();
|
||||
}
|
||||
}
|
||||
|
||||
static int parse(const QString &urlTemplate,
|
||||
const QString &propType,
|
||||
const QString &cardName,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ private:
|
|||
QList<QString> currentSetUrls; ///< URLs for the current set being attempted
|
||||
QString currentUrl; ///< Currently active URL to download
|
||||
CardSetPtr currentSet; ///< Currently active set
|
||||
int currentSetIndex = 0; ///< Current position in sortedSets
|
||||
int currentUrlIndex = 0; ///< Current position in currentSetUrls
|
||||
|
||||
public:
|
||||
/**
|
||||
|
|
@ -56,6 +58,9 @@ public:
|
|||
/** @return The short name of the current set, or empty string if no set. */
|
||||
[[nodiscard]] QString getSetName() const;
|
||||
|
||||
/** @return The next URL in the current set's list without advancing, or empty if at end. */
|
||||
[[nodiscard]] QString peekNextUrl() const;
|
||||
|
||||
/**
|
||||
* @brief Transforms a URL template into a concrete URL for this card/set.
|
||||
* @param urlTemplate The URL template to transform
|
||||
|
|
@ -88,6 +93,14 @@ public:
|
|||
*/
|
||||
void populateSetUrls();
|
||||
|
||||
/**
|
||||
* @brief Resets iteration indices to the beginning.
|
||||
*
|
||||
* Restarts URL/set iteration from the first set and first URL.
|
||||
* Used for deferred retry after server backoff expires.
|
||||
*/
|
||||
void resetIndices();
|
||||
|
||||
/**
|
||||
* @brief Extract all sets from the card and sort them by priority.
|
||||
* @param card The card to extract sets from
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
#ifndef COCKATRICE_CONTEXT_CONNECT_TO_SERVER_H
|
||||
#define COCKATRICE_CONTEXT_CONNECT_TO_SERVER_H
|
||||
|
||||
#include <QString>
|
||||
|
||||
struct ContextConnectToServer
|
||||
{
|
||||
QString hostname;
|
||||
QString port;
|
||||
QString username;
|
||||
QString password;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_CONTEXT_CONNECT_TO_SERVER_H
|
||||
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