mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-08 02:53:25 -08:00
Compare commits
2 commits
b7253efefb
...
64f58535dd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64f58535dd | ||
|
|
c3b839a905 |
131 changed files with 797 additions and 4501 deletions
|
|
@ -4,6 +4,7 @@ RUN apt-get update && \
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||||
build-essential \
|
build-essential \
|
||||||
ccache \
|
ccache \
|
||||||
|
clang-format \
|
||||||
cmake \
|
cmake \
|
||||||
file \
|
file \
|
||||||
g++ \
|
g++ \
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ RUN apt-get update && \
|
||||||
build-essential \
|
build-essential \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
ccache \
|
ccache \
|
||||||
|
clang-format \
|
||||||
cmake \
|
cmake \
|
||||||
file \
|
file \
|
||||||
g++ \
|
g++ \
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ RUN apt-get update && \
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||||
build-essential \
|
build-essential \
|
||||||
ccache \
|
ccache \
|
||||||
|
clang-format \
|
||||||
cmake \
|
cmake \
|
||||||
file \
|
file \
|
||||||
g++ \
|
g++ \
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ RUN apt-get update && \
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||||
build-essential \
|
build-essential \
|
||||||
ccache \
|
ccache \
|
||||||
|
clang-format \
|
||||||
cmake \
|
cmake \
|
||||||
file \
|
file \
|
||||||
g++ \
|
g++ \
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,10 @@ RUN apt-get update && \
|
||||||
build-essential \
|
build-essential \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
ccache \
|
ccache \
|
||||||
|
clang-format \
|
||||||
cmake \
|
cmake \
|
||||||
file \
|
file \
|
||||||
g++ \
|
g++ \
|
||||||
gcovr \
|
|
||||||
git \
|
git \
|
||||||
libgl-dev \
|
libgl-dev \
|
||||||
liblzma-dev \
|
liblzma-dev \
|
||||||
|
|
|
||||||
151
.ci/compile.sh
151
.ci/compile.sh
|
|
@ -8,18 +8,14 @@
|
||||||
# --suffix <suffix> renames package with this suffix, requires arg
|
# --suffix <suffix> renames package with this suffix, requires arg
|
||||||
# --server compiles servatrice
|
# --server compiles servatrice
|
||||||
# --test runs tests
|
# --test runs tests
|
||||||
# --sanitize builds with AddressSanitizer and UndefinedBehaviorSanitizer, gcc/clang only
|
|
||||||
# --coverage instruments the test run for line coverage and prints a gcovr summary, gcc only
|
|
||||||
|
|
||||||
# --debug or --release sets the build type ie CMAKE_BUILD_TYPE
|
# --debug or --release sets the build type ie CMAKE_BUILD_TYPE
|
||||||
# --ccache [<size>] uses ccache and shows stats, optionally provide size
|
# --ccache [<size>] uses ccache and shows stats, optionally provide size
|
||||||
# --evict-ccache <age> runs ccache eviction based on given age after build
|
# --evict-ccache <age> runs ccache eviction based on given age after build
|
||||||
# --dir <dir> sets the name of the build dir, default is "build"
|
# --dir <dir> sets the name of the build dir, default is "build"
|
||||||
# --cmake-generator <generator> sets CMAKE_GENERATOR as used by cmake
|
# --cmake-generator <generator> sets CMAKE_GENERATOR as used by cmake
|
||||||
# --target-macos-version <version> sets the min os version - only used for macOS builds
|
# --target-macos-version <version> sets the min os version - only used for macOS builds
|
||||||
# --profile writes an MSBuild performance summary and binary log, Windows only
|
# uses env: BUILDTYPE MAKE_INSTALL MAKE_PACKAGE PACKAGE_TYPE PACKAGE_SUFFIX MAKE_SERVER MAKE_NO_CLIENT MAKE_TEST USE_CCACHE CCACHE_SIZE CCACHE_EVICTION_AGE BUILD_DIR CMAKE_GENERATOR TARGET_MACOS_VERSION
|
||||||
# uses env: BUILDTYPE MAKE_INSTALL MAKE_PACKAGE PACKAGE_TYPE PACKAGE_SUFFIX MAKE_SERVER MAKE_NO_CLIENT MAKE_TEST USE_CCACHE CCACHE_SIZE CCACHE_EVICTION_AGE BUILD_DIR CMAKE_GENERATOR TARGET_MACOS_VERSION MAKE_SANITIZE MAKE_COVERAGE VCPKG_TARGET_MACOS_VERSION BUILD_PARALLEL_LEVEL BUILD_PROFILE
|
# (correspond to args: --debug/--release --install --package <package type> --suffix <suffix> --server --test --ccache <ccache_size> --dir <dir>)
|
||||||
# (correspond to args: --debug/--release --install --package <package type> --suffix <suffix> --server --test --sanitize --coverage --ccache <ccache_size> --dir <dir> --profile)
|
|
||||||
# exitcode: 1 for failure, 3 for invalid arguments
|
# exitcode: 1 for failure, 3 for invalid arguments
|
||||||
|
|
||||||
# Read arguments
|
# Read arguments
|
||||||
|
|
@ -61,14 +57,6 @@ while [[ $# != 0 ]]; do
|
||||||
MAKE_TEST=1
|
MAKE_TEST=1
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
'--sanitize')
|
|
||||||
MAKE_SANITIZE=1
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
'--coverage')
|
|
||||||
MAKE_COVERAGE=1
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
'--debug')
|
'--debug')
|
||||||
BUILDTYPE="Debug"
|
BUILDTYPE="Debug"
|
||||||
shift
|
shift
|
||||||
|
|
@ -116,10 +104,6 @@ while [[ $# != 0 ]]; do
|
||||||
export CMAKE_GENERATOR=$1
|
export CMAKE_GENERATOR=$1
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
'--profile')
|
|
||||||
BUILD_PROFILE=1
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
'--target-macos-version')
|
'--target-macos-version')
|
||||||
shift
|
shift
|
||||||
if [[ $# == 0 ]]; then
|
if [[ $# == 0 ]]; then
|
||||||
|
|
@ -147,15 +131,6 @@ if [[ ! $BUILD_DIR ]]; then
|
||||||
BUILD_DIR="build"
|
BUILD_DIR="build"
|
||||||
fi
|
fi
|
||||||
mkdir -p "$BUILD_DIR"
|
mkdir -p "$BUILD_DIR"
|
||||||
|
|
||||||
if [[ $MAKE_COVERAGE ]]; then
|
|
||||||
# Resolved while the working directory is still the source root. $0 is relative to the
|
|
||||||
# directory the script was invoked from, so after the cd below it no longer resolves, and
|
|
||||||
# under `set -e` the failed subshell would abort the build rather than just yielding an empty
|
|
||||||
# variable. Only the coverage report needs this, so only coverage pays for it.
|
|
||||||
SRC_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
cd "$BUILD_DIR"
|
cd "$BUILD_DIR"
|
||||||
|
|
||||||
# Set minimum CMake Version
|
# Set minimum CMake Version
|
||||||
|
|
@ -172,17 +147,6 @@ fi
|
||||||
if [[ $MAKE_TEST ]]; then
|
if [[ $MAKE_TEST ]]; then
|
||||||
flags+=("-DTEST=1")
|
flags+=("-DTEST=1")
|
||||||
fi
|
fi
|
||||||
if [[ $MAKE_SANITIZE ]]; then
|
|
||||||
flags+=("-DENABLE_SANITIZERS=ON")
|
|
||||||
else
|
|
||||||
# The release-package step re-runs cmake in the same build dir, so an
|
|
||||||
# ENABLE_SANITIZERS=ON left over from the debug step would otherwise leak
|
|
||||||
# into the shipped binaries (libasan/libubsan Depends in the DEB).
|
|
||||||
flags+=("-DENABLE_SANITIZERS=OFF")
|
|
||||||
fi
|
|
||||||
if [[ $MAKE_COVERAGE ]]; then
|
|
||||||
flags+=("-DENABLE_COVERAGE=ON")
|
|
||||||
fi
|
|
||||||
if [[ $USE_CCACHE ]]; then
|
if [[ $USE_CCACHE ]]; then
|
||||||
flags+=("-DUSE_CCACHE=1")
|
flags+=("-DUSE_CCACHE=1")
|
||||||
# PCH-aware caching is required or ccache refuses to cache any TU that
|
# PCH-aware caching is required or ccache refuses to cache any TU that
|
||||||
|
|
@ -214,22 +178,6 @@ function ccachestatsverbose() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
function cpuinfo() {
|
|
||||||
# The number of cores decides how much build parallelism is useful; the hosted
|
|
||||||
# runners are not all sized alike, so read it off the machine instead of assuming.
|
|
||||||
case "$RUNNER_OS" in
|
|
||||||
Windows)
|
|
||||||
echo "cores: ${NUMBER_OF_PROCESSORS:-unknown}"
|
|
||||||
;;
|
|
||||||
macOS)
|
|
||||||
echo "cores: $(sysctl -n hw.ncpu)"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "cores: $(nproc 2>/dev/null || echo unknown)"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# Compile
|
# Compile
|
||||||
if [[ $RUNNER_OS == macOS ]]; then
|
if [[ $RUNNER_OS == macOS ]]; then
|
||||||
# QTDIR is needed for macOS since we actually only use the cached thin Qt binaries instead of the install-qt-action,
|
# QTDIR is needed for macOS since we actually only use the cached thin Qt binaries instead of the install-qt-action,
|
||||||
|
|
@ -264,14 +212,8 @@ if [[ $RUNNER_OS == macOS ]]; then
|
||||||
triplet_source="../vcpkg/triplets/community/$arch-osx.cmake"
|
triplet_source="../vcpkg/triplets/community/$arch-osx.cmake"
|
||||||
fi
|
fi
|
||||||
cp "$triplet_source" "$triplet_file"
|
cp "$triplet_source" "$triplet_file"
|
||||||
# The triplet is what vcpkg hashes into the ABI hash of every port, so two legs that
|
echo "set(VCPKG_CMAKE_SYSTEM_VERSION $TARGET_MACOS_VERSION)" >>"$triplet_file"
|
||||||
# agree here reuse each other's binary cache. Targets 14, 15 and 26 on the same Xcode
|
echo "set(VCPKG_OSX_DEPLOYMENT_TARGET $TARGET_MACOS_VERSION)" >>"$triplet_file"
|
||||||
# produce identical binaries when they share the lowest deployment target, so they all
|
|
||||||
# build against 14 to keep one vcpkg cache scope instead of three.
|
|
||||||
# Keep in sync with "vcpkg_scope" and "vcpkg_deployment_target" in desktop-build.yml.
|
|
||||||
vcpkg_target="${VCPKG_TARGET_MACOS_VERSION:-$TARGET_MACOS_VERSION}"
|
|
||||||
echo "set(VCPKG_CMAKE_SYSTEM_VERSION $vcpkg_target)" >>"$triplet_file"
|
|
||||||
echo "set(VCPKG_OSX_DEPLOYMENT_TARGET $vcpkg_target)" >>"$triplet_file"
|
|
||||||
flags+=("-DVCPKG_OVERLAY_TRIPLETS=$triplets_dir")
|
flags+=("-DVCPKG_OVERLAY_TRIPLETS=$triplets_dir")
|
||||||
flags+=("-DVCPKG_HOST_TRIPLET=$triplet_version")
|
flags+=("-DVCPKG_HOST_TRIPLET=$triplet_version")
|
||||||
flags+=("-DVCPKG_TARGET_TRIPLET=$triplet_version")
|
flags+=("-DVCPKG_TARGET_TRIPLET=$triplet_version")
|
||||||
|
|
@ -314,34 +256,9 @@ if [[ $RUNNER_OS == macOS ]]; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
elif [[ $RUNNER_OS == Windows ]]; then
|
elif [[ $RUNNER_OS == Windows ]]; then
|
||||||
if [[ $CMAKE_GENERATOR == "Visual Studio"* ]]; then
|
# Enable MTT, see https://devblogs.microsoft.com/cppblog/improved-parallelism-in-msbuild/
|
||||||
# cmake only hands MSBuild a job count when one is asked for, and without it
|
# and https://devblogs.microsoft.com/cppblog/cpp-build-throughput-investigation-and-tune-up/#multitooltask-mtt
|
||||||
# MSBuild builds a single node: one project at a time, whatever MTT does inside
|
buildflags+=(-- -p:UseMultiToolTask=true -p:EnableClServerMode=true)
|
||||||
# it. Ask for a node per core.
|
|
||||||
if [[ ! $BUILD_PARALLEL_LEVEL ]]; then
|
|
||||||
BUILD_PARALLEL_LEVEL="${NUMBER_OF_PROCESSORS:-1}"
|
|
||||||
fi
|
|
||||||
buildflags+=(--parallel "$BUILD_PARALLEL_LEVEL")
|
|
||||||
|
|
||||||
# Enable MTT, see https://devblogs.microsoft.com/cppblog/improved-parallelism-in-msbuild/
|
|
||||||
# and https://devblogs.microsoft.com/cppblog/cpp-build-throughput-investigation-and-tune-up/#multitooltask-mtt
|
|
||||||
# EnforceProcessCountAcrossBuilds makes the CL_MPCount ceiling apply across all
|
|
||||||
# the projects the nodes are building, instead of each node running its own
|
|
||||||
# cl.exe processes and /m and MTT multiplying into an oversubscribed machine.
|
|
||||||
buildflags+=(-- -p:UseMultiToolTask=true -p:EnableClServerMode=true -p:EnforceProcessCountAcrossBuilds=true -p:CL_MPCount="$BUILD_PARALLEL_LEVEL")
|
|
||||||
if [[ $BUILD_PROFILE ]]; then
|
|
||||||
# A performance summary says which projects and tasks cost what, the binary log
|
|
||||||
# keeps the full timeline; both are far too noisy to always produce. The files
|
|
||||||
# land next to the solution, i.e. in the build dir the workflow uploads. These
|
|
||||||
# join the MTT options above, which already carried the "--" separator.
|
|
||||||
buildflags+=("-flp:PerformanceSummary;v=q;LogFile=msbuild-perf.log" -bl:msbuild.binlog)
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
# Ninja runs the compiler itself, so it neither takes the MSBuild switches
|
|
||||||
# (it rejects -p: options outright) nor a job count: it sizes its own pool
|
|
||||||
# for the cores it finds.
|
|
||||||
echo "ninja $(ninja --version)"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $USE_CCACHE ]]; then
|
if [[ $USE_CCACHE ]]; then
|
||||||
|
|
@ -357,19 +274,10 @@ cmake .. "${flags[@]}"
|
||||||
echo "::endgroup::"
|
echo "::endgroup::"
|
||||||
|
|
||||||
echo "::group::Build project"
|
echo "::group::Build project"
|
||||||
cpuinfo
|
|
||||||
echo "Running cmake --build with flags: ${buildflags[*]}"
|
echo "Running cmake --build with flags: ${buildflags[*]}"
|
||||||
cmake --build . "${buildflags[@]}"
|
cmake --build . "${buildflags[@]}"
|
||||||
echo "::endgroup::"
|
echo "::endgroup::"
|
||||||
|
|
||||||
if [[ $BUILD_PROFILE && $RUNNER_OS == Windows && -f msbuild-perf.log ]]; then
|
|
||||||
# Repeat the summary in the job log so the numbers are readable without
|
|
||||||
# downloading the binary log, which is hundreds of megabytes.
|
|
||||||
echo "::group::MSBuild performance summary"
|
|
||||||
cat msbuild-perf.log
|
|
||||||
echo "::endgroup::"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ $USE_CCACHE ]]; then
|
if [[ $USE_CCACHE ]]; then
|
||||||
if [[ $CCACHE_EVICTION_AGE ]]; then
|
if [[ $CCACHE_EVICTION_AGE ]]; then
|
||||||
echo "::group::evict ccache files older than $CCACHE_EVICTION_AGE"
|
echo "::group::evict ccache files older than $CCACHE_EVICTION_AGE"
|
||||||
|
|
@ -398,50 +306,7 @@ fi
|
||||||
|
|
||||||
if [[ $MAKE_TEST ]]; then
|
if [[ $MAKE_TEST ]]; then
|
||||||
echo "::group::Run tests"
|
echo "::group::Run tests"
|
||||||
if [[ $MAKE_SANITIZE ]]; then
|
ctest -C "$BUILDTYPE" --output-on-failure
|
||||||
# detect_leaks=0: Qt and the sanitizers' own runtimes both leak at exit by design, and
|
|
||||||
# LeakSanitizer's default is aggressive enough to bury our real findings under hundreds
|
|
||||||
# of process-teardown reports. Malloc/free misuse and use-after-free are what ASan is for
|
|
||||||
# and they stay fully on.
|
|
||||||
# abort_on_error=1: a bad access leaves the process in an unusable state, so continuing
|
|
||||||
# only produces a second, more confusing report from the wreckage.
|
|
||||||
# UBSAN halt_on_error=1: without it UBSan prints "runtime error: ..." but the
|
|
||||||
# process exits 0 (recover mode), so gtest/ctest report a pass and the job
|
|
||||||
# stays green. Combined with -fno-sanitize-recover=undefined at compile time,
|
|
||||||
# the first finding aborts the test process with a non-zero exit.
|
|
||||||
export ASAN_OPTIONS="detect_leaks=0:abort_on_error=1:print_stacktrace=1:symbolize=1"
|
|
||||||
export UBSAN_OPTIONS="print_stacktrace=1:halt_on_error=1:symbolize=1"
|
|
||||||
fi
|
|
||||||
# Every TEST() is its own CTest entry, so the suite is process-spawn bound rather than CPU
|
|
||||||
# bound and -j recovers nearly all of the per-process overhead. The test binaries are safe to
|
|
||||||
# run concurrently: the only one that claims a fixed resource is single_instance_manager_test,
|
|
||||||
# which already scopes its local socket name to its own pid so parallel copies cannot collide.
|
|
||||||
ctest -C "$BUILDTYPE" --output-on-failure -j "${CTEST_JOBS:-4}"
|
|
||||||
echo "::endgroup::"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ $MAKE_COVERAGE ]]; then
|
|
||||||
# gcovr is not a build dependency and is not available everywhere, so a missing one is reported
|
|
||||||
# and skipped rather than failing an otherwise green build.
|
|
||||||
echo "::group::Coverage report"
|
|
||||||
GCOVR="$(command -v gcovr || true)"
|
|
||||||
if [[ -z $GCOVR ]]; then
|
|
||||||
echo "gcovr not found; install it to produce a coverage report"
|
|
||||||
else
|
|
||||||
"$GCOVR" --version
|
|
||||||
# This script configures with `cmake ..`, so the build dir it was started from is the object
|
|
||||||
# directory and the source root is one level up. Both are passed explicitly: gcovr otherwise
|
|
||||||
# walks the current directory and trips over in-tree build directories left by other tooling,
|
|
||||||
# whose stale .gcno files cannot be resolved. --config is named too, so the filters apply no
|
|
||||||
# matter what directory gcovr happens to start from.
|
|
||||||
GCOVR_ARGS=("." --root "$SRC_ROOT" --object-directory "." --config "$SRC_ROOT/gcovr.cfg")
|
|
||||||
"$GCOVR" "${GCOVR_ARGS[@]}" --txt --decisions
|
|
||||||
"$GCOVR" "${GCOVR_ARGS[@]}" --html-details coverage-details.html --html-title "Cockatrice line coverage"
|
|
||||||
"$GCOVR" "${GCOVR_ARGS[@]}" --lcov -o coverage.lcov
|
|
||||||
"$GCOVR" "${GCOVR_ARGS[@]}" --cobertura-pretty -o coverage.xml
|
|
||||||
"$GCOVR" "${GCOVR_ARGS[@]}" --json-summary-pretty -o coverage-summary.json
|
|
||||||
echo "Coverage artifacts written to coverage-details.html, coverage.lcov, coverage.xml and coverage-summary.json"
|
|
||||||
fi
|
|
||||||
echo "::endgroup::"
|
echo "::endgroup::"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,17 +13,11 @@
|
||||||
# --set-cache <location> sets the location to cache the image or for ccache
|
# --set-cache <location> sets the location to cache the image or for ccache
|
||||||
#
|
#
|
||||||
# requires: docker
|
# requires: docker
|
||||||
# uses env: NAME IMAGE_SUFFIX CACHE BUILD GET SAVE INTERACTIVE
|
# uses env: NAME CACHE BUILD GET SAVE INTERACTIVE
|
||||||
# (correspond to args: <name> --set-cache <cache> --build --get --save --interactive)
|
# (correspond to args: <name> --set-cache <cache> --build --get --save --interactive)
|
||||||
# sets env: RUN CCACHE_DIR IMAGE_NAME RUN_ARGS RUN_OPTS BUILD_SCRIPT
|
# sets env: RUN CCACHE_DIR IMAGE_NAME RUN_ARGS RUN_OPTS BUILD_SCRIPT
|
||||||
# exitcode: 1 for failure, 2 for missing dockerfile, 3 for invalid arguments
|
# exitcode: 1 for failure, 2 for missing dockerfile, 3 for invalid arguments
|
||||||
#
|
#
|
||||||
# IMAGE_SUFFIX is optional and only appended to the image name, never to NAME, so that a matrix
|
|
||||||
# leg can have its own image and image cache without needing its own directory under .ci. Two legs
|
|
||||||
# building and tagging the same image concurrently would race on `docker build --tag`.
|
|
||||||
# It is deliberately not called SUFFIX: compile.sh already uses SUFFIX for output file names, and
|
|
||||||
# a job-level SUFFIX would be silently overridden by those per-step env blocks.
|
|
||||||
#
|
|
||||||
# exported RUN function will run the BUILD_SCRIPT inside of the docker container.
|
# exported RUN function will run the BUILD_SCRIPT inside of the docker container.
|
||||||
# note that the docker container will not inherit any environment variables!
|
# note that the docker container will not inherit any environment variables!
|
||||||
#
|
#
|
||||||
|
|
@ -87,7 +81,7 @@ if ! [[ $NAME ]]; then
|
||||||
return 3
|
return 3
|
||||||
fi
|
fi
|
||||||
|
|
||||||
export IMAGE_NAME="${project_name,,}_${NAME,,}${IMAGE_SUFFIX:+_${IMAGE_SUFFIX,,}}" # lower case
|
export IMAGE_NAME="${project_name,,}_${NAME,,}" # lower case
|
||||||
|
|
||||||
docker_dir=".ci/$NAME"
|
docker_dir=".ci/$NAME"
|
||||||
if ! [[ -r $docker_dir/Dockerfile ]]; then
|
if ! [[ -r $docker_dir/Dockerfile ]]; then
|
||||||
|
|
|
||||||
249
.github/workflows/desktop-build.yml
vendored
249
.github/workflows/desktop-build.yml
vendored
|
|
@ -144,36 +144,17 @@ jobs:
|
||||||
|
|
||||||
package: DEB
|
package: DEB
|
||||||
|
|
||||||
# The one leg that runs the suite under AddressSanitizer and
|
name: ${{ matrix.distro }} ${{ matrix.version }}
|
||||||
# UndefinedBehaviorSanitizer. Debian 13 and Fedora 44 keep covering the
|
|
||||||
# uninstrumented suite, so nothing is lost by instrumenting this one.
|
|
||||||
sanitize: yes
|
|
||||||
|
|
||||||
- distro: Ubuntu
|
|
||||||
version: 26.04
|
|
||||||
variant: cover # separate image and ccache, same distro
|
|
||||||
|
|
||||||
# The one leg that runs instrumented for line coverage and publishes the report as
|
|
||||||
# a workflow artifact. It is a separate leg rather than a second flag on the
|
|
||||||
# sanitizer leg because the two instruments answer different questions: the
|
|
||||||
# sanitizer leg reports what the run did wrong, the coverage leg reports what the
|
|
||||||
# run never touched at all. Combining them would also slow both down. Packaging is
|
|
||||||
# skipped because the leg above already publishes the DEB for this distro.
|
|
||||||
coverage: yes
|
|
||||||
package: skip
|
|
||||||
|
|
||||||
name: ${{ matrix.distro }} ${{ matrix.version }}${{ matrix.variant && format(' {0}', matrix.variant) || '' }}
|
|
||||||
needs: configure
|
needs: configure
|
||||||
runs-on: ubuntu-latest # https://github.com/actions/runner-images
|
runs-on: ubuntu-latest # https://github.com/actions/runner-images
|
||||||
continue-on-error: ${{ matrix.allow-failure == 'yes' }}
|
continue-on-error: ${{ matrix.allow-failure == 'yes' }}
|
||||||
timeout-minutes: 70
|
timeout-minutes: 70
|
||||||
env:
|
env:
|
||||||
CACHE: ${{ github.workspace }}/.cache/${{ matrix.distro }}${{ matrix.version }}${{ matrix.variant }} # directory for caching docker image and ccache
|
CACHE: ${{ github.workspace }}/.cache/${{ matrix.distro }}${{ matrix.version }} # directory for caching docker image and ccache
|
||||||
CCACHE_EVICTION_AGE: 7d
|
CCACHE_EVICTION_AGE: 7d
|
||||||
CCACHE_SIZE: 600M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
|
CCACHE_SIZE: 600M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
|
||||||
CMAKE_GENERATOR: 'Ninja'
|
CMAKE_GENERATOR: 'Ninja'
|
||||||
NAME: ${{ matrix.distro }}${{ matrix.version }}
|
NAME: ${{ matrix.distro }}${{ matrix.version }}
|
||||||
IMAGE_SUFFIX: ${{ matrix.variant }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout"
|
- name: "Checkout"
|
||||||
|
|
@ -183,8 +164,8 @@ jobs:
|
||||||
id: restore_ccache
|
id: restore_ccache
|
||||||
uses: actions/cache/restore@v6
|
uses: actions/cache/restore@v6
|
||||||
with:
|
with:
|
||||||
key: ccache-${{ matrix.distro }}${{ matrix.version }}${{ matrix.variant }}
|
key: ccache-${{ matrix.distro }}${{ matrix.version }}
|
||||||
path: ${{ env.CACHE }}/.ccache
|
path: ${{ env.CACHE }}
|
||||||
|
|
||||||
- name: "Build ${{ matrix.distro }} ${{ matrix.version }} Docker image"
|
- name: "Build ${{ matrix.distro }} ${{ matrix.version }} Docker image"
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
@ -193,9 +174,6 @@ jobs:
|
||||||
- name: "Build debug and test"
|
- name: "Build debug and test"
|
||||||
if: matrix.test != 'skip'
|
if: matrix.test != 'skip'
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
|
||||||
sanitize: '${{ matrix.sanitize }}'
|
|
||||||
coverage: '${{ matrix.coverage }}'
|
|
||||||
run: |
|
run: |
|
||||||
source .ci/docker.sh
|
source .ci/docker.sh
|
||||||
args=()
|
args=()
|
||||||
|
|
@ -203,33 +181,7 @@ jobs:
|
||||||
args+=(--ccache "$CCACHE_SIZE")
|
args+=(--ccache "$CCACHE_SIZE")
|
||||||
args+=(--cmake-generator "$CMAKE_GENERATOR")
|
args+=(--cmake-generator "$CMAKE_GENERATOR")
|
||||||
|
|
||||||
extra=()
|
RUN --server --debug --test "${args[@]}"
|
||||||
[[ $sanitize == yes ]] && extra+=(--sanitize)
|
|
||||||
[[ $coverage == yes ]] && extra+=(--coverage)
|
|
||||||
|
|
||||||
RUN --server --debug --test "${extra[@]}" "${args[@]}"
|
|
||||||
|
|
||||||
# RUN bind-mounts the workspace into the container, so the report the build wrote into
|
|
||||||
# build/ is already on the host and no second container is needed to collect it.
|
|
||||||
- name: "Collect coverage report"
|
|
||||||
if: matrix.coverage == 'yes'
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
mkdir -p coverage
|
|
||||||
# --html-details writes one page per source file plus a stylesheet and script, and the
|
|
||||||
# index links to all of them by relative name. Copying the index alone would publish a
|
|
||||||
# report where every link 404s, so the whole family has to travel together.
|
|
||||||
cp build/coverage-details* coverage/
|
|
||||||
cp build/coverage.lcov build/coverage.xml build/coverage-summary.json coverage/
|
|
||||||
ls -la coverage/
|
|
||||||
|
|
||||||
- name: "Upload coverage artifact"
|
|
||||||
if: matrix.coverage == 'yes'
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: coverage-report
|
|
||||||
path: coverage/
|
|
||||||
if-no-files-found: warn
|
|
||||||
|
|
||||||
- name: "Build release package"
|
- name: "Build release package"
|
||||||
id: build
|
id: build
|
||||||
|
|
@ -267,7 +219,7 @@ jobs:
|
||||||
uses: actions/cache/save@v6
|
uses: actions/cache/save@v6
|
||||||
with:
|
with:
|
||||||
key: ${{ steps.restore_ccache.outputs.cache-primary-key }}
|
key: ${{ steps.restore_ccache.outputs.cache-primary-key }}
|
||||||
path: ${{ env.CACHE }}/.ccache
|
path: ${{ env.CACHE }}
|
||||||
|
|
||||||
- name: "Upload artifact"
|
- name: "Upload artifact"
|
||||||
id: upload_artifact
|
id: upload_artifact
|
||||||
|
|
@ -325,8 +277,6 @@ jobs:
|
||||||
soc: Intel
|
soc: Intel
|
||||||
type: Release
|
type: Release
|
||||||
use_ccache: 1
|
use_ccache: 1
|
||||||
vcpkg_scope: 'vcpkg/macos13-x64-xcode26.3/'
|
|
||||||
vcpkg_upload: 1 # only one leg per vcpkg_scope hands its archives over
|
|
||||||
xcode: "26.3"
|
xcode: "26.3"
|
||||||
|
|
||||||
- os: macOS
|
- os: macOS
|
||||||
|
|
@ -343,9 +293,6 @@ jobs:
|
||||||
soc: Apple
|
soc: Apple
|
||||||
type: Release
|
type: Release
|
||||||
use_ccache: 1
|
use_ccache: 1
|
||||||
vcpkg_deployment_target: 14
|
|
||||||
vcpkg_scope: 'vcpkg/macos14-arm64-xcode26.6/'
|
|
||||||
vcpkg_upload: 1 # only one leg per vcpkg_scope hands its archives over
|
|
||||||
xcode: "26.6"
|
xcode: "26.6"
|
||||||
|
|
||||||
- os: macOS
|
- os: macOS
|
||||||
|
|
@ -362,8 +309,6 @@ jobs:
|
||||||
soc: Apple
|
soc: Apple
|
||||||
type: Release
|
type: Release
|
||||||
use_ccache: 1
|
use_ccache: 1
|
||||||
vcpkg_deployment_target: 14
|
|
||||||
vcpkg_scope: 'vcpkg/macos14-arm64-xcode26.6/'
|
|
||||||
xcode: "26.6"
|
xcode: "26.6"
|
||||||
|
|
||||||
- os: macOS
|
- os: macOS
|
||||||
|
|
@ -377,30 +322,19 @@ jobs:
|
||||||
soc: Apple
|
soc: Apple
|
||||||
type: Debug
|
type: Debug
|
||||||
use_ccache: 1
|
use_ccache: 1
|
||||||
vcpkg_deployment_target: 14
|
|
||||||
vcpkg_scope: 'vcpkg/macos14-arm64-xcode26.6/'
|
|
||||||
xcode: "26.6"
|
xcode: "26.6"
|
||||||
|
|
||||||
- os: Windows
|
- os: Windows
|
||||||
target: 10
|
target: 10
|
||||||
runner: windows-2025 # https://github.com/actions/runner-images/blob/main/images/windows/Windows2025-VS2026-Readme.md
|
runner: windows-2025 # https://github.com/actions/runner-images/blob/main/images/windows/Windows2025-VS2026-Readme.md
|
||||||
|
|
||||||
# Ninja builds the whole dependency graph at once instead of one
|
cmake_generator: "Visual Studio 18 2026"
|
||||||
# MSBuild node at a time, runs the compiler itself, and matches the
|
cmake_generator_platform: x64
|
||||||
# Linux and macOS legs. Its median build phase measured ~17 min
|
|
||||||
# against ~21 for the Visual Studio generator (shared 4-core hosts
|
|
||||||
# swing either by minutes, so read medians, not single runs).
|
|
||||||
cmake_generator: Ninja
|
|
||||||
make_package: 1
|
make_package: 1
|
||||||
package_suffix: "-Win10"
|
package_suffix: "-Win10"
|
||||||
qt_version: 6.11.*
|
qt_version: 6.11.*
|
||||||
qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets
|
qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets
|
||||||
type: Release
|
type: Release
|
||||||
# Scope is named after the vs2026 toolchain, not the generator: the
|
|
||||||
# ABI hash covers cl.exe, which is identical, so this leg keeps
|
|
||||||
# restoring the warm archives. Renaming would orphan them.
|
|
||||||
vcpkg_scope: 'vcpkg/windows10-x64-vs2026/'
|
|
||||||
vcpkg_upload: 1 # only one leg per vcpkg_scope hands its archives over
|
|
||||||
|
|
||||||
name: ${{ matrix.os }} ${{ matrix.target }}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }}
|
name: ${{ matrix.os }} ${{ matrix.target }}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }}
|
||||||
needs: configure
|
needs: configure
|
||||||
|
|
@ -494,39 +428,12 @@ jobs:
|
||||||
shell: bash
|
shell: bash
|
||||||
run: choco install nsis
|
run: choco install nsis
|
||||||
|
|
||||||
# Ninja invokes cl.exe directly and needs the MSVC environment on PATH.
|
|
||||||
- name: "[Windows] Set up MSVC environment"
|
|
||||||
if: matrix.os == 'Windows'
|
|
||||||
uses: TheMrMilchmann/setup-msvc-dev@368ef7d1ee4d1171b31d4a7f67f4d954f903f5a9 # v4.1.0
|
|
||||||
with:
|
|
||||||
arch: x64
|
|
||||||
|
|
||||||
- name: "Setup vcpkg cache"
|
- name: "Setup vcpkg cache"
|
||||||
id: vcpkg-cache
|
id: vcpkg-cache
|
||||||
uses: TAServers/vcpkg-cache@v3
|
uses: TAServers/vcpkg-cache@v3
|
||||||
with:
|
with:
|
||||||
# Every entry is keyed by vcpkg ABI hash, and one hash set exists per platform,
|
|
||||||
# target and toolchain. The action restores every entry it finds under the
|
|
||||||
# prefix, not just the hashes vcpkg asks for, so with one shared vcpkg/
|
|
||||||
# namespace every leg downloads all 66 archives of all 5 legs on every run.
|
|
||||||
# Each of those restores also refreshes the entry's last access date, which
|
|
||||||
# is why a dead ABI generation is never evicted by age. Naming the scope is
|
|
||||||
# what makes those generations identifiable, and prune-caches below able to
|
|
||||||
# delete them.
|
|
||||||
prefix: ${{ matrix.vcpkg_scope }}
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
# The action only creates its directory when the prefix matched at least one
|
|
||||||
# entry, and it reads that directory unguarded when it saves at the end of the
|
|
||||||
# job. A scope nobody has written to yet therefore leaves the directory missing
|
|
||||||
# and fails the job, which is exactly the state every scope starts in. Make the
|
|
||||||
# directory up front so an empty scope is a cache miss instead of an error.
|
|
||||||
- name: "Create vcpkg cache directory"
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
CACHE_DIR: '${{ steps.vcpkg-cache.outputs.path }}'
|
|
||||||
run: mkdir -p "$CACHE_DIR"
|
|
||||||
|
|
||||||
# Uses environment variables, see compile.sh for more details
|
# Uses environment variables, see compile.sh for more details
|
||||||
- name: "Build Cockatrice"
|
- name: "Build Cockatrice"
|
||||||
id: build
|
id: build
|
||||||
|
|
@ -535,6 +442,7 @@ jobs:
|
||||||
BUILDTYPE: '${{ matrix.type }}'
|
BUILDTYPE: '${{ matrix.type }}'
|
||||||
CCACHE_EVICTION_AGE: ${{ matrix.ccache_eviction_age }}
|
CCACHE_EVICTION_AGE: ${{ matrix.ccache_eviction_age }}
|
||||||
CMAKE_GENERATOR: ${{ matrix.cmake_generator }}
|
CMAKE_GENERATOR: ${{ matrix.cmake_generator }}
|
||||||
|
CMAKE_GENERATOR_PLATFORM: ${{ matrix.cmake_generator_platform }}
|
||||||
DEVELOPER_DIR: '/Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer'
|
DEVELOPER_DIR: '/Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer'
|
||||||
GITHUB_TOKEN: ${{ github.token }} # needed for vcpkg dependency graph updates, see VCPKG_FEATURE_FLAGS
|
GITHUB_TOKEN: ${{ github.token }} # needed for vcpkg dependency graph updates, see VCPKG_FEATURE_FLAGS
|
||||||
MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }}
|
MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }}
|
||||||
|
|
@ -545,19 +453,9 @@ jobs:
|
||||||
PACKAGE_SUFFIX: '${{ matrix.package_suffix }}'
|
PACKAGE_SUFFIX: '${{ matrix.package_suffix }}'
|
||||||
TARGET_MACOS_VERSION: ${{ matrix.override_target }}
|
TARGET_MACOS_VERSION: ${{ matrix.override_target }}
|
||||||
USE_CCACHE: ${{ matrix.use_ccache }}
|
USE_CCACHE: ${{ matrix.use_ccache }}
|
||||||
# Only pushes to the default branch may write the shared binary cache: the
|
|
||||||
# archives a run compiles are only as trustworthy as the code they were
|
|
||||||
# built from, and only merged code may populate the scope that master and
|
|
||||||
# release builds restore and link. Pull requests (same-repository ones
|
|
||||||
# included) restore the shared cache but never write it; with the source
|
|
||||||
# left at read the collect step below finds nothing on PRs, so no
|
|
||||||
# pull-request-scoped entry is ever saved that no later run could restore.
|
|
||||||
# Forks pushing to their own default branch resolve github.ref against
|
|
||||||
# their own repository, so they keep a working cache of their own there.
|
|
||||||
VCPKG_BINARY_SOURCES: "clear;files,${{ steps.vcpkg-cache.outputs.path }},${{ case(github.ref == 'refs/heads/master', 'readwrite', 'read') }}"
|
VCPKG_BINARY_SOURCES: "clear;files,${{ steps.vcpkg-cache.outputs.path }},${{ case(github.ref == 'refs/heads/master', 'readwrite', 'read') }}"
|
||||||
VCPKG_DISABLE_METRICS: 1
|
VCPKG_DISABLE_METRICS: 1
|
||||||
VCPKG_FEATURE_FLAGS: dependencygraph
|
VCPKG_FEATURE_FLAGS: dependencygraph
|
||||||
VCPKG_TARGET_MACOS_VERSION: ${{ matrix.vcpkg_deployment_target }}
|
|
||||||
run: .ci/compile.sh --server --test --vcpkg
|
run: .ci/compile.sh --server --test --vcpkg
|
||||||
|
|
||||||
# CPack reports a failed makensis run as "Problem running NSIS command"
|
# CPack reports a failed makensis run as "Problem running NSIS command"
|
||||||
|
|
@ -571,63 +469,6 @@ jobs:
|
||||||
shell: bash
|
shell: bash
|
||||||
run: find . -name NSISOutput.log -exec cat {} +
|
run: find . -name NSISOutput.log -exec cat {} +
|
||||||
|
|
||||||
# A master run hands over the archives it had to build itself so that
|
|
||||||
# vcpkg-cache-import.yml, which runs in the default branch scope, can save them
|
|
||||||
# where later runs can restore them. Pull requests hand over nothing: their cache
|
|
||||||
# entries would be scoped to refs/pull/<n>/merge and stay unreadable for every
|
|
||||||
# other run, so writing them only burns the repository's quota.
|
|
||||||
# vcpkg_upload keeps this to one leg per scope: macOS 14, 15 and 26 share a scope
|
|
||||||
# by design, and all three rebuild the same archives, so uploading from each of them
|
|
||||||
# would publish three identical 269 MB artifacts under one artifact name and make
|
|
||||||
# the importer spawn a save job per copy.
|
|
||||||
- name: "Collect new vcpkg entries"
|
|
||||||
id: vcpkg_new
|
|
||||||
if: github.ref == 'refs/heads/master' && matrix.vcpkg_upload == 1
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
CACHE_DIR: '${{ steps.vcpkg-cache.outputs.path }}'
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
VCPKG_SCOPE: '${{ matrix.vcpkg_scope }}'
|
|
||||||
run: |
|
|
||||||
# The archive name is the ABI hash, so a missing key is exactly what vcpkg had
|
|
||||||
# to compile from source in this job.
|
|
||||||
gh api --paginate \
|
|
||||||
"repos/$GITHUB_REPOSITORY/actions/caches?ref=refs/heads/master&key=$VCPKG_SCOPE&per_page=100" \
|
|
||||||
--jq '.actions_caches[].key' | sort -u >"$RUNNER_TEMP/master-keys.txt"
|
|
||||||
find "$CACHE_DIR" -type f -name '*.zip' -exec basename {} .zip \; |
|
|
||||||
sed "s|^|$VCPKG_SCOPE|" | sort -u >"$RUNNER_TEMP/local-keys.txt"
|
|
||||||
comm -23 "$RUNNER_TEMP/local-keys.txt" "$RUNNER_TEMP/master-keys.txt" >"$RUNNER_TEMP/new-keys.txt"
|
|
||||||
|
|
||||||
mkdir -p "$RUNNER_TEMP/vcpkg-new"
|
|
||||||
: >"$RUNNER_TEMP/vcpkg-new/keys.txt"
|
|
||||||
while read -r key; do
|
|
||||||
hash="${key##*/}"
|
|
||||||
archive="$(find "$CACHE_DIR" -type f -name "$hash.zip" -print -quit)"
|
|
||||||
dir="$RUNNER_TEMP/vcpkg-new/${hash:0:2}" # vcpkg's own two character fanout
|
|
||||||
mkdir -p "$dir"
|
|
||||||
cp "$archive" "$dir/"
|
|
||||||
echo "$key ${hash:0:2}/$hash.zip" >>"$RUNNER_TEMP/vcpkg-new/keys.txt"
|
|
||||||
done <"$RUNNER_TEMP/new-keys.txt"
|
|
||||||
|
|
||||||
count="$(wc -l <"$RUNNER_TEMP/new-keys.txt")"
|
|
||||||
echo "vcpkg scope $VCPKG_SCOPE: $count entries to import"
|
|
||||||
echo "count=$count" >>"$GITHUB_OUTPUT"
|
|
||||||
echo "slug=$(sed -e 's|^vcpkg/||' -e 's|/$||' -e 's|/|-|g' <<<"$VCPKG_SCOPE")" >>"$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
# archive: false accepts a single file and nothing else, and a scope hands over one
|
|
||||||
# archive per ABI hash vcpkg had to rebuild, which is 23 files on a cold runner.
|
|
||||||
# Zipping the directory keeps the fanout at the artifact root, which is where
|
|
||||||
# vcpkg-cache-import.yml looks for keys.txt once download-artifact unpacks it.
|
|
||||||
- name: "Upload new vcpkg entries"
|
|
||||||
# An empty count means the step above did not run at all, on master and on forks
|
|
||||||
if: steps.vcpkg_new.outputs.count != '' && steps.vcpkg_new.outputs.count != '0'
|
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
with:
|
|
||||||
if-no-files-found: error # a count above zero with nothing staged is a bug
|
|
||||||
name: vcpkg-new-${{ steps.vcpkg_new.outputs.slug }}
|
|
||||||
path: ${{ runner.temp }}/vcpkg-new
|
|
||||||
retention-days: 1
|
|
||||||
|
|
||||||
# Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342
|
# Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342
|
||||||
- name: "[macOS] Delete remote compiler cache (ccache)"
|
- name: "[macOS] Delete remote compiler cache (ccache)"
|
||||||
if: matrix.os == 'macOS' && matrix.use_ccache == 1 && github.ref == 'refs/heads/master' && steps.restore_ccache.outputs.cache-hit
|
if: matrix.os == 'macOS' && matrix.use_ccache == 1 && github.ref == 'refs/heads/master' && steps.restore_ccache.outputs.cache-hit
|
||||||
|
|
@ -703,8 +544,6 @@ jobs:
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
path: ${{ steps.build.outputs.path }}
|
path: ${{ steps.build.outputs.path }}
|
||||||
|
|
||||||
# Ninja is single-config: binaries and their PDBs land directly in the
|
|
||||||
# target directories instead of a Release subdirectory.
|
|
||||||
- name: "[Windows] Upload PDBs (Program Databases)"
|
- name: "[Windows] Upload PDBs (Program Databases)"
|
||||||
if: matrix.os == 'Windows' && github.ref_type != 'tag'
|
if: matrix.os == 'Windows' && github.ref_type != 'tag'
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
|
|
@ -712,9 +551,9 @@ jobs:
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
name: ${{ steps.build.outputs.name }}-PDBs
|
name: ${{ steps.build.outputs.name }}-PDBs
|
||||||
path: |
|
path: |
|
||||||
build/cockatrice/*.pdb
|
build/cockatrice/Release/*.pdb
|
||||||
build/oracle/*.pdb
|
build/oracle/Release/*.pdb
|
||||||
build/servatrice/*.pdb
|
build/servatrice/Release/*.pdb
|
||||||
|
|
||||||
- name: "Upload to release"
|
- name: "Upload to release"
|
||||||
if: needs.configure.outputs.tag != null && matrix.make_package == '1'
|
if: needs.configure.outputs.tag != null && matrix.make_package == '1'
|
||||||
|
|
@ -742,67 +581,3 @@ jobs:
|
||||||
BUILD_PATH: ${{ steps.build.outputs.path }}
|
BUILD_PATH: ${{ steps.build.outputs.path }}
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
run: gh attestation verify "$BUILD_PATH" --repo Cockatrice/Cockatrice
|
run: gh attestation verify "$BUILD_PATH" --repo Cockatrice/Cockatrice
|
||||||
|
|
||||||
prune-caches:
|
|
||||||
name: Prune caches
|
|
||||||
# Only master writes caches at all, so only master may clean them up too
|
|
||||||
if: github.ref == 'refs/heads/master'
|
|
||||||
runs-on: ubuntu-slim # https://github.com/actions/runner-images/blob/main/images/ubuntu-slim/ubuntu-slim-Readme.md
|
|
||||||
continue-on-error: true # freeing space must never fail a build
|
|
||||||
|
|
||||||
steps:
|
|
||||||
# The repository holds 10Gi of cache against entries that want more, and GitHub
|
|
||||||
# evicts the least recently used entry whenever that is exceeded. The vcpkg
|
|
||||||
# entries are the reason that never happens: the restore above touches every
|
|
||||||
# entry under the prefix on every run, so a dead ABI generation keeps its access
|
|
||||||
# date fresh forever and age based eviction can never reach it. Deleting by age
|
|
||||||
# alone cannot fix that, only knowing which scope an entry belongs to can.
|
|
||||||
- name: "Delete unusable cache entries"
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
# Must list every vcpkg_scope of the build-vcpkg matrix above, or the entries of
|
|
||||||
# a scope that is still in use get deleted on the next run.
|
|
||||||
LIVE_VCPKG_SCOPES: 'vcpkg/macos13-x64-xcode26.3/ vcpkg/macos14-arm64-xcode26.6/ vcpkg/windows10-x64-vs2026/'
|
|
||||||
run: |
|
|
||||||
gh api --paginate "repos/$GITHUB_REPOSITORY/actions/caches?per_page=100" \
|
|
||||||
--jq '.actions_caches[] | [.id, .ref, .key, .last_accessed_at] | @tsv' >"$RUNNER_TEMP/caches.tsv"
|
|
||||||
|
|
||||||
cutoff="$(( $(date -u +%s) - 7 * 24 * 3600 ))"
|
|
||||||
live=" ${LIVE_VCPKG_SCOPES} "
|
|
||||||
deleted=0
|
|
||||||
while IFS=$'\t' read -r id ref key accessed; do
|
|
||||||
[[ -n $id ]] || continue
|
|
||||||
# Only ever touch the key shapes this workflow creates, so that an entry of
|
|
||||||
# some other workflow can never be deleted by accident
|
|
||||||
case "$key" in
|
|
||||||
vcpkg/* | ccache-* | Qt*) ;;
|
|
||||||
*) continue ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
reason=""
|
|
||||||
if [[ $ref != refs/heads/master ]]; then
|
|
||||||
reason="scoped to $ref"
|
|
||||||
elif [[ $key == vcpkg/* ]]; then
|
|
||||||
# "<scope>/<abi hash>", cut the hash off the end to get the scope
|
|
||||||
scope="vcpkg/${key#vcpkg/}"
|
|
||||||
scope="${scope%/*}/"
|
|
||||||
if [[ $live != *" $scope "* ]]; then
|
|
||||||
reason="no matrix leg uses $scope"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
if [[ -z $reason ]]; then
|
|
||||||
stamp="$(date -u -d "$accessed" +%s 2>/dev/null || echo 0)"
|
|
||||||
if [[ $stamp -ne 0 && $stamp -lt $cutoff ]]; then
|
|
||||||
reason="unused since $accessed"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n $reason ]]; then
|
|
||||||
echo "::notice::deleting $key, $reason"
|
|
||||||
gh api -X DELETE "repos/$GITHUB_REPOSITORY/actions/caches/$id"
|
|
||||||
deleted=$((deleted + 1))
|
|
||||||
fi
|
|
||||||
done <"$RUNNER_TEMP/caches.tsv"
|
|
||||||
|
|
||||||
echo "::notice::deleted $deleted of $(wc -l <"$RUNNER_TEMP/caches.tsv") cache entries"
|
|
||||||
|
|
|
||||||
121
.github/workflows/vcpkg-cache-import.yml
vendored
121
.github/workflows/vcpkg-cache-import.yml
vendored
|
|
@ -1,121 +0,0 @@
|
||||||
name: Import vcpkg cache
|
|
||||||
|
|
||||||
# After a runner image update changes every port's ABI hash, the first master run
|
|
||||||
# rebuilds protobuf, openssl and abseil from source and saves them into the shared
|
|
||||||
# cache, where every later run can restore them.
|
|
||||||
#
|
|
||||||
# Only master pushes are imported here: the collect step, keys.txt and the zip
|
|
||||||
# contents all come from the workflow run being imported, so importing from an
|
|
||||||
# unmerged pull request would let anyone who can push a branch — or a bot actor
|
|
||||||
# such as Dependabot, whose PRs are same-repo — promote arbitrary binaries into
|
|
||||||
# the scope that master and tag builds restore and link into signed releases.
|
|
||||||
# Forks build untrusted code and must never reach that scope either.
|
|
||||||
#
|
|
||||||
# repository_dispatch is not used on purpose: it would also resolve to the default branch,
|
|
||||||
# but it counts as a low-trust trigger and may only write there when it declares a
|
|
||||||
# write-capable cache-mode, which puts it in the same position as workflow_run.
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
actions: write # needed to save entries into the default branch's cache scope
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
workflows: ["Build Desktop"]
|
|
||||||
types: [completed]
|
|
||||||
|
|
||||||
# Every import writes the same scopes, so let them queue instead of racing each other
|
|
||||||
concurrency:
|
|
||||||
group: vcpkg-cache-import
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
list:
|
|
||||||
name: List new vcpkg entries
|
|
||||||
# Only a successful push to the default branch has archives worth importing.
|
|
||||||
# Only code that landed on master may populate the scope master runs restore.
|
|
||||||
# Bot actors are excluded on top: their runs must never promote binaries into
|
|
||||||
# the shared scope even if they somehow target the default branch.
|
|
||||||
if: >-
|
|
||||||
github.event.workflow_run.conclusion == 'success' &&
|
|
||||||
github.event.workflow_run.event == 'push' &&
|
|
||||||
github.event.workflow_run.head_branch == 'master' &&
|
|
||||||
github.event.workflow_run.head_repository.full_name == github.repository &&
|
|
||||||
!endsWith(github.event.workflow_run.actor.login, '[bot]') &&
|
|
||||||
!endsWith(github.event.workflow_run.triggering_actor.login, '[bot]')
|
|
||||||
runs-on: ubuntu-slim # https://github.com/actions/runner-images/blob/main/images/ubuntu-slim/ubuntu-slim-Readme.md
|
|
||||||
# workflow_run is a low-trust trigger, so cache access is read-only by default even
|
|
||||||
# though this run resolves to the default branch. Assert below that this took effect,
|
|
||||||
# because a save under read-only access only warns and would import nothing silently.
|
|
||||||
cache-mode: write
|
|
||||||
outputs:
|
|
||||||
entries: ${{ steps.scan.outputs.entries }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: "Download new vcpkg entries"
|
|
||||||
uses: actions/download-artifact@v8
|
|
||||||
with:
|
|
||||||
pattern: vcpkg-new-*
|
|
||||||
path: incoming
|
|
||||||
run-id: ${{ github.event.workflow_run.id }}
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: "Assert write access"
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
if [[ $ACTIONS_CACHE_MODE != *write* ]]; then
|
|
||||||
echo "::error::expected a write-capable cache, got '$ACTIONS_CACHE_MODE'"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: "List cache entries"
|
|
||||||
id: scan
|
|
||||||
shell: bash
|
|
||||||
working-directory: incoming
|
|
||||||
run: |
|
|
||||||
entries="$(find . -name keys.txt -type f -exec sh -c '
|
|
||||||
for manifest; do
|
|
||||||
artifact="$(basename "$(dirname "$manifest")")"
|
|
||||||
while read -r key path; do
|
|
||||||
printf "{\"artifact\": \"%s\", \"key\": \"%s\", \"path\": \"%s\"}\n" \
|
|
||||||
"$artifact" "$key" "$path"
|
|
||||||
done <"$manifest"
|
|
||||||
done
|
|
||||||
' _ {} +)"
|
|
||||||
[[ -n $entries ]] || exit 0
|
|
||||||
# Compact, because a multi line value cannot be written to GITHUB_OUTPUT
|
|
||||||
entries="$(jq -cs '.' <<<"$entries")"
|
|
||||||
echo "entries=$entries" >>"$GITHUB_OUTPUT"
|
|
||||||
echo "::notice::importing $(jq length <<<"$entries") vcpkg entries"
|
|
||||||
|
|
||||||
save:
|
|
||||||
name: "Save ${{ matrix.entry.key }}"
|
|
||||||
needs: list
|
|
||||||
if: needs.list.outputs.entries
|
|
||||||
runs-on: ubuntu-slim
|
|
||||||
cache-mode: write
|
|
||||||
strategy:
|
|
||||||
# One job per archive, they run in parallel and each only uploads a single file
|
|
||||||
fail-fast: false
|
|
||||||
max-parallel: 20
|
|
||||||
matrix:
|
|
||||||
entry: ${{ fromJSON(needs.list.outputs.entries) }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: "Download new vcpkg entries"
|
|
||||||
uses: actions/download-artifact@v8
|
|
||||||
with:
|
|
||||||
pattern: ${{ matrix.entry.artifact }}
|
|
||||||
path: incoming
|
|
||||||
run-id: ${{ github.event.workflow_run.id }}
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: "Save vcpkg entry"
|
|
||||||
uses: actions/cache/save@v6
|
|
||||||
with:
|
|
||||||
# The payload is a Windows or macOS archive while the upload runs on Linux, and
|
|
||||||
# without this the entry would be stamped as a Linux cache that neither of them
|
|
||||||
# is allowed to restore.
|
|
||||||
enableCrossOsArchive: true
|
|
||||||
key: ${{ matrix.entry.key }}
|
|
||||||
path: incoming/${{ matrix.entry.path }}
|
|
||||||
|
|
@ -22,10 +22,6 @@ option(WITH_ORACLE "Build Cockatrice card database tool (Oracle)" ON)
|
||||||
option(WITH_SERVER "Build Cockatrice server (Servatrice)" OFF)
|
option(WITH_SERVER "Build Cockatrice server (Servatrice)" OFF)
|
||||||
# Compile tests
|
# Compile tests
|
||||||
option(TEST "Build tests" OFF)
|
option(TEST "Build tests" OFF)
|
||||||
# Build with AddressSanitizer and UndefinedBehaviorSanitizer
|
|
||||||
option(ENABLE_SANITIZERS "Build with AddressSanitizer and UndefinedBehaviorSanitizer" OFF)
|
|
||||||
# Build instrumented for GCC line coverage, reported with gcovr
|
|
||||||
option(ENABLE_COVERAGE "Build instrumented for GCC line coverage" OFF)
|
|
||||||
# Use vcpkg regardless of OS
|
# Use vcpkg regardless of OS
|
||||||
option(USE_VCPKG "Use vcpkg regardless of OS" OFF)
|
option(USE_VCPKG "Use vcpkg regardless of OS" OFF)
|
||||||
|
|
||||||
|
|
@ -90,7 +86,7 @@ endif()
|
||||||
|
|
||||||
# A project name is needed for CPack
|
# A project name is needed for CPack
|
||||||
# Version can be overriden by git tags, see cmake/getversion.cmake
|
# Version can be overriden by git tags, see cmake/getversion.cmake
|
||||||
project("Cockatrice" VERSION 3.1.2)
|
project("Cockatrice" VERSION 3.1.1)
|
||||||
|
|
||||||
# Set release name if not provided via env/cmake var
|
# Set release name if not provided via env/cmake var
|
||||||
if(NOT DEFINED GIT_TAG_RELEASENAME)
|
if(NOT DEFINED GIT_TAG_RELEASENAME)
|
||||||
|
|
@ -215,12 +211,6 @@ else()
|
||||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pipe")
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pipe")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Always included, because they define cockatrice_disable_sanitizers_for() and
|
|
||||||
# cockatrice_disable_coverage_for() for the vendored trees. They only touch the global flags
|
|
||||||
# when ENABLE_SANITIZERS or ENABLE_COVERAGE is on.
|
|
||||||
include(Sanitizers)
|
|
||||||
include(Coverage)
|
|
||||||
|
|
||||||
# GNU systems need to define the Mersenne exponent for the RNG to compile w/o warning
|
# GNU systems need to define the Mersenne exponent for the RNG to compile w/o warning
|
||||||
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
||||||
add_definitions("-DSFMT_MEXP=19937")
|
add_definitions("-DSFMT_MEXP=19937")
|
||||||
|
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
# Line-coverage instrumentation opt-in, plus per-source opt-out
|
|
||||||
# Input: ENABLE_COVERAGE
|
|
||||||
# Defines: cockatrice_disable_coverage_for
|
|
||||||
#
|
|
||||||
# Included once from the root CMakeLists.txt, immediately after the per-compiler flag
|
|
||||||
# block, so add_compile_options() reaches every library and executable defined below.
|
|
||||||
#
|
|
||||||
# Note the name deliberately shadows CMake's own built-in Modules/Coverage.cmake. It resolves
|
|
||||||
# to this file only because the root CMakeLists.txt puts cmake/ at the front of
|
|
||||||
# CMAKE_MODULE_PATH; if that ever moves, include(Coverage) would silently pick up the built-in
|
|
||||||
# module, which only defines a `COVERAGE` custom target and touches no global flags, so
|
|
||||||
# instrumentation would quietly stop happening rather than erroring.
|
|
||||||
|
|
||||||
include(CheckCXXSourceCompiles)
|
|
||||||
|
|
||||||
# Opt a set of vendored sources out of instrumentation.
|
|
||||||
#
|
|
||||||
# cockatrice_disable_coverage_for(<source> ...)
|
|
||||||
#
|
|
||||||
# Third-party code drags the total down with lines no test can ever reach, and a total that
|
|
||||||
# cannot move is not a useful signal. This is the same carve-out the sanitizer build uses.
|
|
||||||
#
|
|
||||||
# The sources must be named relative to the directory that compiles them. CMake resolves a
|
|
||||||
# source-file property against the directory scope the target was declared in, so calling
|
|
||||||
# this from the root with absolute paths silently does nothing. That is why the call sites
|
|
||||||
# live next to the add_library/add_executable that pull the vendored code in.
|
|
||||||
#
|
|
||||||
# SKIP_PRECOMPILE_HEADERS mirrors the sanitizer helper. The precompiled header carries the
|
|
||||||
# instrumentation flags, and mixing an instrumented PCH with an uninstrumented translation
|
|
||||||
# unit makes GCC reject it.
|
|
||||||
function(cockatrice_disable_coverage_for)
|
|
||||||
if(NOT ENABLE_COVERAGE)
|
|
||||||
return()
|
|
||||||
endif()
|
|
||||||
# There is no -fno-coverage: --coverage is shorthand for -fprofile-arcs -ftest-coverage, so both
|
|
||||||
# halves have to be negated. CC rejects "-fno-coverage" outright, which is how the vendored C
|
|
||||||
# files below hit it first.
|
|
||||||
set_source_files_properties(
|
|
||||||
${ARGN} PROPERTIES COMPILE_OPTIONS "-fno-profile-arcs;-fno-test-coverage" SKIP_PRECOMPILE_HEADERS ON
|
|
||||||
)
|
|
||||||
endfunction()
|
|
||||||
|
|
||||||
if(ENABLE_COVERAGE)
|
|
||||||
# Line coverage needs the compiler's own toolchain, so only GCC is supported. Clang's
|
|
||||||
# equivalent is llvm-cov, which reads a different intermediate format and would need a
|
|
||||||
# separate report path; rejecting it is better than emitting .gcno files that gcovr cannot
|
|
||||||
# read.
|
|
||||||
if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
|
||||||
message(FATAL_ERROR "ENABLE_COVERAGE requires GCC, but the compiler is ${CMAKE_CXX_COMPILER_ID}. "
|
|
||||||
"Clang coverage needs llvm-cov and is not wired up."
|
|
||||||
)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# Probe the way the flag will actually be used: compile AND link. A .gcno file comes from the
|
|
||||||
# compile step, but the runtime that writes .gcda files comes from the link step, so probing
|
|
||||||
# only the compile step would pass on a toolchain that cannot actually produce coverage.
|
|
||||||
set(_cockatrice_saved_required_flags "${CMAKE_REQUIRED_FLAGS}")
|
|
||||||
set(_cockatrice_saved_required_link_options "${CMAKE_REQUIRED_LINK_OPTIONS}")
|
|
||||||
set(CMAKE_REQUIRED_FLAGS "-O0 -g")
|
|
||||||
set(CMAKE_REQUIRED_LINK_OPTIONS -fprofile-arcs -ftest-coverage)
|
|
||||||
check_cxx_source_compiles("int main() { return 0; }" COCKATRICE_HAVE_COVERAGE_FLAGS)
|
|
||||||
set(CMAKE_REQUIRED_FLAGS "${_cockatrice_saved_required_flags}")
|
|
||||||
set(CMAKE_REQUIRED_LINK_OPTIONS "${_cockatrice_saved_required_link_options}")
|
|
||||||
|
|
||||||
if(NOT COCKATRICE_HAVE_COVERAGE_FLAGS)
|
|
||||||
message(FATAL_ERROR "ENABLE_COVERAGE is on, but this toolchain rejected the coverage flags.")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# -O0 keeps the line mapping honest: at higher optimisation levels the compiler merges and
|
|
||||||
# relocates lines, and the report describes code that no longer exists in that form.
|
|
||||||
add_compile_options(--coverage -O0 -g)
|
|
||||||
add_link_options(--coverage)
|
|
||||||
|
|
||||||
message(STATUS "Coverage instrumentation enabled (--coverage). Report with gcovr over the build dir.")
|
|
||||||
endif()
|
|
||||||
|
|
@ -40,9 +40,7 @@ endif()
|
||||||
if(TEST)
|
if(TEST)
|
||||||
# Union of Qt modules required across all test targets (independent of application targets).
|
# Union of Qt modules required across all test targets (independent of application targets).
|
||||||
# When adding a new test that needs additional Qt modules, add them here rather than in the test's CMakeLists.txt.
|
# When adding a new test that needs additional Qt modules, add them here rather than in the test's CMakeLists.txt.
|
||||||
# Test is QtTest: it brings QAbstractItemModelTester and QSignalSpy, which are how the Qt model
|
set(_TEST_NEEDED Concurrent Network Svg Widgets)
|
||||||
# and signal contracts get verified without standing up a GUI.
|
|
||||||
set(_TEST_NEEDED Concurrent Network Svg Widgets Test)
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
set(REQUIRED_QT_COMPONENTS ${REQUIRED_QT_COMPONENTS} ${_SERVATRICE_NEEDED} ${_COCKATRICE_NEEDED} ${_ORACLE_NEEDED}
|
set(REQUIRED_QT_COMPONENTS ${REQUIRED_QT_COMPONENTS} ${_SERVATRICE_NEEDED} ${_COCKATRICE_NEEDED} ${_ORACLE_NEEDED}
|
||||||
|
|
|
||||||
|
|
@ -304,11 +304,10 @@ SetShellVarContext all
|
||||||
; _?=$INSTDIR keeps the old uninstaller running in place. An NSIS uninstaller
|
; _?=$INSTDIR keeps the old uninstaller running in place. An NSIS uninstaller
|
||||||
; normally copies itself to %TEMP% and runs from there so that it can delete its
|
; normally copies itself to %TEMP% and runs from there so that it can delete its
|
||||||
; own directory, and the process we started exits at once. ExecWait then returns
|
; own directory, and the process we started exits at once. ExecWait then returns
|
||||||
; before the uninstall has run, and the temp copy keeps deleting files - from
|
; before the uninstall has run, and the temp copy's "RMDir /r $INSTDIR" executes
|
||||||
; 3.1.0 and 3.1.1 on disk a blanket "RMDir /r $INSTDIR" - while the section
|
; while the section below is still writing the new payload. _?= disables the copy
|
||||||
; below is still writing the new payload. _?= disables the copy and makes
|
; and makes ExecWait block until the uninstall finishes. It must be the last
|
||||||
; ExecWait block until the uninstall finishes. It must be the last argument
|
; argument and must not be quoted, even when the path contains spaces.
|
||||||
; and must not be quoted, even when the path contains spaces.
|
|
||||||
;
|
;
|
||||||
; --- 32-bit uninstall ---
|
; --- 32-bit uninstall ---
|
||||||
SetRegView 32
|
SetRegView 32
|
||||||
|
|
@ -1108,14 +1107,9 @@ ${AndIf} ${FileExists} "$INSTDIR\portable.dat"
|
||||||
; upgrade portable mode
|
; upgrade portable mode
|
||||||
!insertmacro CockLog "section: portable upgrade purge"
|
!insertmacro CockLog "section: portable upgrade purge"
|
||||||
!insertmacro CockLogListing "before-portable-purge" "*.dll"
|
!insertmacro CockLogListing "before-portable-purge" "*.dll"
|
||||||
; plugins and translations are payload and are rewritten by the payload
|
|
||||||
; below, so removing them here is safe. sounds and themes are payload
|
|
||||||
; too - the default packs are rewritten below as well - but a portable
|
|
||||||
; user's custom sounds and themes live in exactly those folders, so a
|
|
||||||
; recursive purge here would delete user content along with the defaults.
|
|
||||||
; Stale default files from an older release are overwritten or replaced
|
|
||||||
; by the payload, never by a purge.
|
|
||||||
RMDir /r "$INSTDIR\plugins"
|
RMDir /r "$INSTDIR\plugins"
|
||||||
|
RMDir /r "$INSTDIR\sounds"
|
||||||
|
RMDir /r "$INSTDIR\themes"
|
||||||
RMDir /r "$INSTDIR\translations"
|
RMDir /r "$INSTDIR\translations"
|
||||||
Delete "$INSTDIR\uninstall.exe"
|
Delete "$INSTDIR\uninstall.exe"
|
||||||
Delete "$INSTDIR\cockatrice.exe"
|
Delete "$INSTDIR\cockatrice.exe"
|
||||||
|
|
@ -1269,32 +1263,16 @@ Section "un.Application" UnSecApplication
|
||||||
SetShellVarContext all
|
SetShellVarContext all
|
||||||
StrCpy $R1 0
|
StrCpy $R1 0
|
||||||
|
|
||||||
; Record whether this was a portable install before portable.dat is
|
; Record whether this was a portable install before the directory (and the
|
||||||
; removed below.
|
; portable.dat marker inside it) is removed below.
|
||||||
IfFileExists "$INSTDIR\portable.dat" 0 +2
|
IfFileExists "$INSTDIR\portable.dat" 0 +2
|
||||||
StrCpy $R1 1
|
StrCpy $R1 1
|
||||||
|
|
||||||
; Remove only what the installer wrote. The two lists below are generated
|
; Remove the entire application directory so any file that is not part of
|
||||||
; by CPack from the staged install tree: one Delete per installed file and
|
; the installed payload (e.g. build-tree artifacts such as *.dir folders,
|
||||||
; one non-recursive RMDir per installed directory, children first.
|
; *_autogen and *.tlog files from a build) cannot survive between an
|
||||||
; Anything the user put in $INSTDIR - decks, pictures, portable data
|
; uninstall and a fresh reinstall.
|
||||||
; folders - is not on those lists and survives, and $INSTDIR itself is
|
RMDir /r "$INSTDIR"
|
||||||
; only removed when nothing else is left in it. An earlier version of this
|
|
||||||
; section ran a blanket "RMDir /r $INSTDIR" to keep build-tree artifacts
|
|
||||||
; out of a fresh reinstall; that also deleted user decks whenever a user
|
|
||||||
; had pointed their data folders into the install directory, so payload
|
|
||||||
; purity may never be bought at that price again.
|
|
||||||
@CPACK_NSIS_DELETE_FILES@
|
|
||||||
@CPACK_NSIS_DELETE_DIRECTORIES@
|
|
||||||
|
|
||||||
; Written by the installer itself, so absent from the staged payload
|
|
||||||
; above; qdebug.txt is app-generated log output next to the executables.
|
|
||||||
Delete "$INSTDIR\uninstall.exe"
|
|
||||||
Delete "$INSTDIR\portable.dat"
|
|
||||||
Delete "$INSTDIR\qdebug.txt"
|
|
||||||
|
|
||||||
; Succeeds only when no foreign files remain.
|
|
||||||
RMDir "$INSTDIR"
|
|
||||||
|
|
||||||
RMDir /r "$SMPROGRAMS\Cockatrice"
|
RMDir /r "$SMPROGRAMS\Cockatrice"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,109 +0,0 @@
|
||||||
# AddressSanitizer + UndefinedBehaviorSanitizer opt-in, plus per-source opt-out
|
|
||||||
# Input: ENABLE_SANITIZERS
|
|
||||||
# Defines: cockatrice_disable_sanitizers_for
|
|
||||||
#
|
|
||||||
# Included once from the root CMakeLists.txt, immediately after the per-compiler flag
|
|
||||||
# block, so add_compile_options() reaches every library and executable defined below.
|
|
||||||
|
|
||||||
include(CheckCXXSourceCompiles)
|
|
||||||
|
|
||||||
# Opt a set of vendored sources out of instrumentation.
|
|
||||||
#
|
|
||||||
# cockatrice_disable_sanitizers_for(<source> ...)
|
|
||||||
#
|
|
||||||
# Sanitizer findings inside vendored code are not actionable for us, and letting them
|
|
||||||
# through buries our own bugs in noise until everyone learns to skip the job.
|
|
||||||
#
|
|
||||||
# The sources must be named relative to the directory that compiles them. CMake resolves a
|
|
||||||
# source-file property against the directory scope the target was declared in, so calling
|
|
||||||
# this from the root with absolute paths silently does nothing. That is why the call sites
|
|
||||||
# live next to the add_library/add_executable that pull the vendored code in.
|
|
||||||
#
|
|
||||||
# SKIP_PRECOMPILE_HEADERS is not optional. The precompiled header is built with the
|
|
||||||
# sanitizer flags, so it defines __SANITIZE_ADDRESS__; a translation unit compiled with
|
|
||||||
# -fno-sanitize=all does not, and GCC then refuses the PCH with
|
|
||||||
# "not used because __SANITIZE_ADDRESS__ not defined", which is a fatal -Werror=invalid-pch
|
|
||||||
# in the servatrice and oracle builds.
|
|
||||||
function(cockatrice_disable_sanitizers_for)
|
|
||||||
if(NOT ENABLE_SANITIZERS)
|
|
||||||
return()
|
|
||||||
endif()
|
|
||||||
set_source_files_properties(${ARGN} PROPERTIES COMPILE_OPTIONS "-fno-sanitize=all" SKIP_PRECOMPILE_HEADERS ON)
|
|
||||||
endfunction()
|
|
||||||
|
|
||||||
if(ENABLE_SANITIZERS)
|
|
||||||
# Probe a flag the way it will actually be used: on the compile step AND the link step.
|
|
||||||
# check_cxx_compiler_flag() only probes the compile step, which makes every sanitizer
|
|
||||||
# flag look unsupported because linking an ASan object without -fsanitize=address leaves
|
|
||||||
# __asan_init undefined. That is a silent misconfiguration rather than an error, so the
|
|
||||||
# probe has to link.
|
|
||||||
function(cockatrice_probe_flag FLAG VARNAME)
|
|
||||||
set(CMAKE_REQUIRED_FLAGS "${FLAG}")
|
|
||||||
set(CMAKE_REQUIRED_LINK_OPTIONS "${FLAG}")
|
|
||||||
check_cxx_source_compiles("int main() { return 0; }" ${VARNAME})
|
|
||||||
unset(CMAKE_REQUIRED_FLAGS)
|
|
||||||
unset(CMAKE_REQUIRED_LINK_OPTIONS)
|
|
||||||
endfunction()
|
|
||||||
|
|
||||||
set(SANITIZER_FLAGS
|
|
||||||
-fsanitize=address,undefined
|
|
||||||
-fno-omit-frame-pointer
|
|
||||||
# UBSan defaults to recover mode: it prints "runtime error: ..." but the
|
|
||||||
# process exits 0, so gtest/ctest report a pass and CI stays green.
|
|
||||||
# -fno-sanitize-recover makes the first finding abort instead. This is
|
|
||||||
# probed like the other cosmetic flags, but CI also sets
|
|
||||||
# UBSAN_OPTIONS=halt_on_error=1 as the runtime-side guarantee.
|
|
||||||
-fno-sanitize-recover=undefined
|
|
||||||
# Qt builds QObject subclasses into raw storage and writes the vptr afterwards, which
|
|
||||||
# the vptr check reports on essentially every QObject instantiation. The check has no
|
|
||||||
# value for us because nothing outside moc-generated code inherits QObject, so it is
|
|
||||||
# off outright rather than suppressed piecemeal.
|
|
||||||
-fno-sanitize=vptr
|
|
||||||
)
|
|
||||||
|
|
||||||
# MSVC has no ASan/UBSan equivalent, and clang-cl would need /fsanitize=address plus a
|
|
||||||
# matching runtime link, which is a separate piece of work.
|
|
||||||
if(MSVC)
|
|
||||||
message(FATAL_ERROR "ENABLE_SANITIZERS is not supported with MSVC")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# Probe rather than assume, so an older toolchain gets the sanitizers it can support
|
|
||||||
# instead of failing the build on a flag it does not recognize.
|
|
||||||
set(SANITIZER_SUPPORTED_FLAGS)
|
|
||||||
foreach(FLAG ${SANITIZER_FLAGS})
|
|
||||||
string(MAKE_C_IDENTIFIER "COCKATRICE_HAS${FLAG}" _FLAG_VAR)
|
|
||||||
cockatrice_probe_flag("${FLAG}" ${_FLAG_VAR})
|
|
||||||
if(${_FLAG_VAR})
|
|
||||||
list(APPEND SANITIZER_SUPPORTED_FLAGS ${FLAG})
|
|
||||||
else()
|
|
||||||
message(STATUS "Sanitizers: ${FLAG} unsupported by ${CMAKE_CXX_COMPILER_ID}, skipping")
|
|
||||||
endif()
|
|
||||||
endforeach()
|
|
||||||
|
|
||||||
# The whole point of the option is the sanitizers, so an unsupported pair is fatal even
|
|
||||||
# though the cosmetic flags above are allowed to degrade.
|
|
||||||
if(NOT "-fsanitize=address,undefined" IN_LIST SANITIZER_SUPPORTED_FLAGS)
|
|
||||||
message(FATAL_ERROR "ENABLE_SANITIZERS requires -fsanitize=address,undefined, which "
|
|
||||||
"${CMAKE_CXX_COMPILER_ID} does not accept"
|
|
||||||
)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
add_compile_options(${SANITIZER_SUPPORTED_FLAGS})
|
|
||||||
add_link_options(-fsanitize=address,undefined)
|
|
||||||
|
|
||||||
# Recorded so the test tree can label its entries and a plain `make` at the same time
|
|
||||||
# cannot quietly disagree with what CI ran.
|
|
||||||
set(COCKATRICE_SANITIZERS
|
|
||||||
ON
|
|
||||||
CACHE INTERNAL "AddressSanitizer and UBSan are enabled"
|
|
||||||
)
|
|
||||||
else()
|
|
||||||
# ENABLE_SANITIZERS is OFF, but COCKATRICE_SANITIZERS is CACHE INTERNAL, so a
|
|
||||||
# reused build dir (e.g. CI's release-package step reconfiguring the same dir
|
|
||||||
# after the sanitized debug step) would otherwise keep skipping NO_SANITIZE
|
|
||||||
# tests and keep the 300s discovery timeout. Reset it explicitly.
|
|
||||||
set(COCKATRICE_SANITIZERS
|
|
||||||
OFF
|
|
||||||
CACHE INTERNAL "AddressSanitizer and UBSan are enabled" FORCE
|
|
||||||
)
|
|
||||||
endif()
|
|
||||||
|
|
@ -3,53 +3,22 @@
|
||||||
*
|
*
|
||||||
* Safe for every target that links Qt Core, including the headless
|
* Safe for every target that links Qt Core, including the headless
|
||||||
* Servatrice binary. Keep this header free of any widget/gui types.
|
* Servatrice binary. Keep this header free of any widget/gui types.
|
||||||
*
|
|
||||||
* Candidates are chosen from IWYU include statistics: a header earns a
|
|
||||||
* spot when it is not already reachable from the rest of this list and
|
|
||||||
* enough translation units re-parse it. Qt headers are stable across
|
|
||||||
* builds, so the precompiled header rarely invalidates.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <QBasicTimer>
|
#include <QBasicTimer>
|
||||||
#include <QByteArray>
|
#include <QByteArray>
|
||||||
#include <QCommandLineParser>
|
|
||||||
#include <QCoreApplication>
|
|
||||||
#include <QCryptographicHash>
|
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QDir>
|
|
||||||
#include <QElapsedTimer>
|
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QFileInfo>
|
|
||||||
#include <QHash>
|
#include <QHash>
|
||||||
#include <QItemSelectionModel>
|
|
||||||
#include <QJsonArray>
|
|
||||||
#include <QJsonDocument>
|
|
||||||
#include <QJsonObject>
|
|
||||||
#include <QJsonValue>
|
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QLoggingCategory>
|
#include <QLoggingCategory>
|
||||||
#include <QMap>
|
#include <QMap>
|
||||||
#include <QMetaEnum>
|
|
||||||
#include <QMetaObject>
|
#include <QMetaObject>
|
||||||
#include <QMutex>
|
|
||||||
#include <QMutexLocker>
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QPointer>
|
|
||||||
#include <QPropertyAnimation>
|
|
||||||
#include <QRandomGenerator>
|
#include <QRandomGenerator>
|
||||||
#include <QReadWriteLock>
|
|
||||||
#include <QRegularExpression>
|
|
||||||
#include <QSet>
|
|
||||||
#include <QSettings>
|
|
||||||
#include <QSharedPointer>
|
#include <QSharedPointer>
|
||||||
#include <QSortFilterProxyModel>
|
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
#include <QTemporaryDir>
|
|
||||||
#include <QThread>
|
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QUuid>
|
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
#include <QXmlStreamReader>
|
|
||||||
#include <QXmlStreamWriter>
|
|
||||||
|
|
|
||||||
|
|
@ -4,56 +4,27 @@
|
||||||
* Includes the Qt Core precompiled header plus the heavy Gui, Widgets and
|
* Includes the Qt Core precompiled header plus the heavy Gui, Widgets and
|
||||||
* Network layers that virtually every client translation unit re-parses.
|
* Network layers that virtually every client translation unit re-parses.
|
||||||
* Do not use on Servatrice (headless, QT_DONT_USE_QTGUI).
|
* Do not use on Servatrice (headless, QT_DONT_USE_QTGUI).
|
||||||
*
|
|
||||||
* Candidates are chosen from IWYU include statistics: a header earns a
|
|
||||||
* spot when it is not already reachable from the rest of this list and
|
|
||||||
* enough translation units re-parse it. Qt headers are stable across
|
|
||||||
* builds, so the precompiled header rarely invalidates.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "qtcore_pch.h"
|
#include "qtcore_pch.h"
|
||||||
|
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QCheckBox>
|
|
||||||
#include <QComboBox>
|
|
||||||
#include <QCompleter>
|
|
||||||
#include <QDialogButtonBox>
|
|
||||||
#include <QDockWidget>
|
|
||||||
#include <QFileDialog>
|
|
||||||
#include <QFormLayout>
|
|
||||||
#include <QFrame>
|
#include <QFrame>
|
||||||
#include <QGraphicsItem>
|
#include <QGraphicsItem>
|
||||||
#include <QGraphicsScene>
|
#include <QGraphicsScene>
|
||||||
#include <QGraphicsSceneMouseEvent>
|
|
||||||
#include <QGraphicsView>
|
#include <QGraphicsView>
|
||||||
#include <QGridLayout>
|
|
||||||
#include <QGroupBox>
|
|
||||||
#include <QHBoxLayout>
|
|
||||||
#include <QHeaderView>
|
|
||||||
#include <QImage>
|
#include <QImage>
|
||||||
#include <QImageReader>
|
|
||||||
#include <QInputDialog>
|
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QLayout>
|
#include <QLayout>
|
||||||
#include <QListWidget>
|
|
||||||
#include <QMainWindow>
|
#include <QMainWindow>
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
#include <QMessageBox>
|
|
||||||
#include <QNetworkAccessManager>
|
#include <QNetworkAccessManager>
|
||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QPlainTextEdit>
|
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QScrollArea>
|
#include <QScrollArea>
|
||||||
#include <QSpinBox>
|
|
||||||
#include <QSplitter>
|
|
||||||
#include <QStandardItemModel>
|
|
||||||
#include <QTabWidget>
|
#include <QTabWidget>
|
||||||
#include <QTableWidget>
|
|
||||||
#include <QTableWidgetItem>
|
|
||||||
#include <QToolBar>
|
#include <QToolBar>
|
||||||
#include <QToolButton>
|
|
||||||
#include <QTreeWidget>
|
#include <QTreeWidget>
|
||||||
#include <QVBoxLayout>
|
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,6 @@ set(cockatrice_SOURCES
|
||||||
src/interface/widgets/dialogs/dlg_update.cpp
|
src/interface/widgets/dialogs/dlg_update.cpp
|
||||||
src/interface/widgets/dialogs/dlg_view_log.cpp
|
src/interface/widgets/dialogs/dlg_view_log.cpp
|
||||||
src/interface/widgets/dialogs/override_printing_warning.cpp
|
src/interface/widgets/dialogs/override_printing_warning.cpp
|
||||||
src/interface/widgets/dialogs/path_safety_warning.cpp
|
|
||||||
src/interface/widgets/dialogs/settings_view.cpp
|
src/interface/widgets/dialogs/settings_view.cpp
|
||||||
src/interface/widgets/dialogs/tip_of_the_day.cpp
|
src/interface/widgets/dialogs/tip_of_the_day.cpp
|
||||||
src/interface/widgets/deck_share/deck_share_utils.cpp
|
src/interface/widgets/deck_share/deck_share_utils.cpp
|
||||||
|
|
|
||||||
|
|
@ -239,17 +239,6 @@ void ConnectionController::onLoginError(int r,
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case Response::RespTooManyRequests: {
|
|
||||||
QString bannedStr = endTime ? tr("Too many login attempts. Try again after %1.")
|
|
||||||
.arg(QDateTime::fromSecsSinceEpoch(endTime).toString())
|
|
||||||
: tr("Too many login attempts. Please try again later.");
|
|
||||||
if (!reasonStr.isEmpty()) {
|
|
||||||
bannedStr.append("\n\n" + reasonStr);
|
|
||||||
}
|
|
||||||
QMessageBox::critical(dialogParent, tr("Error"), bannedStr);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case Response::RespWrongPassword: {
|
case Response::RespWrongPassword: {
|
||||||
QMessageBox::critical(dialogParent, tr("Error"),
|
QMessageBox::critical(dialogParent, tr("Error"),
|
||||||
tr("Incorrect username or password. "
|
tr("Incorrect username or password. "
|
||||||
|
|
@ -376,15 +365,12 @@ void ConnectionController::onRegisterError(int r, QString reasonStr, quint32 end
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Response::RespTooManyRequests: {
|
case Response::RespTooManyRequests: {
|
||||||
QString deniedStr =
|
QMessageBox::critical(dialogParent, tr("Registration denied"),
|
||||||
endTime ? tr("Too many registration attempts. Try again after %1.")
|
tr("It appears you are attempting to register a new account on this server yet you "
|
||||||
.arg(QDateTime::fromSecsSinceEpoch(endTime).toString())
|
"already have an account registered with the email provided. This server "
|
||||||
: tr("It appears you are attempting to register a new account on this server yet you "
|
"restricts the number of accounts a user can register per address. Please "
|
||||||
"already have an account registered with the email provided. This server "
|
"contact the server operator for further assistance or to obtain your "
|
||||||
"restricts the number of accounts a user can register per address. Please "
|
"credential information."));
|
||||||
"contact the server operator for further assistance or to obtain your "
|
|
||||||
"credential information.");
|
|
||||||
QMessageBox::critical(dialogParent, tr("Registration denied"), deniedStr);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Response::RespPasswordTooShort: {
|
case Response::RespPasswordTooShort: {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QFutureWatcher>
|
|
||||||
#include <QIODevice>
|
#include <QIODevice>
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QLocale>
|
#include <QLocale>
|
||||||
|
|
@ -178,15 +177,7 @@ bool SpoilerBackgroundUpdater::saveDownloadedFile(QByteArray data)
|
||||||
|
|
||||||
// Data written, so reload the card database
|
// Data written, so reload the card database
|
||||||
qCInfo(SpoilerBackgroundUpdaterLog) << "Spoiler Service Data Written";
|
qCInfo(SpoilerBackgroundUpdaterLog) << "Spoiler Service Data Written";
|
||||||
// Spoilers can introduce sets the config has never seen; the loader never
|
const auto reloadOk = QtConcurrent::run([] { CardDatabaseManager::getInstance()->loadCardDatabases(); });
|
||||||
// runs checkUnknownSets() itself, so run it once the finished snapshot has
|
|
||||||
// been swapped in.
|
|
||||||
auto *watcher = new QFutureWatcher<void>(this);
|
|
||||||
connect(watcher, &QFutureWatcher<void>::finished, this, [watcher] {
|
|
||||||
watcher->deleteLater();
|
|
||||||
CardDatabaseManager::getInstance()->checkUnknownSets();
|
|
||||||
});
|
|
||||||
watcher->setFuture(QtConcurrent::run([] { CardDatabaseManager::getInstance()->loadCardDatabases(); }));
|
|
||||||
|
|
||||||
// If the user has notifications enabled, let them know
|
// If the user has notifications enabled, let them know
|
||||||
// when the database was last updated
|
// when the database was last updated
|
||||||
|
|
|
||||||
|
|
@ -133,10 +133,6 @@ SettingsCache::SettingsCache()
|
||||||
// Migrate settings from global.ini to per-class INI files (runs at most once)
|
// Migrate settings from global.ini to per-class INI files (runs at most once)
|
||||||
SettingsMigration::migrateSettingsFromGlobalIni(settingsPath);
|
SettingsMigration::migrateSettingsFromGlobalIni(settingsPath);
|
||||||
|
|
||||||
// Repair gamefilters.ini keys renamed to camelCase by #7084 for profiles that migrated
|
|
||||||
// before that rename (idempotent, safe to run on every startup, portable builds included).
|
|
||||||
SettingsMigration::migrateGameFiltersToCamelCase(settingsPath);
|
|
||||||
|
|
||||||
shortcutsSettings = new ShortcutsSettings(settingsPath, this);
|
shortcutsSettings = new ShortcutsSettings(settingsPath, this);
|
||||||
cardDatabaseSettings = new CardDatabaseSettings(settingsPath, this);
|
cardDatabaseSettings = new CardDatabaseSettings(settingsPath, this);
|
||||||
serversSettings = new ServersSettings(settingsPath, this);
|
serversSettings = new ServersSettings(settingsPath, this);
|
||||||
|
|
|
||||||
|
|
@ -277,6 +277,8 @@ void GameEventHandler::eventSpectatorLeave(const Event_Leave &event,
|
||||||
emit spectatorLeft(eventPlayerId);
|
emit spectatorLeft(eventPlayerId);
|
||||||
|
|
||||||
game->getPlayerManager()->removeSpectator(eventPlayerId);
|
game->getPlayerManager()->removeSpectator(eventPlayerId);
|
||||||
|
|
||||||
|
emitUserEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameEventHandler::eventGameStateChanged(const Event_GameStateChanged &event,
|
void GameEventHandler::eventGameStateChanged(const Event_GameStateChanged &event,
|
||||||
|
|
@ -463,9 +465,9 @@ void GameEventHandler::eventJoin(const Event_Join &event, int /*eventPlayerId*/,
|
||||||
PlayerLogic *newPlayer = game->getPlayerManager()->addPlayer(playerId, playerInfo.user_info());
|
PlayerLogic *newPlayer = game->getPlayerManager()->addPlayer(playerId, playerInfo.user_info());
|
||||||
emit logJoinPlayer(newPlayer);
|
emit logJoinPlayer(newPlayer);
|
||||||
emit playerJoined(playerInfo);
|
emit playerJoined(playerInfo);
|
||||||
|
|
||||||
emitUserEvent();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emitUserEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString GameEventHandler::getLeaveReason(Event_Leave::LeaveReason reason)
|
QString GameEventHandler::getLeaveReason(Event_Leave::LeaveReason reason)
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@
|
||||||
#include "libcockatrice/card/printing/exact_card.h"
|
#include "libcockatrice/card/printing/exact_card.h"
|
||||||
#include "libcockatrice/utility/card_ref.h"
|
#include "libcockatrice/utility/card_ref.h"
|
||||||
|
|
||||||
#include <QApplication>
|
|
||||||
#include <QBrush>
|
#include <QBrush>
|
||||||
#include <QChar>
|
#include <QChar>
|
||||||
#include <QCursor>
|
#include <QCursor>
|
||||||
|
|
@ -29,7 +28,6 @@
|
||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
#include <QPointF>
|
#include <QPointF>
|
||||||
#include <QRectF>
|
#include <QRectF>
|
||||||
#include <QTimer>
|
|
||||||
#include <QTransform>
|
#include <QTransform>
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
|
|
@ -44,7 +42,7 @@ class QSizeF;
|
||||||
|
|
||||||
AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef, PlayerLogic *_owner, int _id)
|
AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef, PlayerLogic *_owner, int _id)
|
||||||
: ArrowTarget(_owner, parent), id(_id), cardRef(cardRef), tapped(false), facedown(false), tapAngle(0),
|
: ArrowTarget(_owner, parent), id(_id), cardRef(cardRef), tapped(false), facedown(false), tapAngle(0),
|
||||||
bgColor(Qt::transparent), isHovered(false), realZValue(0), pendingSelectionCollapse(false)
|
bgColor(Qt::transparent), isHovered(false), realZValue(0)
|
||||||
{
|
{
|
||||||
setCursor(Qt::OpenHandCursor);
|
setCursor(Qt::OpenHandCursor);
|
||||||
setFlag(ItemIsSelectable);
|
setFlag(ItemIsSelectable);
|
||||||
|
|
@ -346,16 +344,6 @@ void AbstractCardItem::setFaceDown(bool _facedown)
|
||||||
|
|
||||||
void AbstractCardItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
|
void AbstractCardItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
|
||||||
{
|
{
|
||||||
cancelPendingSelectionCollapse();
|
|
||||||
if (scene() != nullptr) {
|
|
||||||
// A new press starts a new gesture: no card may collapse the selection out from under it afterwards.
|
|
||||||
for (QGraphicsItem *selectedItem : scene()->selectedItems()) {
|
|
||||||
auto *selectedCard = qgraphicsitem_cast<AbstractCardItem *>(selectedItem);
|
|
||||||
if (selectedCard != nullptr && selectedCard != this) {
|
|
||||||
selectedCard->cancelPendingSelectionCollapse();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ((event->modifiers() & Qt::AltModifier) && event->button() == Qt::LeftButton) {
|
if ((event->modifiers() & Qt::AltModifier) && event->button() == Qt::LeftButton) {
|
||||||
emit cardShiftClicked(cardRef.name);
|
emit cardShiftClicked(cardRef.name);
|
||||||
} else if ((event->modifiers() & Qt::ControlModifier)) {
|
} else if ((event->modifiers() & Qt::ControlModifier)) {
|
||||||
|
|
@ -363,10 +351,6 @@ void AbstractCardItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
|
||||||
} else if (!isSelected()) {
|
} else if (!isSelected()) {
|
||||||
scene()->clearSelection();
|
scene()->clearSelection();
|
||||||
setSelected(true);
|
setSelected(true);
|
||||||
} else if (event->button() == Qt::LeftButton && scene()->selectedItems().count() > 1) {
|
|
||||||
// Part of a multi-selection; collapse only if this turns out to be a click rather than a drag,
|
|
||||||
// so that dragging the whole selection keeps it intact.
|
|
||||||
pendingSelectionCollapse = true;
|
|
||||||
}
|
}
|
||||||
if (event->button() == Qt::LeftButton) {
|
if (event->button() == Qt::LeftButton) {
|
||||||
setCursor(Qt::ClosedHandCursor);
|
setCursor(Qt::ClosedHandCursor);
|
||||||
|
|
@ -378,32 +362,6 @@ void AbstractCardItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
|
||||||
|
|
||||||
void AbstractCardItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
void AbstractCardItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
||||||
{
|
{
|
||||||
if (pendingSelectionCollapse) {
|
|
||||||
pendingSelectionCollapse = false;
|
|
||||||
bool dragged = (event->screenPos() - event->buttonDownScreenPos(event->button())).manhattanLength() >=
|
|
||||||
2 * QApplication::startDragDistance();
|
|
||||||
if (!dragged) {
|
|
||||||
if (doubleClickActsOnSelection()) {
|
|
||||||
// A double-click may still arrive and must act on the whole multi-selection, so collapse only
|
|
||||||
// once the double-click window has passed without one. The generation guard (and the receiver
|
|
||||||
// context) drops the collapse if the item is gone, deselected, or a double-click cancelled it;
|
|
||||||
// the count guard drops it if the selection changed meanwhile (rubber band, ctrl-toggle).
|
|
||||||
const int generation = ++selectionCollapseGeneration;
|
|
||||||
const int armedSelectionCount = scene()->selectedItems().count();
|
|
||||||
QTimer::singleShot(QApplication::doubleClickInterval(), this, [this, generation, armedSelectionCount] {
|
|
||||||
if (generation == selectionCollapseGeneration && scene() != nullptr && isSelected() &&
|
|
||||||
scene()->selectedItems().count() == armedSelectionCount && armedSelectionCount > 1) {
|
|
||||||
scene()->clearSelection();
|
|
||||||
setSelected(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
scene()->clearSelection();
|
|
||||||
setSelected(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event->button() == Qt::MiddleButton) {
|
if (event->button() == Qt::MiddleButton) {
|
||||||
emit deleteCardInfoPopup(cardRef.name);
|
emit deleteCardInfoPopup(cardRef.name);
|
||||||
}
|
}
|
||||||
|
|
@ -412,17 +370,6 @@ void AbstractCardItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
||||||
event->accept();
|
event->accept();
|
||||||
}
|
}
|
||||||
|
|
||||||
void AbstractCardItem::cancelPendingSelectionCollapse()
|
|
||||||
{
|
|
||||||
pendingSelectionCollapse = false;
|
|
||||||
++selectionCollapseGeneration;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool AbstractCardItem::doubleClickActsOnSelection() const
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void AbstractCardItem::processHoverEvent()
|
void AbstractCardItem::processHoverEvent()
|
||||||
{
|
{
|
||||||
emit hovered(this);
|
emit hovered(this);
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,6 @@ protected:
|
||||||
private:
|
private:
|
||||||
bool isHovered;
|
bool isHovered;
|
||||||
qreal realZValue;
|
qreal realZValue;
|
||||||
bool pendingSelectionCollapse;
|
|
||||||
int selectionCollapseGeneration = 0; ///< Guards the deferred collapse timer against stale firings
|
|
||||||
private slots:
|
private slots:
|
||||||
void pixmapUpdated();
|
void pixmapUpdated();
|
||||||
|
|
||||||
|
|
@ -132,8 +130,6 @@ public:
|
||||||
}
|
}
|
||||||
void setFaceDown(bool _facedown);
|
void setFaceDown(bool _facedown);
|
||||||
void processHoverEvent();
|
void processHoverEvent();
|
||||||
/** @brief Drop a pending collapse and disarm any deferred collapse timer. */
|
|
||||||
void cancelPendingSelectionCollapse();
|
|
||||||
void deleteCardInfoPopup()
|
void deleteCardInfoPopup()
|
||||||
{
|
{
|
||||||
emit deleteCardInfoPopup(cardRef.name);
|
emit deleteCardInfoPopup(cardRef.name);
|
||||||
|
|
@ -148,16 +144,6 @@ protected:
|
||||||
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;
|
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;
|
||||||
QVariant itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value) override;
|
QVariant itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value) override;
|
||||||
void cacheBgColor();
|
void cacheBgColor();
|
||||||
/**
|
|
||||||
* @brief Whether a double-click on this item acts on the whole selection.
|
|
||||||
*
|
|
||||||
* When true, a release without drag defers the multi-selection collapse
|
|
||||||
* until the double-click window passes, so a following double-click still
|
|
||||||
* sees every selected card. Subclasses that handle double-clicks on the
|
|
||||||
* selection must return true here and call
|
|
||||||
* cancelPendingSelectionCollapse() from their double-click handler.
|
|
||||||
*/
|
|
||||||
virtual bool doubleClickActsOnSelection() const;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -518,9 +518,6 @@ void CardItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
||||||
|
|
||||||
void CardItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
|
void CardItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
|
||||||
{
|
{
|
||||||
// The preceding release deferred its multi-selection collapse for exactly this: the double-click plays
|
|
||||||
// every selected card, so the collapse must not run.
|
|
||||||
cancelPendingSelectionCollapse();
|
|
||||||
if ((event->modifiers() != Qt::AltModifier) && (event->buttons() == Qt::LeftButton) &&
|
if ((event->modifiers() != Qt::AltModifier) && (event->buttons() == Qt::LeftButton) &&
|
||||||
(SettingsCache::instance().userInterface().getDoubleClickToPlay())) {
|
(SettingsCache::instance().userInterface().getDoubleClickToPlay())) {
|
||||||
handleClickedToPlay(event->modifiers().testFlag(Qt::ShiftModifier));
|
handleClickedToPlay(event->modifiers().testFlag(Qt::ShiftModifier));
|
||||||
|
|
@ -528,11 +525,6 @@ void CardItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
|
||||||
event->accept();
|
event->accept();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CardItem::doubleClickActsOnSelection() const
|
|
||||||
{
|
|
||||||
return SettingsCache::instance().userInterface().getDoubleClickToPlay();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool CardItem::animationEvent()
|
bool CardItem::animationEvent()
|
||||||
{
|
{
|
||||||
int rotation = ROTATION_DEGREES_PER_FRAME;
|
int rotation = ROTATION_DEGREES_PER_FRAME;
|
||||||
|
|
|
||||||
|
|
@ -175,7 +175,6 @@ protected:
|
||||||
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;
|
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;
|
||||||
void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override;
|
void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override;
|
||||||
QVariant itemChange(GraphicsItemChange change, const QVariant &value) override;
|
QVariant itemChange(GraphicsItemChange change, const QVariant &value) override;
|
||||||
bool doubleClickActsOnSelection() const override;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -182,15 +182,6 @@ void DeckView::mouseDoubleClickEvent(QMouseEvent *event)
|
||||||
QList<MoveCard_ToZone> result;
|
QList<MoveCard_ToZone> result;
|
||||||
QList<QGraphicsItem *> sel = scene()->selectedItems();
|
QList<QGraphicsItem *> sel = scene()->selectedItems();
|
||||||
|
|
||||||
// The double-click goes to the view, not the card, so cancel any pending
|
|
||||||
// selection collapse on the cards before reading the selection.
|
|
||||||
for (auto *item : sel) {
|
|
||||||
if (auto *c = dynamic_cast<DeckViewCard *>(item)) {
|
|
||||||
c->cancelPendingSelectionCollapse();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sel = scene()->selectedItems();
|
|
||||||
|
|
||||||
for (int i = 0; i < sel.size(); i++) {
|
for (int i = 0; i < sel.size(); i++) {
|
||||||
auto *c = static_cast<DeckViewCard *>(sel.at(i));
|
auto *c = static_cast<DeckViewCard *>(sel.at(i));
|
||||||
auto *zone = static_cast<DeckViewCardContainer *>(c->parentItem());
|
auto *zone = static_cast<DeckViewCardContainer *>(c->parentItem());
|
||||||
|
|
|
||||||
|
|
@ -49,10 +49,6 @@ public:
|
||||||
{
|
{
|
||||||
return originZone;
|
return originZone;
|
||||||
}
|
}
|
||||||
bool doubleClickActsOnSelection() const override
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
|
void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
|
||||||
|
|
|
||||||
|
|
@ -127,7 +127,6 @@ void GameView::startRubberBand(const QPointF &_selectionOrigin)
|
||||||
selectionOrigin = _selectionOrigin;
|
selectionOrigin = _selectionOrigin;
|
||||||
previousBandRect = QRect();
|
previousBandRect = QRect();
|
||||||
rubberBand->setGeometry(QRect(mapFromScene(selectionOrigin), QSize(0, 0)));
|
rubberBand->setGeometry(QRect(mapFromScene(selectionOrigin), QSize(0, 0)));
|
||||||
rubberBand->raise();
|
|
||||||
rubberBand->show();
|
rubberBand->show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
#include "path_safety_warning.h"
|
|
||||||
|
|
||||||
#include "../../client/settings/cache_settings.h"
|
|
||||||
|
|
||||||
#include <QCoreApplication>
|
|
||||||
#include <QMessageBox>
|
|
||||||
#include <QObject>
|
|
||||||
#include <QString>
|
|
||||||
#include <libcockatrice/settings/paths_settings.h>
|
|
||||||
|
|
||||||
class QWidget;
|
|
||||||
|
|
||||||
bool PathSafetyWarning::confirmOutsideProgramDir(QWidget *parent, const QString &path)
|
|
||||||
{
|
|
||||||
if (SettingsCache::instance().getIsPortableBuild()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!PathsSettings::isInsideDir(path, QCoreApplication::applicationDirPath())) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
QMessageBox box(QMessageBox::Warning, QObject::tr("Path inside the program directory"),
|
|
||||||
QObject::tr("'%1' is inside the Cockatrice program directory.\n\n"
|
|
||||||
"Files stored there can be deleted by an update or by uninstalling "
|
|
||||||
"Cockatrice, so this change was not applied. Choose a location outside "
|
|
||||||
"the program directory.")
|
|
||||||
.arg(path),
|
|
||||||
QMessageBox::Ok, parent);
|
|
||||||
box.setTextFormat(Qt::PlainText);
|
|
||||||
box.exec();
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
#ifndef COCKATRICE_PATH_SAFETY_WARNING_H
|
|
||||||
#define COCKATRICE_PATH_SAFETY_WARNING_H
|
|
||||||
|
|
||||||
class QString;
|
|
||||||
class QWidget;
|
|
||||||
|
|
||||||
namespace PathSafetyWarning
|
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Warns when the user tries to configure a data path inside the program directory.
|
|
||||||
*
|
|
||||||
* Shows a message box explaining that files kept there can be deleted by an
|
|
||||||
* update or by uninstalling Cockatrice. Portable builds are always exempt:
|
|
||||||
* their data lives beside the executables by design.
|
|
||||||
*
|
|
||||||
* @param parent The parent widget
|
|
||||||
* @param path The path the user is trying to configure
|
|
||||||
* @return true when the path is outside the program directory and safe to apply
|
|
||||||
*/
|
|
||||||
bool confirmOutsideProgramDir(QWidget *parent, const QString &path);
|
|
||||||
|
|
||||||
} // namespace PathSafetyWarning
|
|
||||||
|
|
||||||
#endif // COCKATRICE_PATH_SAFETY_WARNING_H
|
|
||||||
|
|
@ -17,9 +17,7 @@ struct GameFilterConfigs
|
||||||
static constexpr int DEFAULT_MAX_PLAYERS_MAX = 99;
|
static constexpr int DEFAULT_MAX_PLAYERS_MAX = 99;
|
||||||
|
|
||||||
bool hideBuddiesOnlyGames = false;
|
bool hideBuddiesOnlyGames = false;
|
||||||
// Defaults to true to match GameFiltersSettings::isHideIgnoredUserGames(), so freshly loaded
|
bool hideIgnoredUserGames = false;
|
||||||
// filters compare equal to a default-constructed config.
|
|
||||||
bool hideIgnoredUserGames = true;
|
|
||||||
bool hideFullGames = false;
|
bool hideFullGames = false;
|
||||||
bool hideGamesThatStarted = false;
|
bool hideGamesThatStarted = false;
|
||||||
bool hidePasswordProtectedGames = false;
|
bool hidePasswordProtectedGames = false;
|
||||||
|
|
|
||||||
|
|
@ -113,17 +113,6 @@ GameSelector::GameSelector(AbstractClient *_client,
|
||||||
quickFilterToolBar->setVisible(
|
quickFilterToolBar->setVisible(
|
||||||
SettingsCache::instance().userInterface().getShowGameSelectorFilterToolbar());
|
SettingsCache::instance().userInterface().getShowGameSelectorFilterToolbar());
|
||||||
});
|
});
|
||||||
// The quick-filter toolbar only updates the in-memory model, so persist every change.
|
|
||||||
// Otherwise each new GameSelector (room switch, reconnect, restart) would revert to stale
|
|
||||||
// saved values and e.g. hide full/started games the user had unchecked. The save is
|
|
||||||
// debounced so typing in the search field does not rewrite the settings file on every
|
|
||||||
// keystroke; a pending save is flushed in the destructor.
|
|
||||||
filterSaveDebounceTimer.setSingleShot(true);
|
|
||||||
filterSaveDebounceTimer.setInterval(500);
|
|
||||||
connect(&filterSaveDebounceTimer, &QTimer::timeout, this,
|
|
||||||
[this] { gameListProxyModel->saveFilterParameters(gameTypeMap); });
|
|
||||||
connect(gameListProxyModel, &GamesProxyModel::filtersChanged, this,
|
|
||||||
[this] { filterSaveDebounceTimer.start(); });
|
|
||||||
} else {
|
} else {
|
||||||
quickFilterToolBar = nullptr;
|
quickFilterToolBar = nullptr;
|
||||||
}
|
}
|
||||||
|
|
@ -198,18 +187,6 @@ GameSelector::GameSelector(AbstractClient *_client,
|
||||||
connect(client, &AbstractClient::removeFromListEventReceived, this, &GameSelector::processRemoveFromListEvent);
|
connect(client, &AbstractClient::removeFromListEventReceived, this, &GameSelector::processRemoveFromListEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
GameSelector::~GameSelector()
|
|
||||||
{
|
|
||||||
// Nothing else persists quick-filter toolbar edits: the toolbar only updates the in-memory model,
|
|
||||||
// and the debounced timer above is the sole writer for that path (actSetFilter/actClearFilter save
|
|
||||||
// synchronously, but the toolbar does not). Flush a pending debounced save so edits made within
|
|
||||||
// the debounce interval before destruction are not lost on room switch, reconnect, or close.
|
|
||||||
if (filterSaveDebounceTimer.isActive()) {
|
|
||||||
filterSaveDebounceTimer.stop();
|
|
||||||
gameListProxyModel->saveFilterParameters(gameTypeMap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void GameSelector::ignoreListReceived(const QList<ServerInfo_User> &)
|
void GameSelector::ignoreListReceived(const QList<ServerInfo_User> &)
|
||||||
{
|
{
|
||||||
gameListProxyModel->refresh();
|
gameListProxyModel->refresh();
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
#include "game_type_map.h"
|
#include "game_type_map.h"
|
||||||
|
|
||||||
#include <QGroupBox>
|
#include <QGroupBox>
|
||||||
#include <QTimer>
|
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
// IWYU pragma: keep
|
// IWYU pragma: keep
|
||||||
// ServerInfo_User appears by value and inside QList in slot signatures, so the
|
// ServerInfo_User appears by value and inside QList in slot signatures, so the
|
||||||
|
|
@ -149,9 +148,6 @@ private:
|
||||||
const bool showFilters; /**< Determines whether filter buttons are displayed. */
|
const bool showFilters; /**< Determines whether filter buttons are displayed. */
|
||||||
GameTypeMap gameTypeMap; /**< Mapping of game types for the current room. */
|
GameTypeMap gameTypeMap; /**< Mapping of game types for the current room. */
|
||||||
|
|
||||||
QTimer filterSaveDebounceTimer; /**< Delays persisting quick-filter edits so typing does not rewrite the settings
|
|
||||||
file on every keystroke. */
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Updates the widget title to reflect the current number of displayed games.
|
* @brief Updates the widget title to reflect the current number of displayed games.
|
||||||
*
|
*
|
||||||
|
|
@ -215,7 +211,6 @@ public:
|
||||||
const bool restoresettings,
|
const bool restoresettings,
|
||||||
const bool _showfilters,
|
const bool _showfilters,
|
||||||
QWidget *parent = nullptr);
|
QWidget *parent = nullptr);
|
||||||
~GameSelector() override;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Updates UI text for translation/localization.
|
* @brief Updates UI text for translation/localization.
|
||||||
|
|
|
||||||
|
|
@ -523,9 +523,6 @@ bool GamesProxyModel::filterAcceptsRow(int sourceRow) const
|
||||||
|
|
||||||
void GamesProxyModel::refresh()
|
void GamesProxyModel::refresh()
|
||||||
{
|
{
|
||||||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 9, 0))
|
|
||||||
beginFilterChange();
|
|
||||||
#endif
|
|
||||||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 10, 0))
|
#if (QT_VERSION >= QT_VERSION_CHECK(6, 10, 0))
|
||||||
endFilterChange(QSortFilterProxyModel::Direction::Rows);
|
endFilterChange(QSortFilterProxyModel::Direction::Rows);
|
||||||
#else
|
#else
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@
|
||||||
#include <QGroupBox>
|
#include <QGroupBox>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QPushButton>
|
|
||||||
#include <QSharedPointer>
|
#include <QSharedPointer>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
|
|
@ -85,9 +84,10 @@ AppearanceSettingsPage::AppearanceSettingsPage()
|
||||||
const QString currentStyle = cfg.styleName;
|
const QString currentStyle = cfg.styleName;
|
||||||
const int styleSeedIdx = currentStyle.isEmpty() ? 0 : styleCombo.findData(currentStyle);
|
const int styleSeedIdx = currentStyle.isEmpty() ? 0 : styleCombo.findData(currentStyle);
|
||||||
styleCombo.setCurrentIndex(styleSeedIdx >= 0 ? styleSeedIdx : 0);
|
styleCombo.setCurrentIndex(styleSeedIdx >= 0 ? styleSeedIdx : 0);
|
||||||
previousStyle = styleCombo.currentData().toString();
|
|
||||||
|
|
||||||
connect(&styleCombo, &QComboBox::currentIndexChanged, this, &AppearanceSettingsPage::styleComboChanged);
|
connect(&styleCombo, &QComboBox::currentIndexChanged, this,
|
||||||
|
[this] { themeManager->setStyleName(styleCombo.currentData().toString()); });
|
||||||
|
connect(&styleCombo, &QComboBox::currentIndexChanged, this, &AppearanceSettingsPage::updateStyleWarning);
|
||||||
|
|
||||||
connect(themeManager, &ThemeManager::themeChanged, this, [this, dirPath] {
|
connect(themeManager, &ThemeManager::themeChanged, this, [this, dirPath] {
|
||||||
const QString newDir = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName());
|
const QString newDir = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName());
|
||||||
|
|
@ -104,7 +104,6 @@ AppearanceSettingsPage::AppearanceSettingsPage()
|
||||||
const int styleIdx = currentStyle.isEmpty() ? 0 : styleCombo.findData(currentStyle);
|
const int styleIdx = currentStyle.isEmpty() ? 0 : styleCombo.findData(currentStyle);
|
||||||
styleCombo.setCurrentIndex(styleIdx >= 0 ? styleIdx : 0);
|
styleCombo.setCurrentIndex(styleIdx >= 0 ? styleIdx : 0);
|
||||||
styleCombo.blockSignals(false);
|
styleCombo.blockSignals(false);
|
||||||
previousStyle = styleCombo.currentData().toString();
|
|
||||||
|
|
||||||
updateStyleWarning();
|
updateStyleWarning();
|
||||||
});
|
});
|
||||||
|
|
@ -468,88 +467,12 @@ void AppearanceSettingsPage::updateHomeTabSettingsVisibility()
|
||||||
homeTabDisplayCardNameCheckBox.setVisible(visible);
|
homeTabDisplayCardNameCheckBox.setVisible(visible);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool isBrokenStyle(const QString &style)
|
|
||||||
{
|
|
||||||
return style.compare("windows11", Qt::CaseInsensitive) == 0 ||
|
|
||||||
style.compare("windowsvista", Qt::CaseInsensitive) == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int findFusionIndex(const QComboBox &combo)
|
|
||||||
{
|
|
||||||
int fusionIdx = combo.findData(QStringLiteral("Fusion"));
|
|
||||||
if (fusionIdx >= 0) {
|
|
||||||
return fusionIdx;
|
|
||||||
}
|
|
||||||
for (int i = 0; i < combo.count(); ++i) {
|
|
||||||
if (combo.itemData(i).toString().compare(QStringLiteral("Fusion"), Qt::CaseInsensitive) == 0) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
void AppearanceSettingsPage::updateStyleWarning()
|
void AppearanceSettingsPage::updateStyleWarning()
|
||||||
{
|
{
|
||||||
styleWarningLabel.setVisible(isBrokenStyle(styleCombo.currentData().toString()));
|
const QString style = styleCombo.currentData().toString();
|
||||||
}
|
const bool broken =
|
||||||
|
style.compare("windows11", Qt::CaseInsensitive) == 0 || style.compare("windowsvista", Qt::CaseInsensitive) == 0;
|
||||||
void AppearanceSettingsPage::styleComboChanged(int index)
|
styleWarningLabel.setVisible(broken);
|
||||||
{
|
|
||||||
if (index < 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QString newStyle = styleCombo.itemData(index).toString();
|
|
||||||
if (!isBrokenStyle(newStyle)) {
|
|
||||||
themeManager->setStyleName(newStyle);
|
|
||||||
previousStyle = newStyle;
|
|
||||||
updateStyleWarning();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QMessageBox msgBox(this);
|
|
||||||
msgBox.setWindowTitle(tr("Broken style"));
|
|
||||||
msgBox.setIcon(QMessageBox::Warning);
|
|
||||||
msgBox.setText(tr("The \"%1\" style is broken and can break the game view: dragging cards across zones can "
|
|
||||||
"shrink the board to a tiny grey window that can only be fixed by rejoining the game.")
|
|
||||||
.arg(newStyle));
|
|
||||||
msgBox.setInformativeText(
|
|
||||||
tr("It can also render controls such as combo boxes unreadable, especially in dark mode.\n\n"
|
|
||||||
"Fusion is the recommended style on every platform. Switch to Fusion instead?"));
|
|
||||||
|
|
||||||
int fusionIdx = findFusionIndex(styleCombo);
|
|
||||||
QPushButton *fusionButton = nullptr;
|
|
||||||
if (fusionIdx >= 0) {
|
|
||||||
fusionButton = msgBox.addButton(tr("Use Fusion instead"), QMessageBox::AcceptRole);
|
|
||||||
}
|
|
||||||
QPushButton *keepButton = msgBox.addButton(tr("Use %1 anyway").arg(newStyle), QMessageBox::DestructiveRole);
|
|
||||||
msgBox.addButton(QMessageBox::Cancel);
|
|
||||||
if (fusionButton != nullptr) {
|
|
||||||
msgBox.setDefaultButton(fusionButton);
|
|
||||||
} else {
|
|
||||||
msgBox.setDefaultButton(keepButton);
|
|
||||||
}
|
|
||||||
|
|
||||||
msgBox.exec();
|
|
||||||
|
|
||||||
const QAbstractButton *clicked = msgBox.clickedButton();
|
|
||||||
if (fusionButton != nullptr && clicked == fusionButton) {
|
|
||||||
styleCombo.setCurrentIndex(fusionIdx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (clicked == keepButton) {
|
|
||||||
themeManager->setStyleName(newStyle);
|
|
||||||
previousStyle = newStyle;
|
|
||||||
updateStyleWarning();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cancel: revert to the previously applied style without re-prompting
|
|
||||||
styleCombo.blockSignals(true);
|
|
||||||
const int previousIdx = styleCombo.findData(previousStyle);
|
|
||||||
styleCombo.setCurrentIndex(previousIdx >= 0 ? previousIdx : 0);
|
|
||||||
styleCombo.blockSignals(false);
|
|
||||||
updateStyleWarning();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void AppearanceSettingsPage::showShortcutsChanged(QT_STATE_CHANGED_T value)
|
void AppearanceSettingsPage::showShortcutsChanged(QT_STATE_CHANGED_T value)
|
||||||
|
|
@ -615,10 +538,9 @@ void AppearanceSettingsPage::retranslateUi()
|
||||||
styleComboLabel.setText(tr("Active theme style:"));
|
styleComboLabel.setText(tr("Active theme style:"));
|
||||||
styleCombo.setToolTip(tr("Qt widget style saved to this theme (\"System\" lets the application decide)"));
|
styleCombo.setToolTip(tr("Qt widget style saved to this theme (\"System\" lets the application decide)"));
|
||||||
styleWarningLabel.setText(
|
styleWarningLabel.setText(
|
||||||
tr("Warning: the Windows 11 and Windows Vista styles are broken and can break the game view (dragging "
|
tr("Warning: the Windows 11 and Windows Vista styles are buggy and can render controls such as combo boxes "
|
||||||
"cards can shrink the board to a tiny grey window that requires rejoining to fix), and can render "
|
"unreadable, especially in dark mode. Fusion is the recommended style on every platform: it is Qt's "
|
||||||
"controls such as combo boxes unreadable, especially in dark mode. Fusion is the recommended style on "
|
"cross-platform style and the one Cockatrice is developed against."));
|
||||||
"every platform: it is Qt's cross-platform style and the one Cockatrice is developed against."));
|
|
||||||
updateStyleWarning();
|
updateStyleWarning();
|
||||||
editPaletteButton.setText(tr("Edit theme palette"));
|
editPaletteButton.setText(tr("Edit theme palette"));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ private slots:
|
||||||
void editPalette();
|
void editPalette();
|
||||||
void updateHomeTabSettingsVisibility();
|
void updateHomeTabSettingsVisibility();
|
||||||
void updateStyleWarning();
|
void updateStyleWarning();
|
||||||
void styleComboChanged(int index);
|
|
||||||
void showShortcutsChanged(QT_STATE_CHANGED_T enabled);
|
void showShortcutsChanged(QT_STATE_CHANGED_T enabled);
|
||||||
void overrideAllCardArtWithPersonalPreferenceToggled(QT_STATE_CHANGED_T enabled);
|
void overrideAllCardArtWithPersonalPreferenceToggled(QT_STATE_CHANGED_T enabled);
|
||||||
|
|
||||||
|
|
@ -41,7 +40,6 @@ private:
|
||||||
QComboBox styleCombo;
|
QComboBox styleCombo;
|
||||||
QLabel styleWarningLabel;
|
QLabel styleWarningLabel;
|
||||||
QPushButton editPaletteButton;
|
QPushButton editPaletteButton;
|
||||||
QString previousStyle;
|
|
||||||
|
|
||||||
QLabel homeTabBackgroundSourceLabel;
|
QLabel homeTabBackgroundSourceLabel;
|
||||||
QComboBox homeTabBackgroundSourceBox;
|
QComboBox homeTabBackgroundSourceBox;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
#include "../../../client/settings/cache_settings.h"
|
#include "../../../client/settings/cache_settings.h"
|
||||||
#include "../../pixel_map_generator.h"
|
#include "../../pixel_map_generator.h"
|
||||||
#include "../dialogs/path_safety_warning.h"
|
|
||||||
#include "update/card_spoiler/spoiler_background_updater.h"
|
#include "update/card_spoiler/spoiler_background_updater.h"
|
||||||
|
|
||||||
#include <QAbstractItemModel>
|
#include <QAbstractItemModel>
|
||||||
|
|
@ -364,9 +363,6 @@ void DeckEditorSettingsPage::spoilerPathButtonClicked()
|
||||||
if (lsPath.isEmpty()) {
|
if (lsPath.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!PathSafetyWarning::confirmOutsideProgramDir(this, lsPath + "/spoiler.xml")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mpSpoilerSavePathLineEdit->setText(lsPath + "/spoiler.xml");
|
mpSpoilerSavePathLineEdit->setText(lsPath + "/spoiler.xml");
|
||||||
SettingsCache::instance().paths().setSpoilerDatabasePath(lsPath + "/spoiler.xml");
|
SettingsCache::instance().paths().setSpoilerDatabasePath(lsPath + "/spoiler.xml");
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
#include "general_settings_page.h"
|
#include "general_settings_page.h"
|
||||||
|
|
||||||
#include "../../../client/settings/cache_settings.h"
|
#include "../../../client/settings/cache_settings.h"
|
||||||
#include "../dialogs/path_safety_warning.h"
|
|
||||||
#include "../interface/card_picture_loader/card_picture_loader.h"
|
#include "../interface/card_picture_loader/card_picture_loader.h"
|
||||||
#include "../main.h"
|
#include "../main.h"
|
||||||
#include "../server/user/user_info_connection.h"
|
#include "../server/user/user_info_connection.h"
|
||||||
|
|
@ -379,9 +378,6 @@ void GeneralSettingsPage::deckPathButtonClicked()
|
||||||
if (path.isEmpty()) {
|
if (path.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!PathSafetyWarning::confirmOutsideProgramDir(this, path)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
deckPathEdit->setText(path);
|
deckPathEdit->setText(path);
|
||||||
SettingsCache::instance().paths().setDeckPath(path);
|
SettingsCache::instance().paths().setDeckPath(path);
|
||||||
|
|
@ -393,9 +389,6 @@ void GeneralSettingsPage::filtersPathButtonClicked()
|
||||||
if (path.isEmpty()) {
|
if (path.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!PathSafetyWarning::confirmOutsideProgramDir(this, path)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
filtersPathEdit->setText(path);
|
filtersPathEdit->setText(path);
|
||||||
SettingsCache::instance().paths().setFiltersPath(path);
|
SettingsCache::instance().paths().setFiltersPath(path);
|
||||||
|
|
@ -407,9 +400,6 @@ void GeneralSettingsPage::replaysPathButtonClicked()
|
||||||
if (path.isEmpty()) {
|
if (path.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!PathSafetyWarning::confirmOutsideProgramDir(this, path)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
replaysPathEdit->setText(path);
|
replaysPathEdit->setText(path);
|
||||||
SettingsCache::instance().paths().setReplaysPath(path);
|
SettingsCache::instance().paths().setReplaysPath(path);
|
||||||
|
|
@ -421,9 +411,6 @@ void GeneralSettingsPage::picsPathButtonClicked()
|
||||||
if (path.isEmpty()) {
|
if (path.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!PathSafetyWarning::confirmOutsideProgramDir(this, path)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
picsPathEdit->setText(path);
|
picsPathEdit->setText(path);
|
||||||
SettingsCache::instance().paths().setPicsPath(path);
|
SettingsCache::instance().paths().setPicsPath(path);
|
||||||
|
|
@ -435,9 +422,6 @@ void GeneralSettingsPage::cardDatabasePathButtonClicked()
|
||||||
if (path.isEmpty()) {
|
if (path.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!PathSafetyWarning::confirmOutsideProgramDir(this, path)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cardDatabasePathEdit->setText(path);
|
cardDatabasePathEdit->setText(path);
|
||||||
SettingsCache::instance().paths().setCardDatabasePath(path);
|
SettingsCache::instance().paths().setCardDatabasePath(path);
|
||||||
|
|
@ -449,9 +433,6 @@ void GeneralSettingsPage::customCardDatabaseButtonClicked()
|
||||||
if (path.isEmpty()) {
|
if (path.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!PathSafetyWarning::confirmOutsideProgramDir(this, path)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
customCardDatabasePathEdit->setText(path);
|
customCardDatabasePathEdit->setText(path);
|
||||||
SettingsCache::instance().paths().setCustomCardDatabasePath(path);
|
SettingsCache::instance().paths().setCustomCardDatabasePath(path);
|
||||||
|
|
@ -463,9 +444,6 @@ void GeneralSettingsPage::tokenDatabasePathButtonClicked()
|
||||||
if (path.isEmpty()) {
|
if (path.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!PathSafetyWarning::confirmOutsideProgramDir(this, path)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
tokenDatabasePathEdit->setText(path);
|
tokenDatabasePathEdit->setText(path);
|
||||||
SettingsCache::instance().paths().setTokenDatabasePath(path);
|
SettingsCache::instance().paths().setTokenDatabasePath(path);
|
||||||
|
|
|
||||||
|
|
@ -1060,12 +1060,6 @@ void TabGame::setCardMenu(CardMenu *menu)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (aCardMenu->menu() && aCardMenu->menu()->isVisible()) {
|
|
||||||
aCardMenu->menu()->hide();
|
|
||||||
if (gameView && gameView->viewport()) {
|
|
||||||
gameView->viewport()->update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (menu) {
|
if (menu) {
|
||||||
aCardMenu->setMenu(menu);
|
aCardMenu->setMenu(menu);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
#include "tab_report.h"
|
#include "tab_report.h"
|
||||||
|
|
||||||
#include "../server/user/user_context_menu.h"
|
|
||||||
#include "../utility/report_utils.h"
|
#include "../utility/report_utils.h"
|
||||||
#include "abstract_client.h"
|
#include "abstract_client.h"
|
||||||
#include "libcockatrice/protocol/pb/response.pb.h"
|
#include "libcockatrice/protocol/pb/response.pb.h"
|
||||||
|
|
@ -34,13 +33,11 @@
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
#include <libcockatrice/network/server/remote/user_level.h>
|
|
||||||
#include <libcockatrice/protocol/pb/command_replay_download_by_game_id.pb.h>
|
#include <libcockatrice/protocol/pb/command_replay_download_by_game_id.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_report_add_comment.pb.h>
|
#include <libcockatrice/protocol/pb/command_report_add_comment.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_report_assign.pb.h>
|
#include <libcockatrice/protocol/pb/command_report_assign.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_report_details.pb.h>
|
#include <libcockatrice/protocol/pb/command_report_details.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_report_list.pb.h>
|
#include <libcockatrice/protocol/pb/command_report_list.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_report_reopen.pb.h>
|
|
||||||
#include <libcockatrice/protocol/pb/command_report_resolve.pb.h>
|
#include <libcockatrice/protocol/pb/command_report_resolve.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_report_stats.pb.h>
|
#include <libcockatrice/protocol/pb/command_report_stats.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_report_user_info.pb.h>
|
#include <libcockatrice/protocol/pb/command_report_user_info.pb.h>
|
||||||
|
|
@ -123,10 +120,6 @@ TabReport::TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client) : T
|
||||||
table->horizontalHeader()->setSectionResizeMode(COL_REPORTED, QHeaderView::Stretch);
|
table->horizontalHeader()->setSectionResizeMode(COL_REPORTED, QHeaderView::Stretch);
|
||||||
table->horizontalHeader()->setSectionResizeMode(COL_REPORTER, QHeaderView::ResizeToContents);
|
table->horizontalHeader()->setSectionResizeMode(COL_REPORTER, QHeaderView::ResizeToContents);
|
||||||
connect(table, &QTableWidget::itemSelectionChanged, this, &TabReport::onSelectionChanged);
|
connect(table, &QTableWidget::itemSelectionChanged, this, &TabReport::onSelectionChanged);
|
||||||
table->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
||||||
connect(table, &QTableWidget::customContextMenuRequested, this, &TabReport::showTableContextMenu);
|
|
||||||
userContextMenu = new UserContextMenu(tabSupervisor, this);
|
|
||||||
connect(userContextMenu, &UserContextMenu::openMessageDialog, this, &TabReport::openMessageDialog);
|
|
||||||
|
|
||||||
descGroup = new QGroupBox;
|
descGroup = new QGroupBox;
|
||||||
descriptionEdit = new QTextEdit;
|
descriptionEdit = new QTextEdit;
|
||||||
|
|
@ -238,14 +231,14 @@ TabReport::TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client) : T
|
||||||
|
|
||||||
assignButton = new QPushButton;
|
assignButton = new QPushButton;
|
||||||
resolveButton = new QPushButton;
|
resolveButton = new QPushButton;
|
||||||
|
resolveWithNoteButton = new QPushButton;
|
||||||
dismissButton = new QPushButton;
|
dismissButton = new QPushButton;
|
||||||
reopenButton = new QPushButton;
|
|
||||||
viewReplayButton = new QPushButton;
|
viewReplayButton = new QPushButton;
|
||||||
joinGameButton = new QPushButton;
|
joinGameButton = new QPushButton;
|
||||||
connect(assignButton, &QPushButton::clicked, this, &TabReport::assignReport);
|
connect(assignButton, &QPushButton::clicked, this, &TabReport::assignReport);
|
||||||
connect(resolveButton, &QPushButton::clicked, this, [this]() { resolveReport(false); });
|
connect(resolveButton, &QPushButton::clicked, this, [this]() { resolveReport(false, false); });
|
||||||
connect(dismissButton, &QPushButton::clicked, this, [this]() { resolveReport(true); });
|
connect(resolveWithNoteButton, &QPushButton::clicked, this, [this]() { resolveReport(false, true); });
|
||||||
connect(reopenButton, &QPushButton::clicked, this, &TabReport::reopenReport);
|
connect(dismissButton, &QPushButton::clicked, this, [this]() { resolveReport(true, true); });
|
||||||
connect(viewReplayButton, &QPushButton::clicked, this, &TabReport::viewReplay);
|
connect(viewReplayButton, &QPushButton::clicked, this, &TabReport::viewReplay);
|
||||||
connect(joinGameButton, &QPushButton::clicked, this, &TabReport::joinGame);
|
connect(joinGameButton, &QPushButton::clicked, this, &TabReport::joinGame);
|
||||||
|
|
||||||
|
|
@ -254,8 +247,8 @@ TabReport::TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client) : T
|
||||||
auto *actionBar = new QHBoxLayout;
|
auto *actionBar = new QHBoxLayout;
|
||||||
actionBar->addWidget(assignButton);
|
actionBar->addWidget(assignButton);
|
||||||
actionBar->addWidget(resolveButton);
|
actionBar->addWidget(resolveButton);
|
||||||
|
actionBar->addWidget(resolveWithNoteButton);
|
||||||
actionBar->addWidget(dismissButton);
|
actionBar->addWidget(dismissButton);
|
||||||
actionBar->addWidget(reopenButton);
|
|
||||||
actionBar->addSpacing(20);
|
actionBar->addSpacing(20);
|
||||||
actionBar->addWidget(viewReplayButton);
|
actionBar->addWidget(viewReplayButton);
|
||||||
actionBar->addWidget(joinGameButton);
|
actionBar->addWidget(joinGameButton);
|
||||||
|
|
@ -279,7 +272,6 @@ TabReport::TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client) : T
|
||||||
|
|
||||||
void TabReport::retranslateUi()
|
void TabReport::retranslateUi()
|
||||||
{
|
{
|
||||||
userContextMenu->retranslateUi();
|
|
||||||
searchEdit->setPlaceholderText(tr("Search by username, category..."));
|
searchEdit->setPlaceholderText(tr("Search by username, category..."));
|
||||||
statusFilter->clear();
|
statusFilter->clear();
|
||||||
statusFilter->addItem(tr("All Statuses"), "");
|
statusFilter->addItem(tr("All Statuses"), "");
|
||||||
|
|
@ -320,10 +312,9 @@ void TabReport::retranslateUi()
|
||||||
statsGroup->setTitle(tr("Report Statistics"));
|
statsGroup->setTitle(tr("Report Statistics"));
|
||||||
|
|
||||||
assignButton->setText(tr("Assign to Me"));
|
assignButton->setText(tr("Assign to Me"));
|
||||||
resolveButton->setText(tr("Resolve..."));
|
resolveButton->setText(tr("Resolve"));
|
||||||
|
resolveWithNoteButton->setText(tr("Resolve with note..."));
|
||||||
dismissButton->setText(tr("Dismiss..."));
|
dismissButton->setText(tr("Dismiss..."));
|
||||||
reopenButton->setText(tr("Reopen..."));
|
|
||||||
reopenButton->setToolTip(tr("Available for resolved or dismissed reports."));
|
|
||||||
viewReplayButton->setText(tr("View Replay"));
|
viewReplayButton->setText(tr("View Replay"));
|
||||||
joinGameButton->setText(tr("Join Game"));
|
joinGameButton->setText(tr("Join Game"));
|
||||||
}
|
}
|
||||||
|
|
@ -355,7 +346,6 @@ void TabReport::reportListResponse(const Response &response)
|
||||||
refreshButton->setEnabled(true);
|
refreshButton->setEnabled(true);
|
||||||
|
|
||||||
if (response.response_code() != Response::RespOk) {
|
if (response.response_code() != Response::RespOk) {
|
||||||
pendingStatusMessage.clear();
|
|
||||||
statusLabel->setText(tr("Failed to load reports."));
|
statusLabel->setText(tr("Failed to load reports."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -369,11 +359,6 @@ void TabReport::reportListResponse(const Response &response)
|
||||||
applyFilters();
|
applyFilters();
|
||||||
updateStats();
|
updateStats();
|
||||||
|
|
||||||
if (!pendingStatusMessage.isEmpty()) {
|
|
||||||
statusLabel->setText(pendingStatusMessage);
|
|
||||||
pendingStatusMessage.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedReportIdBeforeRefresh >= 0) {
|
if (selectedReportIdBeforeRefresh >= 0) {
|
||||||
bool found = false;
|
bool found = false;
|
||||||
for (int row = 0; row < table->rowCount(); ++row) {
|
for (int row = 0; row < table->rowCount(); ++row) {
|
||||||
|
|
@ -509,36 +494,6 @@ void TabReport::onSelectionChanged()
|
||||||
updateActionStates();
|
updateActionStates();
|
||||||
}
|
}
|
||||||
|
|
||||||
void TabReport::showTableContextMenu(const QPoint &pos)
|
|
||||||
{
|
|
||||||
QTableWidgetItem *clickedItem = table->itemAt(pos);
|
|
||||||
if (!clickedItem || clickedItem->row() < 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int row = clickedItem->row();
|
|
||||||
const QTableWidgetItem *reportedItem = table->item(row, COL_REPORTED);
|
|
||||||
const QTableWidgetItem *idItem = table->item(row, COL_ID);
|
|
||||||
if (!reportedItem || !idItem || reportedItem->text().isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int userLevel = ServerInfo_User::IsUser;
|
|
||||||
const int reportId = idItem->data(Qt::UserRole).toInt();
|
|
||||||
for (const ServerInfo_Report &r : filteredReports) {
|
|
||||||
if (r.report_id() == reportId) {
|
|
||||||
if (r.reported_user_level() != 0) {
|
|
||||||
userLevel = r.reported_user_level();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
table->selectRow(row);
|
|
||||||
userContextMenu->showContextMenu(table->viewport()->mapToGlobal(pos), reportedItem->text(),
|
|
||||||
UserLevelFlags(userLevel));
|
|
||||||
}
|
|
||||||
|
|
||||||
void TabReport::loadReportDetails(int reportId)
|
void TabReport::loadReportDetails(int reportId)
|
||||||
{
|
{
|
||||||
detailsRequestedReportId = reportId;
|
detailsRequestedReportId = reportId;
|
||||||
|
|
@ -622,8 +577,8 @@ void TabReport::updateActionStates()
|
||||||
const QString status = QString::fromStdString(report.status());
|
const QString status = QString::fromStdString(report.status());
|
||||||
assignButton->setEnabled(status == "open");
|
assignButton->setEnabled(status == "open");
|
||||||
resolveButton->setEnabled(status == "open" || status == "assigned");
|
resolveButton->setEnabled(status == "open" || status == "assigned");
|
||||||
|
resolveWithNoteButton->setEnabled(status == "open" || status == "assigned");
|
||||||
dismissButton->setEnabled(status == "open" || status == "assigned");
|
dismissButton->setEnabled(status == "open" || status == "assigned");
|
||||||
reopenButton->setEnabled(status == "resolved" || status == "dismissed");
|
|
||||||
|
|
||||||
const bool hasGameId = report.game_id() > 0;
|
const bool hasGameId = report.game_id() > 0;
|
||||||
const bool hasReplay = hasGameId && report.has_replay_id() && report.replay_id() > 0;
|
const bool hasReplay = hasGameId && report.has_replay_id() && report.replay_id() > 0;
|
||||||
|
|
@ -635,8 +590,8 @@ void TabReport::setActionsEnabled(bool enabled)
|
||||||
{
|
{
|
||||||
assignButton->setEnabled(enabled);
|
assignButton->setEnabled(enabled);
|
||||||
resolveButton->setEnabled(enabled);
|
resolveButton->setEnabled(enabled);
|
||||||
|
resolveWithNoteButton->setEnabled(enabled);
|
||||||
dismissButton->setEnabled(enabled);
|
dismissButton->setEnabled(enabled);
|
||||||
reopenButton->setEnabled(enabled);
|
|
||||||
viewReplayButton->setEnabled(false);
|
viewReplayButton->setEnabled(false);
|
||||||
joinGameButton->setEnabled(false);
|
joinGameButton->setEnabled(false);
|
||||||
commentButton->setEnabled(enabled);
|
commentButton->setEnabled(enabled);
|
||||||
|
|
@ -690,7 +645,7 @@ void TabReport::assignReport()
|
||||||
void TabReport::assignResponse(const Response &response)
|
void TabReport::assignResponse(const Response &response)
|
||||||
{
|
{
|
||||||
if (response.response_code() == Response::RespOk) {
|
if (response.response_code() == Response::RespOk) {
|
||||||
pendingStatusMessage = tr("Assigned.");
|
statusLabel->setText(tr("Assigned."));
|
||||||
refreshList();
|
refreshList();
|
||||||
} else {
|
} else {
|
||||||
statusLabel->setText(tr("Assignment failed."));
|
statusLabel->setText(tr("Assignment failed."));
|
||||||
|
|
@ -698,19 +653,22 @@ void TabReport::assignResponse(const Response &response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void TabReport::resolveReport(bool dismissed)
|
void TabReport::resolveReport(bool dismissed, bool promptNote)
|
||||||
{
|
{
|
||||||
const int reportId = selectedReportId();
|
const int reportId = selectedReportId();
|
||||||
if (reportId < 0) {
|
if (reportId < 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ok;
|
QString note;
|
||||||
QString note = QInputDialog::getText(this, dismissed ? tr("Dismiss Report") : tr("Resolve Report"),
|
if (promptNote) {
|
||||||
dismissed ? tr("Optional note:") : tr("Resolution note (optional):"),
|
bool ok;
|
||||||
QLineEdit::Normal, QString(), &ok);
|
note = QInputDialog::getText(this, dismissed ? tr("Dismiss Report") : tr("Resolve Report"),
|
||||||
if (!ok) {
|
dismissed ? tr("Optional note:") : tr("Resolution note (optional):"),
|
||||||
return;
|
QLineEdit::Normal, QString(), &ok);
|
||||||
|
if (!ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setActionsEnabled(false);
|
setActionsEnabled(false);
|
||||||
|
|
@ -731,46 +689,7 @@ void TabReport::resolveReport(bool dismissed)
|
||||||
void TabReport::resolveResponse(const Response &response)
|
void TabReport::resolveResponse(const Response &response)
|
||||||
{
|
{
|
||||||
if (response.response_code() == Response::RespOk) {
|
if (response.response_code() == Response::RespOk) {
|
||||||
pendingStatusMessage = tr("Done.");
|
statusLabel->setText(tr("Done."));
|
||||||
refreshList();
|
|
||||||
} else {
|
|
||||||
statusLabel->setText(tr("Action failed."));
|
|
||||||
updateActionStates();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void TabReport::reopenReport()
|
|
||||||
{
|
|
||||||
const int reportId = selectedReportId();
|
|
||||||
if (reportId < 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ok;
|
|
||||||
QString note = QInputDialog::getText(this, tr("Reopen Report"), tr("Reopening note (optional):"), QLineEdit::Normal,
|
|
||||||
QString(), &ok);
|
|
||||||
if (!ok) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setActionsEnabled(false);
|
|
||||||
statusLabel->setText(tr("Reopening..."));
|
|
||||||
|
|
||||||
Command_ReportReopen cmd;
|
|
||||||
cmd.set_report_id(reportId);
|
|
||||||
if (!note.isEmpty()) {
|
|
||||||
cmd.set_reopen_note(note.toStdString());
|
|
||||||
}
|
|
||||||
|
|
||||||
PendingCommand *pend = client->prepareModeratorCommand(cmd);
|
|
||||||
connect(pend, &PendingCommand::finished, this, &TabReport::reopenResponse);
|
|
||||||
client->sendCommand(pend);
|
|
||||||
}
|
|
||||||
|
|
||||||
void TabReport::reopenResponse(const Response &response)
|
|
||||||
{
|
|
||||||
if (response.response_code() == Response::RespOk) {
|
|
||||||
pendingStatusMessage = tr("Reopened.");
|
|
||||||
refreshList();
|
refreshList();
|
||||||
} else {
|
} else {
|
||||||
statusLabel->setText(tr("Action failed."));
|
statusLabel->setText(tr("Action failed."));
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
#include "tab.h"
|
#include "tab.h"
|
||||||
|
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QPoint>
|
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <libcockatrice/protocol/pb/serverinfo_report.pb.h>
|
#include <libcockatrice/protocol/pb/serverinfo_report.pb.h>
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
|
|
@ -23,7 +22,6 @@ class QTimer;
|
||||||
class GameReplay;
|
class GameReplay;
|
||||||
class Response;
|
class Response;
|
||||||
class TabSupervisor;
|
class TabSupervisor;
|
||||||
class UserContextMenu;
|
|
||||||
|
|
||||||
class TabReport : public Tab
|
class TabReport : public Tab
|
||||||
{
|
{
|
||||||
|
|
@ -39,17 +37,15 @@ public:
|
||||||
signals:
|
signals:
|
||||||
void openReplay(GameReplay *replay);
|
void openReplay(GameReplay *replay);
|
||||||
void requestJoinGame(int gameId, int roomId);
|
void requestJoinGame(int gameId, int roomId);
|
||||||
void openMessageDialog(const QString &userName, bool focus);
|
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void refreshList();
|
void refreshList();
|
||||||
void reportListResponse(const Response &response);
|
void reportListResponse(const Response &response);
|
||||||
void assignReport();
|
void assignReport();
|
||||||
void assignResponse(const Response &response);
|
void assignResponse(const Response &response);
|
||||||
void resolveReport(bool dismissed);
|
void resolveReport(bool dismissed, bool promptNote);
|
||||||
void resolveResponse(const Response &response);
|
void resolveResponse(const Response &response);
|
||||||
void onSelectionChanged();
|
void onSelectionChanged();
|
||||||
void showTableContextMenu(const QPoint &pos);
|
|
||||||
void viewReplay();
|
void viewReplay();
|
||||||
void viewReplayResponse(const Response &response);
|
void viewReplayResponse(const Response &response);
|
||||||
void joinGame();
|
void joinGame();
|
||||||
|
|
@ -60,8 +56,6 @@ private slots:
|
||||||
void reportDetailsResponse(const Response &response);
|
void reportDetailsResponse(const Response &response);
|
||||||
void requestStats();
|
void requestStats();
|
||||||
void statsResponse(const Response &response);
|
void statsResponse(const Response &response);
|
||||||
void reopenReport();
|
|
||||||
void reopenResponse(const Response &response);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
int selectedReportId() const;
|
int selectedReportId() const;
|
||||||
|
|
@ -91,13 +85,12 @@ private:
|
||||||
QPushButton *commentButton;
|
QPushButton *commentButton;
|
||||||
QPushButton *assignButton;
|
QPushButton *assignButton;
|
||||||
QPushButton *resolveButton;
|
QPushButton *resolveButton;
|
||||||
|
QPushButton *resolveWithNoteButton;
|
||||||
QPushButton *dismissButton;
|
QPushButton *dismissButton;
|
||||||
QPushButton *reopenButton;
|
|
||||||
QPushButton *viewReplayButton;
|
QPushButton *viewReplayButton;
|
||||||
QPushButton *joinGameButton;
|
QPushButton *joinGameButton;
|
||||||
QLabel *statusLabel;
|
QLabel *statusLabel;
|
||||||
QTimer *refreshTimer;
|
QTimer *refreshTimer;
|
||||||
UserContextMenu *userContextMenu;
|
|
||||||
|
|
||||||
QGroupBox *userContextGroup;
|
QGroupBox *userContextGroup;
|
||||||
QLabel *userContextName;
|
QLabel *userContextName;
|
||||||
|
|
@ -115,7 +108,6 @@ private:
|
||||||
QLabel *userContextNotesLabel;
|
QLabel *userContextNotesLabel;
|
||||||
QLabel *userContextRecentReportsLabel;
|
QLabel *userContextRecentReportsLabel;
|
||||||
QString lastRequestedUser;
|
QString lastRequestedUser;
|
||||||
QString pendingStatusMessage;
|
|
||||||
QGroupBox *statsGroup;
|
QGroupBox *statsGroup;
|
||||||
QLabel *statsTotalLabel;
|
QLabel *statsTotalLabel;
|
||||||
QLabel *statsTrendLabel;
|
QLabel *statsTrendLabel;
|
||||||
|
|
|
||||||
|
|
@ -919,7 +919,6 @@ void TabSupervisor::openTabReport()
|
||||||
myAddTab(tabReport, aTabReport);
|
myAddTab(tabReport, aTabReport);
|
||||||
connect(tabReport, &TabReport::openReplay, this, &TabSupervisor::openReplay);
|
connect(tabReport, &TabReport::openReplay, this, &TabSupervisor::openReplay);
|
||||||
connect(tabReport, &TabReport::requestJoinGame, this, &TabSupervisor::joinReportGame);
|
connect(tabReport, &TabReport::requestJoinGame, this, &TabSupervisor::joinReportGame);
|
||||||
connect(tabReport, &TabReport::openMessageDialog, this, &TabSupervisor::addMessageTab);
|
|
||||||
connect(tabReport, &QObject::destroyed, this, [this] {
|
connect(tabReport, &QObject::destroyed, this, [this] {
|
||||||
tabReport = nullptr;
|
tabReport = nullptr;
|
||||||
aTabReport->setChecked(false);
|
aTabReport->setChecked(false);
|
||||||
|
|
|
||||||
|
|
@ -46,22 +46,6 @@ template <class T> class QSharedPointer;
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
|
||||||
// Below this row width the delegate switches to a compact layout so the card
|
|
||||||
// name stays readable when the popup cannot be as wide as PopupRowWidth.
|
|
||||||
constexpr int CompactRowWidth = 380;
|
|
||||||
constexpr int CompactSymbolRadius = 6;
|
|
||||||
constexpr int CompactSymbolSpacing = 1;
|
|
||||||
constexpr int CompactRightPad = 8;
|
|
||||||
constexpr int ManaZoneWidth = 110;
|
|
||||||
constexpr int CompactManaZoneWidth = 70;
|
|
||||||
constexpr int RightPad = 14;
|
|
||||||
constexpr int TextLeftPad = 12;
|
|
||||||
constexpr int TextRightGap = 8;
|
|
||||||
|
|
||||||
// Below this text width the type-line band is dropped so the name can use the
|
|
||||||
// whole row height instead of being elided to a single readable character.
|
|
||||||
constexpr int MinNameWidth = 120;
|
|
||||||
|
|
||||||
struct ManaColor
|
struct ManaColor
|
||||||
{
|
{
|
||||||
QColor fill;
|
QColor fill;
|
||||||
|
|
@ -141,7 +125,7 @@ QSize CardCompleterDelegate::sizeHint(const QStyleOptionViewItem &option, const
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fixed wide rows so the popup has room for name, type line, set and mana
|
// Fixed wide rows so the popup has room for name, type line, set and mana
|
||||||
return {PopupRowWidth, CardRowHeight};
|
return {480, CardRowHeight};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -192,12 +176,7 @@ void CardCompleterDelegate::drawManaSymbol(QPainter *p, QPoint centre, const QSt
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
int CardCompleterDelegate::drawManaCost(QPainter *p,
|
int CardCompleterDelegate::drawManaCost(QPainter *p, const QRect &row, const QString &manaCost, int radius) const
|
||||||
const QRect &row,
|
|
||||||
const QString &manaCost,
|
|
||||||
int radius,
|
|
||||||
int spacing,
|
|
||||||
int rightPad) const
|
|
||||||
{
|
{
|
||||||
if (manaCost.isEmpty()) {
|
if (manaCost.isEmpty()) {
|
||||||
return row.right();
|
return row.right();
|
||||||
|
|
@ -226,9 +205,11 @@ int CardCompleterDelegate::drawManaCost(QPainter *p,
|
||||||
totalW += PartGap;
|
totalW += PartGap;
|
||||||
}
|
}
|
||||||
|
|
||||||
totalW += parts.at(i).size() * diam + qMax(0, parts.at(i).size() - 1) * spacing;
|
totalW += parts.at(i).size() * diam + qMax(0, parts.at(i).size() - 1) * SymbolSpacing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const int rightPad = 14;
|
||||||
|
|
||||||
int x = row.right() - rightPad - totalW + radius;
|
int x = row.right() - rightPad - totalW + radius;
|
||||||
|
|
||||||
const int cy = row.center().y();
|
const int cy = row.center().y();
|
||||||
|
|
@ -238,11 +219,11 @@ int CardCompleterDelegate::drawManaCost(QPainter *p,
|
||||||
|
|
||||||
for (const QString &sym : symbols) {
|
for (const QString &sym : symbols) {
|
||||||
drawManaSymbol(p, {x, cy}, sym, radius);
|
drawManaSymbol(p, {x, cy}, sym, radius);
|
||||||
x += diam + spacing;
|
x += diam + SymbolSpacing;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i < parts.size() - 1) {
|
if (i < parts.size() - 1) {
|
||||||
x += PartGap - spacing;
|
x += PartGap - SymbolSpacing;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -309,14 +290,6 @@ void CardCompleterDelegate::paint(QPainter *painter, const QStyleOptionViewItem
|
||||||
const QString typeLine = card->getCardType();
|
const QString typeLine = card->getCardType();
|
||||||
const QString setCode = setCodeForCard(card);
|
const QString setCode = setCodeForCard(card);
|
||||||
|
|
||||||
// When the row is too narrow for the full layout, use smaller mana pips and
|
|
||||||
// a slimmer mana zone so the card name keeps as much room as possible.
|
|
||||||
const bool compact = rect.width() < CompactRowWidth;
|
|
||||||
const int symbolRadius = compact ? CompactSymbolRadius : SymbolRadius;
|
|
||||||
const int symbolSpacing = compact ? CompactSymbolSpacing : SymbolSpacing;
|
|
||||||
const int rightPad = compact ? CompactRightPad : RightPad;
|
|
||||||
const int manaZoneWidth = compact ? CompactManaZoneWidth : ManaZoneWidth;
|
|
||||||
|
|
||||||
const QColor accent = accentForColors(card->getColors());
|
const QColor accent = accentForColors(card->getColors());
|
||||||
|
|
||||||
const QColor base = pal.color(QPalette::Base);
|
const QColor base = pal.color(QPalette::Base);
|
||||||
|
|
@ -365,7 +338,7 @@ void CardCompleterDelegate::paint(QPainter *painter, const QStyleOptionViewItem
|
||||||
// Right mana zone
|
// Right mana zone
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
const QRectF manaZone(cardRect.right() - manaZoneWidth, cardRect.top(), manaZoneWidth, cardRect.height());
|
const QRectF manaZone(cardRect.right() - 110, cardRect.top(), 110, cardRect.height());
|
||||||
|
|
||||||
QLinearGradient manaGrad(manaZone.topLeft(), manaZone.bottomLeft());
|
QLinearGradient manaGrad(manaZone.topLeft(), manaZone.bottomLeft());
|
||||||
|
|
||||||
|
|
@ -407,27 +380,22 @@ void CardCompleterDelegate::paint(QPainter *painter, const QStyleOptionViewItem
|
||||||
// Mana cost
|
// Mana cost
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
const int costLeft = drawManaCost(painter, cardRect.toRect(), manaCost, symbolRadius, symbolSpacing, rightPad);
|
const int costLeft = drawManaCost(painter, cardRect.toRect(), manaCost, SymbolRadius);
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Card name + type line + set code
|
// Card name + type line + set code
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
const int textLeft = cardRect.left() + AccentBarWidth + TextLeftPad;
|
const int textLeft = cardRect.left() + AccentBarWidth + 12;
|
||||||
const int textRight = costLeft - TextRightGap;
|
const int textRight = costLeft - 8;
|
||||||
const int textWidth = qMax(0, textRight - textLeft);
|
const int textWidth = qMax(0, textRight - textLeft);
|
||||||
|
|
||||||
// If even the compact layout leaves too little room for a readable name,
|
|
||||||
// drop the type-line band and let the name use the full row height.
|
|
||||||
const bool showInfoBand = textWidth >= MinNameWidth;
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Card name (top band)
|
// Card name (top band)
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
{
|
{
|
||||||
const QRect nameRect = showInfoBand ? QRect(textLeft, rect.top() + 2, textWidth, 20)
|
const QRect nameRect(textLeft, rect.top() + 2, textWidth, 20);
|
||||||
: QRect(textLeft, rect.top() + 2, textWidth, rect.height() - 4);
|
|
||||||
|
|
||||||
QFont f = option.font;
|
QFont f = option.font;
|
||||||
f.setPixelSize(13);
|
f.setPixelSize(13);
|
||||||
|
|
@ -448,7 +416,7 @@ void CardCompleterDelegate::paint(QPainter *painter, const QStyleOptionViewItem
|
||||||
// Type line + set code (bottom band)
|
// Type line + set code (bottom band)
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
if (showInfoBand) {
|
{
|
||||||
const QRect infoRect(textLeft, rect.top() + 22, textWidth, rect.height() - 24);
|
const QRect infoRect(textLeft, rect.top() + 22, textWidth, rect.height() - 24);
|
||||||
|
|
||||||
QFont f = option.font;
|
QFont f = option.font;
|
||||||
|
|
|
||||||
|
|
@ -36,9 +36,6 @@ public:
|
||||||
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
|
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
|
||||||
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
|
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
|
||||||
|
|
||||||
// Content width of one popup row; the completer popup is sized to fit it.
|
|
||||||
static constexpr int PopupRowWidth = 480;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Set short codes, resolved once per card name and cached
|
// Set short codes, resolved once per card name and cached
|
||||||
mutable QCache<QString, QString> setCodeCache;
|
mutable QCache<QString, QString> setCodeCache;
|
||||||
|
|
@ -51,8 +48,7 @@ private:
|
||||||
|
|
||||||
// Draw all mana pips for a cost string like "2RG" or "{2}{R}{G}"; split and
|
// Draw all mana pips for a cost string like "2RG" or "{2}{R}{G}"; split and
|
||||||
// adventure costs ("1W // W") are drawn as separate groups. Returns the left-most x used
|
// adventure costs ("1W // W") are drawn as separate groups. Returns the left-most x used
|
||||||
int
|
int drawManaCost(QPainter *p, const QRect &row, const QString &manaCost, int radius) const;
|
||||||
drawManaCost(QPainter *p, const QRect &row, const QString &manaCost, int radius, int spacing, int rightPad) const;
|
|
||||||
|
|
||||||
// Resolve the preferred printing's set short code for a card
|
// Resolve the preferred printing's set short code for a card
|
||||||
QString setCodeForCard(const QSharedPointer<CardInfo> &card) const;
|
QString setCodeForCard(const QSharedPointer<CardInfo> &card) const;
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@
|
||||||
#include <QPropertyAnimation>
|
#include <QPropertyAnimation>
|
||||||
#include <QRect>
|
#include <QRect>
|
||||||
#include <QScreen>
|
#include <QScreen>
|
||||||
#include <QScrollBar>
|
|
||||||
#include <QSharedPointer>
|
#include <QSharedPointer>
|
||||||
#include <QSize>
|
#include <QSize>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
@ -104,7 +103,6 @@ bool CardCompleterStyler::eventFilter(QObject *obj, QEvent *ev)
|
||||||
case QEvent::Show:
|
case QEvent::Show:
|
||||||
case QEvent::Move:
|
case QEvent::Move:
|
||||||
case QEvent::Resize:
|
case QEvent::Resize:
|
||||||
applyPopupWidth();
|
|
||||||
updateOrientation();
|
updateOrientation();
|
||||||
reposition();
|
reposition();
|
||||||
break;
|
break;
|
||||||
|
|
@ -161,42 +159,6 @@ bool CardCompleterStyler::handlePopupKeyPress(QKeyEvent *event)
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
void CardCompleterStyler::applyPopupWidth()
|
|
||||||
{
|
|
||||||
auto *popup = completer->popup();
|
|
||||||
|
|
||||||
// QCompleter sizes its popup to the line edit, so a narrow search field
|
|
||||||
// would crush the rows. Ask for the width the rows are designed for (plus
|
|
||||||
// the frame and scrollbar) and let QWidget's geometry clamping keep the
|
|
||||||
// popup that wide, clamped to the screen it is shown on.
|
|
||||||
int contentWidth = CardCompleterDelegate::PopupRowWidth + popup->frameWidth() * 2;
|
|
||||||
if (auto *vbar = popup->verticalScrollBar(); vbar != nullptr) {
|
|
||||||
contentWidth += vbar->sizeHint().width();
|
|
||||||
}
|
|
||||||
|
|
||||||
const QScreen *screen = popup->screen();
|
|
||||||
const QRect available = screen ? screen->availableGeometry() : QRect();
|
|
||||||
|
|
||||||
const int minWidth = available.isEmpty() ? contentWidth : qMin(contentWidth, available.width());
|
|
||||||
popup->setMinimumWidth(minWidth);
|
|
||||||
|
|
||||||
// setMinimumWidth() already widened the popup if it was narrower, but
|
|
||||||
// QCompleter positioned it for the line-edit width, so the widened popup
|
|
||||||
// can now extend past the right screen edge. The popup is a top-level
|
|
||||||
// Qt::Popup window, so geometry()/move() are already in screen
|
|
||||||
// coordinates: clamp the x from geometry() and move directly.
|
|
||||||
if (!available.isEmpty()) {
|
|
||||||
const QRect geom = popup->geometry();
|
|
||||||
const int clampedX =
|
|
||||||
qBound(available.left(), geom.x(), qMax(available.left(), available.right() - geom.width() + 1));
|
|
||||||
if (clampedX != geom.x()) {
|
|
||||||
popup->move(QPoint(clampedX, popup->y()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
void CardCompleterStyler::updateOrientation()
|
void CardCompleterStyler::updateOrientation()
|
||||||
{
|
{
|
||||||
above = isPopupAboveWidget();
|
above = isPopupAboveWidget();
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,6 @@ private:
|
||||||
void stopPreviewFade();
|
void stopPreviewFade();
|
||||||
void reposition();
|
void reposition();
|
||||||
|
|
||||||
void applyPopupWidth();
|
|
||||||
void updateOrientation();
|
void updateOrientation();
|
||||||
void ensureClosestSelected();
|
void ensureClosestSelected();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -130,19 +130,10 @@ void LineEditCompleter::keyPressEvent(QKeyEvent *event)
|
||||||
CompleterInfo *active = nullptr;
|
CompleterInfo *active = nullptr;
|
||||||
QString prefix;
|
QString prefix;
|
||||||
|
|
||||||
// An open "[[" card link takes precedence over a mention: while the cursor is
|
|
||||||
// inside it, an earlier "@" must not shadow the card completer.
|
|
||||||
const int openCardPos = textValue.lastIndexOf("[[", cursorPos - 1);
|
|
||||||
const int closeCardPos = openCardPos == -1 ? -1 : textValue.indexOf("]]", openCardPos + 2);
|
|
||||||
const bool insideCardLink = openCardPos != -1 && (closeCardPos == -1 || closeCardPos >= cursorPos);
|
|
||||||
|
|
||||||
for (auto &info : completers) {
|
for (auto &info : completers) {
|
||||||
bool triggered = false;
|
bool triggered = false;
|
||||||
switch (info.trigger) {
|
switch (info.trigger) {
|
||||||
case CompleterTrigger::Mention: {
|
case CompleterTrigger::Mention: {
|
||||||
if (insideCardLink) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
int triggerPos = textValue.lastIndexOf("@", cursorPos - 1);
|
int triggerPos = textValue.lastIndexOf("@", cursorPos - 1);
|
||||||
if (triggerPos != -1 && (triggerPos == 0 || textValue[triggerPos - 1].isSpace())) {
|
if (triggerPos != -1 && (triggerPos == 0 || textValue[triggerPos - 1].isSpace())) {
|
||||||
triggered = true;
|
triggered = true;
|
||||||
|
|
|
||||||
|
|
@ -100,27 +100,10 @@ void renderReportDetails(QTextEdit *chatLogEdit,
|
||||||
const QString &moderatorPrefix,
|
const QString &moderatorPrefix,
|
||||||
const QString &nonModeratorPrefix)
|
const QString &nonModeratorPrefix)
|
||||||
{
|
{
|
||||||
QString details;
|
|
||||||
if (report.has_resolution_note() && !report.resolution_note().empty()) {
|
|
||||||
details = QObject::tr("Resolution note: %1").arg(QString::fromStdString(report.resolution_note()));
|
|
||||||
}
|
|
||||||
if (report.has_resolution_time()) {
|
|
||||||
QString resolvedBy = QObject::tr("unknown");
|
|
||||||
if (report.has_resolved_by_name() && !report.resolved_by_name().empty()) {
|
|
||||||
resolvedBy = QString::fromStdString(report.resolved_by_name());
|
|
||||||
}
|
|
||||||
if (!details.isEmpty()) {
|
|
||||||
details += "\n";
|
|
||||||
}
|
|
||||||
details += QObject::tr("Resolved at %1 by %2").arg(formatReportTime(report.resolution_time()), resolvedBy);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (report.has_chat_log() && !report.chat_log().empty()) {
|
if (report.has_chat_log() && !report.chat_log().empty()) {
|
||||||
const QString chatLog = QString::fromStdString(report.chat_log());
|
chatLogEdit->setPlainText(QString::fromStdString(report.chat_log()));
|
||||||
chatLogEdit->setPlainText(
|
|
||||||
details.isEmpty() ? chatLog : details + "\n\n" + QObject::tr("=== Chat Log ===") + "\n" + chatLog);
|
|
||||||
} else {
|
} else {
|
||||||
chatLogEdit->setPlainText(details);
|
chatLogEdit->clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
commentsEdit->clear();
|
commentsEdit->clear();
|
||||||
|
|
|
||||||
|
|
@ -58,9 +58,7 @@
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QButtonGroup>
|
#include <QButtonGroup>
|
||||||
#include <QCheckBox>
|
|
||||||
#include <QCloseEvent>
|
#include <QCloseEvent>
|
||||||
#include <QCoreApplication>
|
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QDesktopServices>
|
#include <QDesktopServices>
|
||||||
|
|
@ -70,7 +68,6 @@
|
||||||
#include <QFileDialog>
|
#include <QFileDialog>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QFlags>
|
#include <QFlags>
|
||||||
#include <QFutureWatcher>
|
|
||||||
#include <QIODevice>
|
#include <QIODevice>
|
||||||
#include <QLocalSocket>
|
#include <QLocalSocket>
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
|
|
@ -606,45 +603,14 @@ MainWindow::MainWindow(QWidget *parent)
|
||||||
QTimer::singleShot(0, this, &MainWindow::applyStartupDestination);
|
QTimer::singleShot(0, this, &MainWindow::applyStartupDestination);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::warnAboutDataPathsInProgramDir()
|
|
||||||
{
|
|
||||||
auto &paths = SettingsCache::instance().paths();
|
|
||||||
if (SettingsCache::instance().getIsPortableBuild() || paths.getAppDirWarningAcknowledged()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QStringList offenders = paths.pathsInsideDir(QCoreApplication::applicationDirPath());
|
|
||||||
if (offenders.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QMessageBox box(QMessageBox::Warning, tr("Data paths inside the program directory"),
|
|
||||||
tr("These paths are configured inside the Cockatrice program directory:\n\n%1\n\n"
|
|
||||||
"Cockatrice keeps its own data outside the program directory by default. Files "
|
|
||||||
"stored inside it can be deleted by an update or by uninstalling Cockatrice. "
|
|
||||||
"Change them in the Settings dialog - most are on the General page - to keep "
|
|
||||||
"them safe.")
|
|
||||||
.arg(offenders.join(QLatin1Char('\n'))),
|
|
||||||
QMessageBox::Ok, this);
|
|
||||||
box.setTextFormat(Qt::PlainText);
|
|
||||||
auto *dontShowAgain = new QCheckBox(tr("Don't show this again"));
|
|
||||||
box.setCheckBox(dontShowAgain);
|
|
||||||
box.exec();
|
|
||||||
if (dontShowAgain->isChecked()) {
|
|
||||||
paths.setAppDirWarningAcknowledged(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::startupConfigCheck()
|
void MainWindow::startupConfigCheck()
|
||||||
{
|
{
|
||||||
warnAboutDataPathsInProgramDir();
|
|
||||||
|
|
||||||
const bool isCleanInstall = SettingsCache::instance().network().getClientVersion() == CLIENT_INFO_NOT_SET;
|
const bool isCleanInstall = SettingsCache::instance().network().getClientVersion() == CLIENT_INFO_NOT_SET;
|
||||||
|
|
||||||
// The loader never runs checkUnknownSets() itself (see main.cpp): the
|
// checkUnknownSets() is intentionally deferred from the card database load
|
||||||
// front-loaded parse runs before MainWindow exists, so
|
// (which runs in main() before MainWindow exists) so that
|
||||||
// cardDatabaseNewSetsFound / cardDatabaseAllNewSetsEnabled would have no
|
// cardDatabaseNewSetsFound / cardDatabaseAllNewSetsEnabled have live
|
||||||
// receivers. MainWindow owns the check after startup.
|
// receivers when emitted.
|
||||||
// On a clean install the onboarding wizard owns the first-run experience;
|
// On a clean install the onboarding wizard owns the first-run experience;
|
||||||
// wait until it closes so the legacy "all sets enabled" welcome and the
|
// wait until it closes so the legacy "all sets enabled" welcome and the
|
||||||
// Manage Sets dialog don't appear first.
|
// Manage Sets dialog don't appear first.
|
||||||
|
|
@ -1138,35 +1104,18 @@ void MainWindow::cardDatabaseNewSetsFound(int numUnknownSets, QStringList unknow
|
||||||
|
|
||||||
void MainWindow::cardDatabaseAllNewSetsEnabled()
|
void MainWindow::cardDatabaseAllNewSetsEnabled()
|
||||||
{
|
{
|
||||||
auto *cardDatabase = CardDatabaseManager::getInstance();
|
if (firstRunWizardActive || CardDatabaseManager::getInstance()->getCardList().isEmpty()) {
|
||||||
|
// The onboarding wizard owns the first-run messaging on a clean install,
|
||||||
// With no card data there is nothing to have enabled and nothing to reparse.
|
// and with no card data there are no sets to have enabled.
|
||||||
if (cardDatabase->getCardList().isEmpty()) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkUnknownSets() enabled every set after the database had already been
|
QMessageBox::information(
|
||||||
// parsed under the (empty) first-run enablement, so the parser dropped every
|
this, tr("Welcome"),
|
||||||
// printing of every set. Reparse now that the sets are enabled, otherwise the
|
tr("Hi! It seems like you're running this version of Cockatrice for the first time.\nAll the sets in the card "
|
||||||
// deck editor's card database display stays empty until the next launch.
|
"database have been enabled.\nRead more about changing the set order or disabling specific sets and "
|
||||||
auto *watcher = new QFutureWatcher<void>(this);
|
"consequent effects in the \"Manage Sets\" dialog."));
|
||||||
connect(watcher, &QFutureWatcher<void>::finished, this, [this, watcher] {
|
actManageSets();
|
||||||
watcher->deleteLater();
|
|
||||||
if (firstRunWizardActive) {
|
|
||||||
// The onboarding wizard owns the first-run messaging on a clean install.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
QMessageBox::information(
|
|
||||||
this, tr("Welcome"),
|
|
||||||
tr("Hi! It seems like you're running this version of Cockatrice for the first time.\nAll the sets in the "
|
|
||||||
"card "
|
|
||||||
"database have been enabled.\nRead more about changing the set order or disabling specific sets and "
|
|
||||||
"consequent effects in the \"Manage Sets\" dialog."));
|
|
||||||
// The reload above replaces the set instances, so Manage Sets must open
|
|
||||||
// only after the finished snapshot has been swapped in.
|
|
||||||
actManageSets();
|
|
||||||
});
|
|
||||||
watcher->setFuture(QtConcurrent::run([cardDatabase] { cardDatabase->reloadCardDatabasesAndNotify(); }));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* CARD UPDATER */
|
/* CARD UPDATER */
|
||||||
|
|
@ -1253,7 +1202,7 @@ void MainWindow::createCardUpdateProcess(bool background)
|
||||||
<< dir.absoluteFilePath(binaryName);
|
<< dir.absoluteFilePath(binaryName);
|
||||||
QMessageBox::warning(this, tr("Error"),
|
QMessageBox::warning(this, tr("Error"),
|
||||||
tr("Unable to run the card database updater: ") + dir.absoluteFilePath(binaryName));
|
tr("Unable to run the card database updater: ") + dir.absoluteFilePath(binaryName));
|
||||||
exitCardDatabaseUpdate(false);
|
exitCardDatabaseUpdate();
|
||||||
emit cardDatabaseUpdateFinished(false);
|
emit cardDatabaseUpdateFinished(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1330,7 +1279,7 @@ void MainWindow::cardUpdateStandardError()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::exitCardDatabaseUpdate(bool doReload)
|
void MainWindow::exitCardDatabaseUpdate()
|
||||||
{
|
{
|
||||||
if (!cardUpdateProcess) {
|
if (!cardUpdateProcess) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -1339,21 +1288,7 @@ void MainWindow::exitCardDatabaseUpdate(bool doReload)
|
||||||
cardUpdateProcess = nullptr;
|
cardUpdateProcess = nullptr;
|
||||||
statusBar()->clearMessage();
|
statusBar()->clearMessage();
|
||||||
|
|
||||||
if (doReload) {
|
const auto reloadOk1 = QtConcurrent::run([] { CardDatabaseManager::getInstance()->loadCardDatabases(); });
|
||||||
// This can be the first successful load of the session (e.g. the onboarding
|
|
||||||
// download), so it must own the set check for that load.
|
|
||||||
reloadCardDatabaseAndCheckSets();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::reloadCardDatabaseAndCheckSets()
|
|
||||||
{
|
|
||||||
auto *watcher = new QFutureWatcher<void>(this);
|
|
||||||
connect(watcher, &QFutureWatcher<void>::finished, this, [watcher] {
|
|
||||||
watcher->deleteLater();
|
|
||||||
CardDatabaseManager::getInstance()->checkUnknownSets();
|
|
||||||
});
|
|
||||||
watcher->setFuture(QtConcurrent::run([] { CardDatabaseManager::getInstance()->loadCardDatabases(); }));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::cardUpdateError(QProcess::ProcessError err)
|
void MainWindow::cardUpdateError(QProcess::ProcessError err)
|
||||||
|
|
@ -1389,7 +1324,7 @@ void MainWindow::cardUpdateError(QProcess::ProcessError err)
|
||||||
}
|
}
|
||||||
|
|
||||||
qCWarning(CardDatabaseUpdateLog) << "The card database updater exited with an error:" << error;
|
qCWarning(CardDatabaseUpdateLog) << "The card database updater exited with an error:" << error;
|
||||||
exitCardDatabaseUpdate(false);
|
exitCardDatabaseUpdate();
|
||||||
QMessageBox::warning(this, tr("Error"), tr("The card database updater exited with an error:\n%1").arg(error));
|
QMessageBox::warning(this, tr("Error"), tr("The card database updater exited with an error:\n%1").arg(error));
|
||||||
emit cardDatabaseUpdateFinished(false);
|
emit cardDatabaseUpdateFinished(false);
|
||||||
}
|
}
|
||||||
|
|
@ -1402,14 +1337,12 @@ void MainWindow::cardUpdateFinished(int exitCode, QProcess::ExitStatus exitStatu
|
||||||
cardUpdateProgressOutput(); // drain any progress lines not yet parsed
|
cardUpdateProgressOutput(); // drain any progress lines not yet parsed
|
||||||
|
|
||||||
const bool success = (exitStatus == QProcess::NormalExit) && (exitCode == 0);
|
const bool success = (exitStatus == QProcess::NormalExit) && (exitCode == 0);
|
||||||
if (success) {
|
if (exitStatus == QProcess::NormalExit) {
|
||||||
SettingsCache::instance().updates().setLastCardUpdateCheck(QDateTime::currentDateTime().date());
|
SettingsCache::instance().updates().setLastCardUpdateCheck(QDateTime::currentDateTime().date());
|
||||||
} else if (exitStatus == QProcess::NormalExit) {
|
|
||||||
statusBar()->showMessage(tr("Card database update cancelled."), 10000);
|
|
||||||
}
|
}
|
||||||
qCInfo(CardDatabaseUpdateLog) << "The card database updater finished with exit code" << exitCode << "exit status"
|
qCInfo(CardDatabaseUpdateLog) << "The card database updater finished with exit code" << exitCode << "exit status"
|
||||||
<< static_cast<int>(exitStatus) << "success:" << success;
|
<< static_cast<int>(exitStatus) << "success:" << success;
|
||||||
exitCardDatabaseUpdate(success);
|
exitCardDatabaseUpdate();
|
||||||
notifyCardUpdateFinishedFromTray(success);
|
notifyCardUpdateFinishedFromTray(success);
|
||||||
emit cardDatabaseUpdateFinished(success);
|
emit cardDatabaseUpdateFinished(success);
|
||||||
}
|
}
|
||||||
|
|
@ -1557,8 +1490,7 @@ void MainWindow::actAddCustomSet()
|
||||||
QMessageBox::information(
|
QMessageBox::information(
|
||||||
this, tr("Load sets/cards"),
|
this, tr("Load sets/cards"),
|
||||||
tr("The new sets/cards have been added successfully.\nCockatrice will now reload the card database."));
|
tr("The new sets/cards have been added successfully.\nCockatrice will now reload the card database."));
|
||||||
// Imported files can introduce sets the config has never seen.
|
const auto reloadOk1 = QtConcurrent::run([] { CardDatabaseManager::getInstance()->loadCardDatabases(); });
|
||||||
reloadCardDatabaseAndCheckSets();
|
|
||||||
} else {
|
} else {
|
||||||
QMessageBox::warning(this, tr("Load sets/cards"), tr("Sets/cards failed to import."));
|
QMessageBox::warning(this, tr("Load sets/cards"), tr("Sets/cards failed to import."));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -149,23 +149,12 @@ private:
|
||||||
|
|
||||||
void runFirstRunWizard(bool firstRun = false);
|
void runFirstRunWizard(bool firstRun = false);
|
||||||
|
|
||||||
/** @brief Warns once when a non-portable install keeps its data paths inside the program directory. */
|
|
||||||
void warnAboutDataPathsInProgramDir();
|
|
||||||
|
|
||||||
inline QString getCardUpdaterBinaryName()
|
inline QString getCardUpdaterBinaryName()
|
||||||
{
|
{
|
||||||
return "oracle";
|
return "oracle";
|
||||||
}
|
}
|
||||||
void createCardUpdateProcess(bool background = false);
|
void createCardUpdateProcess(bool background = false);
|
||||||
void exitCardDatabaseUpdate(bool doReload);
|
void exitCardDatabaseUpdate();
|
||||||
/**
|
|
||||||
* @brief Reloads the card database on a worker thread and runs checkUnknownSets() once the finished
|
|
||||||
* snapshot has been swapped in.
|
|
||||||
*
|
|
||||||
* The loader never runs checkUnknownSets() itself (see main.cpp); post-update loads that can introduce
|
|
||||||
* new sets must own the check, and it has to run after the queued snapshot swap, not on the worker.
|
|
||||||
*/
|
|
||||||
void reloadCardDatabaseAndCheckSets();
|
|
||||||
/**
|
/**
|
||||||
* @brief Tries to bring the open card database updater window to the front.
|
* @brief Tries to bring the open card database updater window to the front.
|
||||||
* @return true when the updater acknowledged the request
|
* @return true when the updater acknowledged the request
|
||||||
|
|
|
||||||
|
|
@ -343,12 +343,11 @@ int main(int argc, char *argv[])
|
||||||
// contention that happens when the load runs alongside window construction.
|
// contention that happens when the load runs alongside window construction.
|
||||||
// The CardDatabaseModel populates from the already-loaded data in its
|
// The CardDatabaseModel populates from the already-loaded data in its
|
||||||
// constructor, so the window appears fully populated with no startup lag.
|
// constructor, so the window appears fully populated with no startup lag.
|
||||||
// Note: checkUnknownSets() is deliberately not run for this front-loaded
|
// Note: checkUnknownSets() is deferred from the initial load to
|
||||||
// parse. It would run before MainWindow exists, so
|
// MainWindow::startupConfigCheck() so that
|
||||||
// cardDatabaseNewSetsFound / cardDatabaseAllNewSetsEnabled would have no
|
// cardDatabaseNewSetsFound / cardDatabaseAllNewSetsEnabled have live
|
||||||
// receivers, and enabling the sets up-front would suppress the first-run
|
// receivers when emitted. Subsequent reloads (e.g. path changes) call
|
||||||
// experience MainWindow::startupConfigCheck() and the onboarding wizard
|
// checkUnknownSets() directly from the loader after the first load.
|
||||||
// provide. Callers that load after startup own the check.
|
|
||||||
CardDatabaseManager::getInstance()->loadCardDatabases();
|
CardDatabaseManager::getInstance()->loadCardDatabases();
|
||||||
|
|
||||||
MainWindow ui;
|
MainWindow ui;
|
||||||
|
|
|
||||||
|
|
@ -38,18 +38,7 @@ Selecting this action will open a new text editor dialog with the contents of yo
|
||||||
|
|
||||||
The import dialog expects each line to be a card with the following format:
|
The import dialog expects each line to be a card with the following format:
|
||||||
|
|
||||||
`[quantity] Card Name [options]`
|
TODO
|
||||||
|
|
||||||
- `quantity` is the number of copies and can be written as `2`, `2x` or `2X`. If omitted, a single copy is imported.
|
|
||||||
- `options` are appended after the card name and are understood on a best-effort basis:
|
|
||||||
- a set code and collector number, either as `Card Name (SET) 123` or `Card Name (SET) 123-1`
|
|
||||||
- a foil marker, `Card Name *F*`
|
|
||||||
- Lines can also carry a `SB:` prefix or a set code prefix (`[SET]`) — Cockatrice tries to stay compatible with the most
|
|
||||||
common deck formats.
|
|
||||||
|
|
||||||
Tab-separated exports from third-party apps (such as Delver Lens) are also accepted: any line whose first column is a
|
|
||||||
plain quantity treats the following columns as the card name and printer/set metadata (which is discarded), for example
|
|
||||||
`2\tCard Name\tSet Name`.
|
|
||||||
|
|
||||||
Each card should be on a separate line and there should be no empty lines between cards. The first empty line between
|
Each card should be on a separate line and there should be no empty lines between cards. The first empty line between
|
||||||
two blocks of cards will be considered as the divider between mainboard and sideboard.
|
two blocks of cards will be considered as the divider between mainboard and sideboard.
|
||||||
|
|
|
||||||
63
gcovr.cfg
63
gcovr.cfg
|
|
@ -1,63 +0,0 @@
|
||||||
# gcovr configuration for Cockatrice line coverage.
|
|
||||||
#
|
|
||||||
# Used by .ci/compile.sh --coverage and by hand:
|
|
||||||
#
|
|
||||||
# cmake -S . -B build/cover -DCMAKE_BUILD_TYPE=Debug -DWITH_SERVER=1 -DTEST=1 -DENABLE_COVERAGE=ON
|
|
||||||
# cmake --build build/cover -j 24
|
|
||||||
# ctest --test-dir build/cover -LE bench -j 24
|
|
||||||
# gcovr build/cover --root . --object-directory build/cover --txt --decisions
|
|
||||||
#
|
|
||||||
# The point of most of this file is subtraction. A raw report is dominated by code no test can
|
|
||||||
# reach: vendored third-party trees, build directories left over from earlier experiments, and
|
|
||||||
# the client's main() and window classes, which have no headless test. Those all dilute the one
|
|
||||||
# number worth watching, so they are filtered out here rather than in every invocation.
|
|
||||||
|
|
||||||
# root is deliberately not set here. This file is normally invoked from the build directory
|
|
||||||
# (compile.sh runs there, because it configures with `cmake ..`), so a `root` of `.` would
|
|
||||||
# resolve to the build tree and every filter below would exclude everything. compile.sh passes
|
|
||||||
# --root explicitly instead.
|
|
||||||
|
|
||||||
# Our own code only. This keeps generated Qt MOC output, protobuf output and anything CMake
|
|
||||||
# built under the source tree out of the report.
|
|
||||||
filter = ^cockatrice/
|
|
||||||
filter = ^libcockatrice_[^/]+/
|
|
||||||
filter = ^servatrice/
|
|
||||||
filter = ^oracle/
|
|
||||||
|
|
||||||
# The tests are the vehicle, not the subject. They are compiled with the same instrumentation,
|
|
||||||
# so leaving them in would add their near-perfect coverage to the numerator and quietly report a
|
|
||||||
# healthier project than we have: test code is only ever executed by the run that is measuring
|
|
||||||
# it. Helpers under tests/ are excluded on the same grounds.
|
|
||||||
exclude = ^tests/
|
|
||||||
|
|
||||||
# Vendored third-party code is not ours to cover and is never going to move the number.
|
|
||||||
exclude = ^vcpkg/
|
|
||||||
exclude = ^doc/doxygen/theme/
|
|
||||||
exclude = ^libcockatrice_rng/libcockatrice/rng/sfmt/
|
|
||||||
exclude = ^libcockatrice_utility/libcockatrice/utility/peglib\.h$
|
|
||||||
exclude = ^oracle/src/lzma/
|
|
||||||
exclude = ^oracle/src/qt-json/
|
|
||||||
exclude = ^oracle/src/zip/
|
|
||||||
exclude = ^servatrice/src/smtp/
|
|
||||||
|
|
||||||
# Qt's own moc/pch/uic output is generated into the build tree, not the source tree, so the
|
|
||||||
# filters above already keep it out. These cover CLion's directories, which sit in-tree and are
|
|
||||||
# picked up by the object-directory walk.
|
|
||||||
exclude = .*cmake-build-.*
|
|
||||||
exclude = .*\.cache/
|
|
||||||
|
|
||||||
# Process entry points and the client's window chrome. Each of these needs a display, an
|
|
||||||
# interactive terminal, or a live database before it does anything a test could observe, so no
|
|
||||||
# headless run will ever move them. Left in, they contribute thousands of lines at 0% and drag
|
|
||||||
# the total down by a fixed amount that hides movement in the code we can actually test. The
|
|
||||||
# window header is excluded with its .cpp because gcovr attributes its inline template code
|
|
||||||
# there, which is where most of its line count lives.
|
|
||||||
exclude = ^cockatrice/src/main\.cpp$
|
|
||||||
exclude = ^cockatrice/src/interface/window_main\.
|
|
||||||
exclude = ^oracle/src/main\.cpp$
|
|
||||||
exclude = ^servatrice/src/main\.cpp$
|
|
||||||
|
|
||||||
exclude-throw-branches = yes
|
|
||||||
exclude-function-lines = yes
|
|
||||||
|
|
||||||
print-summary = yes
|
|
||||||
|
|
@ -60,5 +60,3 @@ target_link_libraries(
|
||||||
PUBLIC libcockatrice_utility
|
PUBLIC libcockatrice_utility
|
||||||
PUBLIC ${QT_CORE_MODULE}
|
PUBLIC ${QT_CORE_MODULE}
|
||||||
)
|
)
|
||||||
|
|
||||||
target_precompile_headers(libcockatrice_card PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QMessageLogger>
|
#include <QMessageLogger>
|
||||||
#include <QMetaObject>
|
|
||||||
#include <QMetaType>
|
#include <QMetaType>
|
||||||
#include <QMutexLocker>
|
#include <QMutexLocker>
|
||||||
#include <QSharedPointer>
|
#include <QSharedPointer>
|
||||||
|
|
@ -81,17 +80,15 @@ void CardDatabase::reloadCardDatabasesAndNotify()
|
||||||
{
|
{
|
||||||
// An explicit reload bypasses the binary cache so the files on disk are
|
// An explicit reload bypasses the binary cache so the files on disk are
|
||||||
// always re-read, even when their size/mtime hash still matches the cache.
|
// always re-read, even when their size/mtime hash still matches the cache.
|
||||||
// Models refresh from the swap's cardDatabaseReset in a single batch, so
|
|
||||||
// no cardDatabaseEnabledSetsChanged is emitted here: that rebuilt the
|
|
||||||
// 36k-card models a second time on the GUI thread, which froze startup
|
|
||||||
// after enabling new/custom sets. The follow-up unknown-sets check is
|
|
||||||
// queued to the GUI thread so it sees the swapped-in data (this may run
|
|
||||||
// on a worker via QtConcurrent::run); it is silent unless new sets
|
|
||||||
// appeared mid-reload.
|
|
||||||
loadStatus = loader->loadCardDatabases(/* ignoreCache */ true);
|
loadStatus = loader->loadCardDatabases(/* ignoreCache */ true);
|
||||||
|
|
||||||
if (loadStatus == Ok) {
|
if (loadStatus == Ok) {
|
||||||
QMetaObject::invokeMethod(this, &CardDatabase::checkUnknownSets, Qt::AutoConnection);
|
checkUnknownSets();
|
||||||
|
// A reload reconstructs the exact same enabled-set state, so the cached
|
||||||
|
// set-name / alt-name data computed during construction is still valid.
|
||||||
|
// Skipping the per-card refresh avoids a full 36k-card recompute that
|
||||||
|
// would otherwise duplicate the work already done while building cards.
|
||||||
|
notifyEnabledSetsChanged(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -210,19 +207,15 @@ void CardDatabase::checkUnknownSets()
|
||||||
if (numUnknownSets > 0) {
|
if (numUnknownSets > 0) {
|
||||||
emit cardDatabaseNewSetsFound(numUnknownSets, unknownSetNames);
|
emit cardDatabaseNewSetsFound(numUnknownSets, unknownSetNames);
|
||||||
} else {
|
} else {
|
||||||
_sets.markAllAsKnownBatched(setPriorityController);
|
_sets.markAllAsKnown();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No set enabled. Probably this is the first time running trice
|
// No set enabled. Probably this is the first time running trice
|
||||||
_sets.enableAllBatched(setPriorityController);
|
_sets.guessSortKeys();
|
||||||
_sets.sortByKey();
|
_sets.sortByKey();
|
||||||
|
_sets.enableAll();
|
||||||
|
notifyEnabledSetsChanged();
|
||||||
|
|
||||||
// No cardDatabaseEnabledSetsChanged here: the reload chained off
|
|
||||||
// cardDatabaseAllNewSetsEnabled re-parses with the sets enabled and its
|
|
||||||
// swap's cardDatabaseReset rebuilds the models in one batch. Emitting
|
|
||||||
// both back-to-back rebuilt the 36k-card models twice on the GUI thread,
|
|
||||||
// stalling startup. (When the card list is empty the slot returns early
|
|
||||||
// without reloading, but there is nothing to display either.)
|
|
||||||
emit cardDatabaseAllNewSetsEnabled();
|
emit cardDatabaseAllNewSetsEnabled();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -230,13 +223,13 @@ void CardDatabase::checkUnknownSets()
|
||||||
void CardDatabase::enableAllUnknownSets()
|
void CardDatabase::enableAllUnknownSets()
|
||||||
{
|
{
|
||||||
auto _sets = getSetList();
|
auto _sets = getSetList();
|
||||||
_sets.enableAllUnknownBatched(setPriorityController);
|
_sets.enableAllUnknown();
|
||||||
}
|
}
|
||||||
|
|
||||||
void CardDatabase::markAllSetsAsKnown()
|
void CardDatabase::markAllSetsAsKnown()
|
||||||
{
|
{
|
||||||
auto _sets = getSetList();
|
auto _sets = getSetList();
|
||||||
_sets.markAllAsKnownBatched(setPriorityController);
|
_sets.markAllAsKnown();
|
||||||
}
|
}
|
||||||
|
|
||||||
void CardDatabase::notifyEnabledSetsChanged(bool recomputeCachedSets)
|
void CardDatabase::notifyEnabledSetsChanged(bool recomputeCachedSets)
|
||||||
|
|
|
||||||
|
|
@ -44,13 +44,7 @@ CardDatabaseLoader::CardDatabaseLoader(QObject *parent,
|
||||||
// the finished snapshot is swapped into the live database on the GUI thread.
|
// the finished snapshot is swapped into the live database on the GUI thread.
|
||||||
|
|
||||||
// when SettingsCache's path changes, trigger reloads
|
// when SettingsCache's path changes, trigger reloads
|
||||||
connect(pathProvider, &ICardDatabasePathProvider::cardDatabasePathChanged, this, [this] {
|
connect(pathProvider, &ICardDatabasePathProvider::cardDatabasePathChanged, this, [this] { loadCardDatabases(); });
|
||||||
loadCardDatabases();
|
|
||||||
// This reload runs synchronously on the emitting (GUI) thread, so the
|
|
||||||
// finished snapshot is already swapped in when it returns: enable any
|
|
||||||
// sets the newly pointed-at database introduced.
|
|
||||||
database->checkUnknownSets();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CardDatabaseLoader::~CardDatabaseLoader()
|
CardDatabaseLoader::~CardDatabaseLoader()
|
||||||
|
|
@ -142,23 +136,7 @@ LoadStatus CardDatabaseLoader::doLoadCardDatabases(bool ignoreCache)
|
||||||
loadCardDatabase(p, data);
|
loadCardDatabase(p, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A snapshot parsed while no set is enabled has had every printing
|
if (!saveToCache(data, sourceHash)) {
|
||||||
// stripped by the parsers, and the cache reader re-derives enablement
|
|
||||||
// from the priority controller, so such an entry can never be reused
|
|
||||||
// meaningfully. Skip writing it so a first-run (blank config) load
|
|
||||||
// cannot poison the cache with an empty snapshot. The check mirrors
|
|
||||||
// what the parsers saw: any set still reporting enabled means the
|
|
||||||
// snapshot is complete for the current enablement.
|
|
||||||
bool anySetEnabled = false;
|
|
||||||
for (const CardSetPtr &set : data.sets) {
|
|
||||||
if (set && set->getEnabled()) {
|
|
||||||
anySetEnabled = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!data.sets.isEmpty() && !anySetEnabled) {
|
|
||||||
qCInfo(CardDatabaseLoadingLog) << "Skipping binary cache: no set is enabled yet, snapshot was stripped";
|
|
||||||
} else if (!saveToCache(data, sourceHash)) {
|
|
||||||
qCWarning(CardDatabaseLoadingLog) << "Failed to write binary cache to" << cachePath();
|
qCWarning(CardDatabaseLoadingLog) << "Failed to write binary cache to" << cachePath();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -170,15 +148,18 @@ LoadStatus CardDatabaseLoader::doLoadCardDatabases(bool ignoreCache)
|
||||||
database->refreshCachedReverseRelatedCards(data.cards);
|
database->refreshCachedReverseRelatedCards(data.cards);
|
||||||
|
|
||||||
qCInfo(CardDatabaseLoadingSuccessOrFailureLog) << "Card Database Loading Success";
|
qCInfo(CardDatabaseLoadingSuccessOrFailureLog) << "Card Database Loading Success";
|
||||||
// NOTE: checkUnknownSets() is intentionally NOT called here. During the
|
|
||||||
// front-loaded parse in main() MainWindow does not exist yet, so
|
|
||||||
// cardDatabaseNewSetsFound / cardDatabaseAllNewSetsEnabled would be
|
|
||||||
// emitted with no receivers, and enabling the sets up-front would
|
|
||||||
// suppress the first-run experience MainWindow::startupConfigCheck()
|
|
||||||
// and the onboarding wizard are designed around. Callers that load
|
|
||||||
// after startup own the check (and any reload it triggers).
|
|
||||||
emit databaseDataReady(std::move(data));
|
emit databaseDataReady(std::move(data));
|
||||||
emit loadingFinished();
|
emit loadingFinished();
|
||||||
|
// During the front-loaded parse in main() this runs before MainWindow
|
||||||
|
// exists, so cardDatabaseNewSetsFound / cardDatabaseAllNewSetsEnabled
|
||||||
|
// would be emitted with no receivers. Skip the check on the first load;
|
||||||
|
// MainWindow::startupConfigCheck() calls checkUnknownSets() once its
|
||||||
|
// signal connections are live. On subsequent reloads (e.g. path change)
|
||||||
|
// MainWindow exists and signals have live receivers.
|
||||||
|
if (initialLoadComplete) {
|
||||||
|
database->checkUnknownSets();
|
||||||
|
}
|
||||||
|
initialLoadComplete = true;
|
||||||
} else {
|
} else {
|
||||||
qCInfo(CardDatabaseLoadingSuccessOrFailureLog) << "Card Database Loading Failed";
|
qCInfo(CardDatabaseLoadingSuccessOrFailureLog) << "Card Database Loading Failed";
|
||||||
emit loadingFailed(); // bring up the settings dialog
|
emit loadingFailed(); // bring up the settings dialog
|
||||||
|
|
|
||||||
|
|
@ -179,6 +179,7 @@ private:
|
||||||
|
|
||||||
QBasicMutex *loadFromFileMutex = new QBasicMutex(); /**< Mutex for single-file loading. */
|
QBasicMutex *loadFromFileMutex = new QBasicMutex(); /**< Mutex for single-file loading. */
|
||||||
QBasicMutex *reloadDatabaseMutex = new QBasicMutex(); /**< Mutex for reloading entire database. */
|
QBasicMutex *reloadDatabaseMutex = new QBasicMutex(); /**< Mutex for reloading entire database. */
|
||||||
|
bool initialLoadComplete = false; /**< Set after the first successful load. */
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // COCKATRICE_CARD_DATABASE_LOADER_H
|
#endif // COCKATRICE_CARD_DATABASE_LOADER_H
|
||||||
|
|
|
||||||
|
|
@ -227,10 +227,6 @@ public:
|
||||||
* @param _isknown True if known, false if unknown.
|
* @param _isknown True if known, false if unknown.
|
||||||
*/
|
*/
|
||||||
void setIsKnown(bool _isknown);
|
void setIsKnown(bool _isknown);
|
||||||
void setIsKnownInMemory(bool _isknown)
|
|
||||||
{
|
|
||||||
isknown = _isknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Determines whether the set has incomplete metadata and should be ignored.
|
* @brief Determines whether the set has incomplete metadata and should be ignored.
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
#include "card_set_list.h"
|
#include "card_set_list.h"
|
||||||
|
|
||||||
#include "libcockatrice/card/set/card_set.h"
|
#include "libcockatrice/card/set/card_set.h"
|
||||||
#include "libcockatrice/interfaces/interface_card_set_priority_controller.h"
|
|
||||||
|
|
||||||
#include <QDate>
|
#include <QDate>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QVector>
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <compare>
|
#include <compare>
|
||||||
|
|
||||||
|
|
@ -64,87 +62,60 @@ QStringList CardSetList::getUnknownSetsNames()
|
||||||
return sets;
|
return sets;
|
||||||
}
|
}
|
||||||
|
|
||||||
void CardSetList::enableAllUnknownBatched(ICardSetPriorityController *controller)
|
void CardSetList::enableAllUnknown()
|
||||||
{
|
{
|
||||||
QVector<ICardSetPriorityController::SetSaveData> saveData;
|
|
||||||
for (int i = 0; i < size(); ++i) {
|
for (int i = 0; i < size(); ++i) {
|
||||||
CardSetPtr set = at(i);
|
CardSetPtr set = at(i);
|
||||||
if (set.isNull()) {
|
if (set && !set->getIsKnown() && !set->getIsKnownIgnored()) {
|
||||||
continue;
|
set->setIsKnown(true);
|
||||||
|
set->setEnabled(true);
|
||||||
|
} else if (set && set->getIsKnownIgnored() && !set->getEnabled()) {
|
||||||
|
set->setEnabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool changed = false;
|
|
||||||
if (!set->getIsKnown() && !set->getIsKnownIgnored()) {
|
|
||||||
set->setIsKnownInMemory(true);
|
|
||||||
set->setEnabledInMemory(true);
|
|
||||||
changed = true;
|
|
||||||
} else if (set->getIsKnownIgnored() && !set->getEnabled()) {
|
|
||||||
set->setEnabledInMemory(true);
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
if (changed) {
|
|
||||||
saveData.append(
|
|
||||||
{set->getShortName(), static_cast<unsigned int>(set->getSortKey()), true, set->getIsKnown()});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!saveData.isEmpty()) {
|
|
||||||
controller->saveSets(saveData);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CardSetList::enableAllBatched(ICardSetPriorityController *controller)
|
void CardSetList::enableAll()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < size(); ++i) {
|
||||||
|
CardSetPtr set = at(i);
|
||||||
|
|
||||||
|
if (set == nullptr) {
|
||||||
|
// qCWarning(CardInfoLog) << "enabledAll has null";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!set->getIsKnownIgnored()) {
|
||||||
|
set->setIsKnown(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
set->setEnabled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CardSetList::markAllAsKnown()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < size(); ++i) {
|
||||||
|
CardSetPtr set = at(i);
|
||||||
|
if (set && !set->getIsKnown() && !set->getIsKnownIgnored()) {
|
||||||
|
set->setIsKnown(true);
|
||||||
|
set->setEnabled(false);
|
||||||
|
} else if (set && set->getIsKnownIgnored() && !set->getEnabled()) {
|
||||||
|
set->setEnabled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CardSetList::guessSortKeys()
|
||||||
{
|
{
|
||||||
defaultSort();
|
defaultSort();
|
||||||
|
|
||||||
QVector<ICardSetPriorityController::SetSaveData> saveData;
|
|
||||||
saveData.reserve(size());
|
|
||||||
for (int i = 0; i < size(); ++i) {
|
for (int i = 0; i < size(); ++i) {
|
||||||
CardSetPtr set = at(i);
|
CardSetPtr set = at(i);
|
||||||
if (set.isNull()) {
|
if (set.isNull()) {
|
||||||
|
// qCWarning(CardInfoLog) << "guessSortKeys set is null";
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
set->setSortKey(i);
|
||||||
const auto sortKey = static_cast<unsigned int>(i);
|
|
||||||
set->setSortKeyInMemory(sortKey);
|
|
||||||
if (!set->getIsKnownIgnored()) {
|
|
||||||
set->setIsKnownInMemory(true);
|
|
||||||
}
|
|
||||||
set->setEnabledInMemory(true);
|
|
||||||
saveData.append({set->getShortName(), sortKey, true, set->getIsKnown()});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!saveData.isEmpty()) {
|
|
||||||
controller->saveSets(saveData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void CardSetList::markAllAsKnownBatched(ICardSetPriorityController *controller)
|
|
||||||
{
|
|
||||||
QVector<ICardSetPriorityController::SetSaveData> saveData;
|
|
||||||
for (int i = 0; i < size(); ++i) {
|
|
||||||
CardSetPtr set = at(i);
|
|
||||||
if (set.isNull()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool changed = false;
|
|
||||||
if (!set->getIsKnown() && !set->getIsKnownIgnored()) {
|
|
||||||
set->setIsKnownInMemory(true);
|
|
||||||
set->setEnabledInMemory(false);
|
|
||||||
changed = true;
|
|
||||||
} else if (set->getIsKnownIgnored() && !set->getEnabled()) {
|
|
||||||
set->setEnabledInMemory(true);
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
if (changed) {
|
|
||||||
saveData.append({set->getShortName(), static_cast<unsigned int>(set->getSortKey()), set->getEnabled(),
|
|
||||||
set->getIsKnown()});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!saveData.isEmpty()) {
|
|
||||||
controller->saveSets(saveData);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,37 +40,36 @@ public:
|
||||||
void sortByKey();
|
void sortByKey();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Enables every set with fresh sort keys, persisting in one flush.
|
* @brief Reassigns sort keys based on the current order.
|
||||||
*
|
*
|
||||||
* Assigns sequential sort keys in default-sorted order and marks every set
|
* Calls defaultSort() and then assigns sequential sort keys
|
||||||
* known and enabled, writing all records through the controller's batched
|
* to all sets according to their resulting positions, replacing
|
||||||
* saveSets() in a single settings flush instead of ~3 QSettings rewrite
|
* any existing sort keys to ensure consistent ordering.
|
||||||
* cycles per set, which froze the GUI thread for seconds on a first run
|
|
||||||
* with a full database.
|
|
||||||
* @param controller The set priority controller to persist the records with.
|
|
||||||
*/
|
*/
|
||||||
void enableAllBatched(ICardSetPriorityController *controller);
|
void guessSortKeys();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Enables all sets that are unknown or ignored, persisting in one flush.
|
* @brief Enables all sets that are unknown or ignored.
|
||||||
*
|
*
|
||||||
* Sets that are not marked as known and not ignored are marked as known
|
* Sets that are not marked as known and not ignored are marked as known
|
||||||
* and enabled. Ignored-known sets are also enabled, but remain ignored.
|
* and enabled. Ignored-known sets are also enabled, but remain ignored.
|
||||||
* Only changed sets are written, so the common case of a few new sets costs
|
|
||||||
* a single settings flush no matter how large the database is.
|
|
||||||
* @param controller The set priority controller to persist the records with.
|
|
||||||
*/
|
*/
|
||||||
void enableAllUnknownBatched(ICardSetPriorityController *controller);
|
void enableAllUnknown();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Marks all sets as known and adjusts their enabled state, persisting in one flush.
|
* @brief Enables all sets in the list.
|
||||||
|
*
|
||||||
|
* Equivalent to calling `setEnabled(true)` on each entry.
|
||||||
|
*/
|
||||||
|
void enableAll();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Marks all sets as known and adjusts their enabled state.
|
||||||
*
|
*
|
||||||
* Unknown, non-ignored sets become known and disabled.
|
* Unknown, non-ignored sets become known and disabled.
|
||||||
* Ignored-known sets are enabled if they were previously disabled.
|
* Ignored-known sets are enabled if they were previously disabled.
|
||||||
* Only changed sets are written; when nothing is unknown no flush happens at all.
|
|
||||||
* @param controller The set priority controller to persist the records with.
|
|
||||||
*/
|
*/
|
||||||
void markAllAsKnownBatched(ICardSetPriorityController *controller);
|
void markAllAsKnown();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Counts the number of sets that are currently enabled.
|
* @brief Counts the number of sets that are currently enabled.
|
||||||
|
|
|
||||||
|
|
@ -41,5 +41,3 @@ target_link_libraries(
|
||||||
libcockatrice_deck_list PUBLIC libcockatrice_interfaces libcockatrice_protocol libcockatrice_utility
|
libcockatrice_deck_list PUBLIC libcockatrice_interfaces libcockatrice_protocol libcockatrice_utility
|
||||||
${QT_CORE_MODULE}
|
${QT_CORE_MODULE}
|
||||||
)
|
)
|
||||||
|
|
||||||
target_precompile_headers(libcockatrice_deck_list PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ bool parse(QTextStream &in,
|
||||||
|
|
||||||
// Regex for advanced card parsing
|
// Regex for advanced card parsing
|
||||||
static const QRegularExpression reMultiplier(R"(^[xX\(\[]*(\d+)[xX\*\)\]]* ?(.+))");
|
static const QRegularExpression reMultiplier(R"(^[xX\(\[]*(\d+)[xX\*\)\]]* ?(.+))");
|
||||||
static const QRegularExpression reTabQuantity(R"(^\d+[xX]?$)");
|
|
||||||
|
|
||||||
// Regex for extracting set code and collector number with attached symbols
|
// Regex for extracting set code and collector number with attached symbols
|
||||||
static const QRegularExpression reHyphenFormat(R"(\((\w{3,})\)\s+(\w{3,})-(\d+[^\w\s]*))");
|
static const QRegularExpression reHyphenFormat(R"(\((\w{3,})\)\s+(\w{3,})-(\d+[^\w\s]*))");
|
||||||
|
|
@ -111,34 +110,8 @@ bool parse(QTextStream &in,
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QString rawLine = match.captured();
|
QString cardName = match.captured().simplified();
|
||||||
QString cardName = rawLine.simplified();
|
|
||||||
bool sideboard = false;
|
bool sideboard = false;
|
||||||
int amount = 1;
|
|
||||||
|
|
||||||
// Card names never contain tab characters, so a leading numeric field
|
|
||||||
// marks a tabular record emitted by third-party deck tools (e.g. Delver
|
|
||||||
// Lens "2\tCard Name\tSet Name"). The first two columns are the quantity
|
|
||||||
// and the card name; any further columns are printer/set metadata and
|
|
||||||
// are discarded.
|
|
||||||
bool hasTabQuantity = false;
|
|
||||||
const auto tabFields = rawLine.split('\t');
|
|
||||||
int firstField = 0;
|
|
||||||
while (firstField < tabFields.size() && tabFields.at(firstField).trimmed().isEmpty()) {
|
|
||||||
++firstField;
|
|
||||||
}
|
|
||||||
if (firstField + 1 < tabFields.size()) {
|
|
||||||
const QString quantity = tabFields.at(firstField).trimmed();
|
|
||||||
if (reTabQuantity.match(quantity).hasMatch()) {
|
|
||||||
hasTabQuantity = true;
|
|
||||||
QString digits = quantity;
|
|
||||||
if (digits.endsWith('x') || digits.endsWith('X')) {
|
|
||||||
digits.chop(1);
|
|
||||||
}
|
|
||||||
amount = digits.toInt();
|
|
||||||
cardName = tabFields.at(firstField + 1).simplified();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sideboard detection
|
// Sideboard detection
|
||||||
if (sBStart < 0) {
|
if (sBStart < 0) {
|
||||||
|
|
@ -183,12 +156,11 @@ bool parse(QTextStream &in,
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if a specific amount is mentioned
|
// check if a specific amount is mentioned
|
||||||
if (!hasTabQuantity) {
|
int amount = 1;
|
||||||
match = reMultiplier.match(cardName);
|
match = reMultiplier.match(cardName);
|
||||||
if (match.hasMatch()) {
|
if (match.hasMatch()) {
|
||||||
amount = match.captured(1).toInt();
|
amount = match.captured(1).toInt();
|
||||||
cardName = match.captured(2);
|
cardName = match.captured(2);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize the card name
|
// Normalize the card name
|
||||||
|
|
|
||||||
|
|
@ -18,5 +18,3 @@ add_dependencies(libcockatrice_filters libcockatrice_card)
|
||||||
target_include_directories(libcockatrice_filters PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
target_include_directories(libcockatrice_filters PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
|
|
||||||
target_link_libraries(libcockatrice_filters PUBLIC libcockatrice_card ${QT_CORE_MODULE})
|
target_link_libraries(libcockatrice_filters PUBLIC libcockatrice_card ${QT_CORE_MODULE})
|
||||||
|
|
||||||
target_precompile_headers(libcockatrice_filters PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
|
||||||
|
|
|
||||||
|
|
@ -31,5 +31,3 @@ add_library(libcockatrice_interfaces STATIC ${MOC_SOURCES})
|
||||||
target_include_directories(libcockatrice_interfaces PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
target_include_directories(libcockatrice_interfaces PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
|
|
||||||
target_link_libraries(libcockatrice_interfaces PUBLIC libcockatrice_utility ${QT_CORE_MODULE})
|
target_link_libraries(libcockatrice_interfaces PUBLIC libcockatrice_utility ${QT_CORE_MODULE})
|
||||||
|
|
||||||
target_precompile_headers(libcockatrice_interfaces PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ public:
|
||||||
QString shortName;
|
QString shortName;
|
||||||
unsigned int sortKey;
|
unsigned int sortKey;
|
||||||
bool enabled;
|
bool enabled;
|
||||||
bool isKnown = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -58,14 +57,6 @@ public:
|
||||||
*/
|
*/
|
||||||
virtual QStringList getEnabledSetNames() const = 0;
|
virtual QStringList getEnabledSetNames() const = 0;
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Persists per-set options for every entry in a single settings flush.
|
|
||||||
*
|
|
||||||
* Each individual setSortKey/setEnabled/setIsKnown call re-opens and re-syncs
|
|
||||||
* the whole .ini when backed by QSettings, so persisting hundreds of sets
|
|
||||||
* one option at a time freezes the GUI thread for seconds. Callers that touch
|
|
||||||
* more than a handful of sets must bundle the records and call this once.
|
|
||||||
*/
|
|
||||||
virtual void saveSets(const QVector<SetSaveData> &data) = 0;
|
virtual void saveSets(const QVector<SetSaveData> &data) = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,5 +29,3 @@ add_library(
|
||||||
target_include_directories(libcockatrice_models_database PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
target_include_directories(libcockatrice_models_database PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
|
|
||||||
target_link_libraries(libcockatrice_models_database PUBLIC libcockatrice_card libcockatrice_filters ${QT_CORE_MODULE})
|
target_link_libraries(libcockatrice_models_database PUBLIC libcockatrice_card libcockatrice_filters ${QT_CORE_MODULE})
|
||||||
|
|
||||||
target_precompile_headers(libcockatrice_models_database PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
|
||||||
|
|
|
||||||
|
|
@ -266,7 +266,7 @@ void SetsModel::save(CardDatabase *db)
|
||||||
const bool enabled = enabledSets.contains(sets[i]);
|
const bool enabled = enabledSets.contains(sets[i]);
|
||||||
sets[i]->setSortKeyInMemory(sortKey);
|
sets[i]->setSortKeyInMemory(sortKey);
|
||||||
sets[i]->setEnabledInMemory(enabled);
|
sets[i]->setEnabledInMemory(enabled);
|
||||||
saveData.append({sets[i]->getShortName(), sortKey, enabled, sets[i]->getIsKnown()});
|
saveData.append({sets[i]->getShortName(), sortKey, enabled});
|
||||||
}
|
}
|
||||||
|
|
||||||
db->getPriorityController()->saveSets(saveData);
|
db->getPriorityController()->saveSets(saveData);
|
||||||
|
|
|
||||||
|
|
@ -16,5 +16,3 @@ target_include_directories(libcockatrice_models_deck_list PUBLIC ${CMAKE_CURRENT
|
||||||
target_link_libraries(
|
target_link_libraries(
|
||||||
libcockatrice_models_deck_list PUBLIC libcockatrice_card libcockatrice_deck_list ${QT_CORE_MODULE}
|
libcockatrice_models_deck_list PUBLIC libcockatrice_card libcockatrice_deck_list ${QT_CORE_MODULE}
|
||||||
)
|
)
|
||||||
|
|
||||||
target_precompile_headers(libcockatrice_models_deck_list PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
|
||||||
|
|
|
||||||
|
|
@ -19,5 +19,3 @@ target_link_libraries(
|
||||||
PUBLIC ${COCKATRICE_QT_VERSION_NAME}::Network ${COCKATRICE_QT_VERSION_NAME}::WebSockets libcockatrice_protocol
|
PUBLIC ${COCKATRICE_QT_VERSION_NAME}::Network ${COCKATRICE_QT_VERSION_NAME}::WebSockets libcockatrice_protocol
|
||||||
libcockatrice_network_server_remote
|
libcockatrice_network_server_remote
|
||||||
)
|
)
|
||||||
|
|
||||||
target_precompile_headers(libcockatrice_network_client_abstract PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
|
||||||
|
|
|
||||||
|
|
@ -19,5 +19,3 @@ target_link_libraries(
|
||||||
PUBLIC ${COCKATRICE_QT_VERSION_NAME}::Network ${COCKATRICE_QT_VERSION_NAME}::WebSockets
|
PUBLIC ${COCKATRICE_QT_VERSION_NAME}::Network ${COCKATRICE_QT_VERSION_NAME}::WebSockets
|
||||||
libcockatrice_network_client_abstract
|
libcockatrice_network_client_abstract
|
||||||
)
|
)
|
||||||
|
|
||||||
target_precompile_headers(libcockatrice_network_client_local PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
|
||||||
|
|
|
||||||
|
|
@ -19,5 +19,3 @@ target_link_libraries(
|
||||||
PUBLIC ${COCKATRICE_QT_VERSION_NAME}::Network ${COCKATRICE_QT_VERSION_NAME}::WebSockets
|
PUBLIC ${COCKATRICE_QT_VERSION_NAME}::Network ${COCKATRICE_QT_VERSION_NAME}::WebSockets
|
||||||
libcockatrice_network_client_abstract libcockatrice_interfaces libcockatrice_utility libcockatrice_protocol
|
libcockatrice_network_client_abstract libcockatrice_interfaces libcockatrice_utility libcockatrice_protocol
|
||||||
)
|
)
|
||||||
|
|
||||||
target_precompile_headers(libcockatrice_network_client_remote PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
|
||||||
|
|
|
||||||
|
|
@ -15,5 +15,3 @@ add_dependencies(libcockatrice_network_server_local libcockatrice_protocol)
|
||||||
target_include_directories(libcockatrice_network_server_local PUBLIC .)
|
target_include_directories(libcockatrice_network_server_local PUBLIC .)
|
||||||
|
|
||||||
target_link_libraries(libcockatrice_network_server_local PUBLIC ${COCKATRICE_QT_MODULES} libcockatrice_protocol)
|
target_link_libraries(libcockatrice_network_server_local PUBLIC ${COCKATRICE_QT_MODULES} libcockatrice_protocol)
|
||||||
|
|
||||||
target_precompile_headers(libcockatrice_network_server_local PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ set(CMAKE_AUTOUIC ON)
|
||||||
set(CMAKE_AUTORCC ON)
|
set(CMAKE_AUTORCC ON)
|
||||||
|
|
||||||
set(HEADERS
|
set(HEADERS
|
||||||
game/dump_zone_range.h
|
|
||||||
game/server_abstract_participant.h
|
game/server_abstract_participant.h
|
||||||
game/server_abstract_player.h
|
game/server_abstract_player.h
|
||||||
game/server_arrow.h
|
game/server_arrow.h
|
||||||
|
|
@ -34,7 +33,6 @@ qt6_wrap_cpp(MOC_SOURCES ${HEADERS})
|
||||||
add_library(
|
add_library(
|
||||||
libcockatrice_network_server_remote STATIC
|
libcockatrice_network_server_remote STATIC
|
||||||
${MOC_SOURCES}
|
${MOC_SOURCES}
|
||||||
game/dump_zone_range.cpp
|
|
||||||
game/server_abstract_participant.cpp
|
game/server_abstract_participant.cpp
|
||||||
game/server_abstract_player.cpp
|
game/server_abstract_player.cpp
|
||||||
game/server_arrow.cpp
|
game/server_arrow.cpp
|
||||||
|
|
@ -64,5 +62,3 @@ target_link_libraries(
|
||||||
libcockatrice_network_server_remote PUBLIC libcockatrice_protocol libcockatrice_utility libcockatrice_rng
|
libcockatrice_network_server_remote PUBLIC libcockatrice_protocol libcockatrice_utility libcockatrice_rng
|
||||||
libcockatrice_deck_list ${COCKATRICE_QT_MODULES}
|
libcockatrice_deck_list ${COCKATRICE_QT_MODULES}
|
||||||
)
|
)
|
||||||
|
|
||||||
target_precompile_headers(libcockatrice_network_server_remote PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
|
||||||
|
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
#include "dump_zone_range.h"
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
|
|
||||||
std::pair<qsizetype, qsizetype> dumpZoneCardRange(qsizetype cardCount, int numberCards, bool isReversed)
|
|
||||||
{
|
|
||||||
qsizetype count = 0;
|
|
||||||
if (numberCards == -1) {
|
|
||||||
count = cardCount;
|
|
||||||
} else if (numberCards > 0) {
|
|
||||||
count = std::min<qsizetype>(numberCards, cardCount);
|
|
||||||
}
|
|
||||||
count = std::max<qsizetype>(count, 0);
|
|
||||||
|
|
||||||
const qsizetype begin = isReversed ? cardCount - count : 0;
|
|
||||||
return {begin, begin + count};
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
#ifndef DUMP_ZONE_RANGE_H
|
|
||||||
#define DUMP_ZONE_RANGE_H
|
|
||||||
|
|
||||||
#include <QtGlobal>
|
|
||||||
#include <utility>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Resolve a client's zone dump request against a zone's real card count.
|
|
||||||
*
|
|
||||||
* @c number_cards arrives from the client unvalidated. -1 means "the whole zone", which
|
|
||||||
* is the convention the client uses when sizing its own view of one. Every other negative
|
|
||||||
* value, and every value larger than the zone, has to be brought into range rather than
|
|
||||||
* used as an index: with a reversed request the zone offset is
|
|
||||||
* @c cardCount - @c numberCards, which runs off the front of the list when the request
|
|
||||||
* exceeds the zone and off the back when @c numberCards is -1.
|
|
||||||
*
|
|
||||||
* Kept in its own translation unit so it can be exercised without the rest of
|
|
||||||
* server_abstract_player.cpp, which refers to the RNG global that only the servatrice
|
|
||||||
* binary defines.
|
|
||||||
*
|
|
||||||
* @param cardCount number of cards the zone actually holds
|
|
||||||
* @param numberCards the requested count, -1 for the whole zone
|
|
||||||
* @param isReversed take the trailing cards instead of the leading ones
|
|
||||||
* @return the half-open index range [begin, end) into the zone's card list, always
|
|
||||||
* contained in [0, cardCount)
|
|
||||||
*/
|
|
||||||
std::pair<qsizetype, qsizetype> dumpZoneCardRange(qsizetype cardCount, int numberCards, bool isReversed);
|
|
||||||
|
|
||||||
#endif // DUMP_ZONE_RANGE_H
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
#include "server_abstract_player.h"
|
#include "server_abstract_player.h"
|
||||||
|
|
||||||
#include "dump_zone_range.h"
|
|
||||||
#include "game/server_abstract_participant.h"
|
#include "game/server_abstract_participant.h"
|
||||||
#include "game/server_arrowtarget.h"
|
#include "game/server_arrowtarget.h"
|
||||||
#include "libcockatrice/protocol/pb/card_attributes.pb.h"
|
#include "libcockatrice/protocol/pb/card_attributes.pb.h"
|
||||||
|
|
@ -1436,23 +1435,23 @@ Server_AbstractPlayer::cmdDumpZone(const Command_DumpZone &cmd, ResponseContaine
|
||||||
|
|
||||||
int numberCards = cmd.number_cards();
|
int numberCards = cmd.number_cards();
|
||||||
const QList<Server_Card *> &cards = zone->getCards();
|
const QList<Server_Card *> &cards = zone->getCards();
|
||||||
const auto cardRange = dumpZoneCardRange(cards.size(), numberCards, cmd.is_reversed());
|
|
||||||
|
|
||||||
auto *re = new Response_DumpZone;
|
auto *re = new Response_DumpZone;
|
||||||
ServerInfo_Zone *zoneInfo = re->mutable_zone_info();
|
ServerInfo_Zone *zoneInfo = re->mutable_zone_info();
|
||||||
zoneInfo->set_name(zone->getName().toStdString());
|
zoneInfo->set_name(zone->getName().toStdString());
|
||||||
zoneInfo->set_type(zone->getType());
|
zoneInfo->set_type(zone->getType());
|
||||||
zoneInfo->set_with_coords(zone->hasCoords());
|
zoneInfo->set_with_coords(zone->hasCoords());
|
||||||
zoneInfo->set_card_count(cards.size());
|
zoneInfo->set_card_count(numberCards < cards.size() ? cards.size() : numberCards);
|
||||||
|
|
||||||
for (qsizetype findId = cardRange.first; findId < cardRange.second; ++findId) {
|
for (int i = 0; (i < cards.size()) && (i < numberCards || numberCards == -1); ++i) {
|
||||||
Server_Card *card = cards.at(findId);
|
const auto &findId = cmd.is_reversed() ? cards.size() - numberCards + i : i;
|
||||||
|
Server_Card *card = cards[findId];
|
||||||
QString displayedName = card->getFaceDown() ? QString() : card->getName();
|
QString displayedName = card->getFaceDown() ? QString() : card->getName();
|
||||||
ServerInfo_Card *cardInfo = zoneInfo->add_card_list();
|
ServerInfo_Card *cardInfo = zoneInfo->add_card_list();
|
||||||
cardInfo->set_provider_id(card->getProviderId().toStdString());
|
cardInfo->set_provider_id(card->getProviderId().toStdString());
|
||||||
cardInfo->set_name(displayedName.toStdString());
|
cardInfo->set_name(displayedName.toStdString());
|
||||||
if (zone->getType() == ServerInfo_Zone::HiddenZone) {
|
if (zone->getType() == ServerInfo_Zone::HiddenZone) {
|
||||||
cardInfo->set_id(static_cast<int>(findId));
|
cardInfo->set_id(findId);
|
||||||
} else {
|
} else {
|
||||||
cardInfo->set_id(card->getId());
|
cardInfo->set_id(card->getId());
|
||||||
cardInfo->set_x(card->getX());
|
cardInfo->set_x(card->getX());
|
||||||
|
|
|
||||||
|
|
@ -56,10 +56,6 @@ public:
|
||||||
{
|
{
|
||||||
return deck;
|
return deck;
|
||||||
}
|
}
|
||||||
bool getSideboardLocked() const
|
|
||||||
{
|
|
||||||
return sideboardLocked;
|
|
||||||
}
|
|
||||||
bool getReadyStart() const
|
bool getReadyStart() const
|
||||||
{
|
{
|
||||||
return readyStart;
|
return readyStart;
|
||||||
|
|
|
||||||
|
|
@ -61,8 +61,6 @@
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <google/protobuf/descriptor.h>
|
#include <google/protobuf/descriptor.h>
|
||||||
#include <google/protobuf/message.h>
|
#include <google/protobuf/message.h>
|
||||||
#include <libcockatrice/deck_list/deck_list.h>
|
|
||||||
#include <libcockatrice/protocol/pb/context_deck_select.pb.h>
|
|
||||||
#include <libcockatrice/protocol/pb/context_ping_changed.pb.h>
|
#include <libcockatrice/protocol/pb/context_ping_changed.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/event_delete_arrow.pb.h>
|
#include <libcockatrice/protocol/pb/event_delete_arrow.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/event_game_closed.pb.h>
|
#include <libcockatrice/protocol/pb/event_game_closed.pb.h>
|
||||||
|
|
@ -827,48 +825,6 @@ void Server_Game::createGameJoinedEvent(Server_AbstractParticipant *joiningParti
|
||||||
}
|
}
|
||||||
|
|
||||||
rc.enqueuePostResponseItem(ServerMessage::GAME_EVENT_CONTAINER, prepareGameEvent(event2, -1));
|
rc.enqueuePostResponseItem(ServerMessage::GAME_EVENT_CONTAINER, prepareGameEvent(event2, -1));
|
||||||
|
|
||||||
// A newly joined player never saw the "Player X has loaded a deck" messages
|
|
||||||
// (they are only broadcast when a deck is selected), so replay a deck-select
|
|
||||||
// context for every player that already has a deck loaded. It is delivered
|
|
||||||
// privately to the joining participant, whose client renders it as a log
|
|
||||||
// entry in the same way as a fresh deck selection. Skipped on reconnect
|
|
||||||
// (resuming) — the player already saw these — and once the game has started,
|
|
||||||
// when the message is no longer useful and would only spam the log.
|
|
||||||
if (!resuming && !gameStarted) {
|
|
||||||
for (auto *participant : participants.values()) {
|
|
||||||
if (participant == joiningParticipant) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
auto *deckOwner = qobject_cast<Server_Player *>(participant);
|
|
||||||
if (deckOwner == nullptr) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const DeckList *deckList = deckOwner->getDeckList();
|
|
||||||
if (deckList == nullptr) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Event_PlayerPropertiesChanged event;
|
|
||||||
event.mutable_player_properties()->set_sideboard_locked(deckOwner->getSideboardLocked());
|
|
||||||
event.mutable_player_properties()->set_deck_hash(deckList->getDeckHash().toStdString());
|
|
||||||
|
|
||||||
Context_DeckSelect deckSelect;
|
|
||||||
deckSelect.set_deck_hash(deckList->getDeckHash().toStdString());
|
|
||||||
deckSelect.set_sideboard_size(deckList->getSideboardSize());
|
|
||||||
if (getShareDecklistsOnLoad()) {
|
|
||||||
deckSelect.set_deck_list(deckList->writeToString_Native().toStdString());
|
|
||||||
}
|
|
||||||
|
|
||||||
GameEventContext context;
|
|
||||||
context.GetReflection()
|
|
||||||
->MutableMessage(&context, deckSelect.GetDescriptor()->FindExtensionByName("ext"))
|
|
||||||
->CopyFrom(deckSelect);
|
|
||||||
|
|
||||||
rc.enqueuePostResponseItem(ServerMessage::GAME_EVENT_CONTAINER,
|
|
||||||
prepareGameEvent(event, deckOwner->getPlayerId(), &context));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Server_Game::sendGameEventContainer(GameEventContainer *cont,
|
void Server_Game::sendGameEventContainer(GameEventContainer *cont,
|
||||||
|
|
|
||||||
|
|
@ -187,39 +187,6 @@ public:
|
||||||
virtual void observeGameStartDurationMs(qint64 /* elapsedMs */)
|
virtual void observeGameStartDurationMs(qint64 /* elapsedMs */)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* @brief Record a failed login attempt for the (address, username) pair; returns true if now locked out.
|
|
||||||
*
|
|
||||||
* Keyed by pair so a successful login as one user cannot wipe out another
|
|
||||||
* user's failure history from the same address (e.g. a guest login or the
|
|
||||||
* attacker's own account clearing the way for further guessing).
|
|
||||||
*/
|
|
||||||
virtual bool recordFailedLogin(const QString & /*ipAddress*/, const QString & /*userName*/)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @brief True if the (address, username) pair is already locked out of logging in.
|
|
||||||
*
|
|
||||||
* Consulted before any authentication work (database round trip, password
|
|
||||||
* verification) so a blocked pair cannot burn server CPU per attempt.
|
|
||||||
*/
|
|
||||||
virtual bool isLoginRateLimited(const QString & /*ipAddress*/, const QString & /*userName*/)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @brief Seconds remaining before the login rate limit for the (address, username) pair lifts.
|
|
||||||
* Returns 0 if not limited.
|
|
||||||
*/
|
|
||||||
virtual int loginRateLimitRemainingSeconds(const QString & /*ipAddress*/, const QString & /*userName*/)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
/** @brief Clear any failed-login lockout for the (address, username) pair, e.g. after a successful login. */
|
|
||||||
virtual void clearFailedLogins(const QString & /*ipAddress*/, const QString & /*userName*/)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
Server_DatabaseInterface *getDatabaseInterface() const;
|
Server_DatabaseInterface *getDatabaseInterface() const;
|
||||||
int getNextLocalGameId()
|
int getNextLocalGameId()
|
||||||
|
|
|
||||||
|
|
@ -592,17 +592,6 @@ Response::ResponseCode Server_ProtocolHandler::cmdLogin(const Command_Login &cmd
|
||||||
QString reasonStr;
|
QString reasonStr;
|
||||||
int banSecondsLeft = 0;
|
int banSecondsLeft = 0;
|
||||||
QString connectionType = getConnectionType();
|
QString connectionType = getConnectionType();
|
||||||
// Throttle before doing any work: a locked-out address must not get a full
|
|
||||||
// database round trip and password verification on every attempt.
|
|
||||||
if (server->isLoginRateLimited(getAddress(), userName)) {
|
|
||||||
int remaining = server->loginRateLimitRemainingSeconds(getAddress(), userName);
|
|
||||||
if (remaining > 0) {
|
|
||||||
auto *re = new Response_Login;
|
|
||||||
re->set_denied_end_time(QDateTime::currentDateTime().addSecs(remaining).toSecsSinceEpoch());
|
|
||||||
rc.setResponseExtension(re);
|
|
||||||
}
|
|
||||||
return Response::RespTooManyRequests;
|
|
||||||
}
|
|
||||||
AuthenticationResult res = server->loginUser(this, userName, password, needsHash, reasonStr, banSecondsLeft,
|
AuthenticationResult res = server->loginUser(this, userName, password, needsHash, reasonStr, banSecondsLeft,
|
||||||
clientId, clientVersion, connectionType);
|
clientId, clientVersion, connectionType);
|
||||||
switch (res) {
|
switch (res) {
|
||||||
|
|
@ -616,20 +605,10 @@ Response::ResponseCode Server_ProtocolHandler::cmdLogin(const Command_Login &cmd
|
||||||
return Response::RespUserIsBanned;
|
return Response::RespUserIsBanned;
|
||||||
}
|
}
|
||||||
case NotLoggedIn:
|
case NotLoggedIn:
|
||||||
if (server->recordFailedLogin(getAddress(), userName)) {
|
|
||||||
int remaining = server->loginRateLimitRemainingSeconds(getAddress(), userName);
|
|
||||||
if (remaining > 0) {
|
|
||||||
auto *re = new Response_Login;
|
|
||||||
re->set_denied_end_time(QDateTime::currentDateTime().addSecs(remaining).toSecsSinceEpoch());
|
|
||||||
rc.setResponseExtension(re);
|
|
||||||
}
|
|
||||||
return Response::RespTooManyRequests;
|
|
||||||
}
|
|
||||||
return Response::RespWrongPassword;
|
return Response::RespWrongPassword;
|
||||||
case WouldOverwriteOldSession:
|
case WouldOverwriteOldSession:
|
||||||
return Response::RespWouldOverwriteOldSession;
|
return Response::RespWouldOverwriteOldSession;
|
||||||
case UsernameInvalid: {
|
case UsernameInvalid: {
|
||||||
server->recordFailedLogin(getAddress(), userName);
|
|
||||||
auto *re = new Response_Login;
|
auto *re = new Response_Login;
|
||||||
re->set_denied_reason_str(reasonStr.toStdString());
|
re->set_denied_reason_str(reasonStr.toStdString());
|
||||||
rc.setResponseExtension(re);
|
rc.setResponseExtension(re);
|
||||||
|
|
@ -640,12 +619,10 @@ Response::ResponseCode Server_ProtocolHandler::cmdLogin(const Command_Login &cmd
|
||||||
case ClientIdRequired:
|
case ClientIdRequired:
|
||||||
return Response::RespClientIdRequired;
|
return Response::RespClientIdRequired;
|
||||||
case UserIsInactive:
|
case UserIsInactive:
|
||||||
server->recordFailedLogin(getAddress(), userName);
|
|
||||||
return Response::RespAccountNotActivated;
|
return Response::RespAccountNotActivated;
|
||||||
case PasswordChangeRequired:
|
case PasswordChangeRequired:
|
||||||
return Response::RespPasswordChangeRequired;
|
return Response::RespPasswordChangeRequired;
|
||||||
default:
|
default:
|
||||||
server->clearFailedLogins(getAddress(), userName);
|
|
||||||
authState = res;
|
authState = res;
|
||||||
usingRealPassword = needsHash;
|
usingRealPassword = needsHash;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,5 +24,3 @@ target_include_directories(
|
||||||
libcockatrice_protocol PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
|
libcockatrice_protocol PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
${CMAKE_CURRENT_BINARY_DIR} # points to the generated headers
|
${CMAKE_CURRENT_BINARY_DIR} # points to the generated headers
|
||||||
)
|
)
|
||||||
|
|
||||||
target_precompile_headers(libcockatrice_protocol PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,6 @@ set(PROTO_FILES
|
||||||
command_report_details.proto
|
command_report_details.proto
|
||||||
command_report_list.proto
|
command_report_list.proto
|
||||||
command_report_my_list.proto
|
command_report_my_list.proto
|
||||||
command_report_reopen.proto
|
|
||||||
command_report_resolve.proto
|
command_report_resolve.proto
|
||||||
command_report_stats.proto
|
command_report_stats.proto
|
||||||
command_report_user_info.proto
|
command_report_user_info.proto
|
||||||
|
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
syntax = "proto2";
|
|
||||||
import "moderator_commands.proto";
|
|
||||||
|
|
||||||
message Command_ReportReopen {
|
|
||||||
extend ModeratorCommand {
|
|
||||||
optional Command_ReportReopen ext = 1206;
|
|
||||||
}
|
|
||||||
required int32 report_id = 1;
|
|
||||||
optional string reopen_note = 2;
|
|
||||||
}
|
|
||||||
|
|
@ -26,7 +26,6 @@ message ModeratorCommand {
|
||||||
REPLAY_DOWNLOAD_BY_GAME_ID = 1203;
|
REPLAY_DOWNLOAD_BY_GAME_ID = 1203;
|
||||||
REPORT_USER_INFO = 1204;
|
REPORT_USER_INFO = 1204;
|
||||||
REPORT_STATS = 1205;
|
REPORT_STATS = 1205;
|
||||||
REPORT_REOPEN = 1206;
|
|
||||||
}
|
}
|
||||||
extensions 100 to max;
|
extensions 100 to max;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,4 @@ message ServerInfo_Report {
|
||||||
optional string chat_log = 14;
|
optional string chat_log = 14;
|
||||||
|
|
||||||
optional string resolution_note = 15;
|
optional string resolution_note = 15;
|
||||||
optional string resolved_by_name = 16;
|
|
||||||
|
|
||||||
optional int32 reported_user_level = 17;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,6 @@ add_library(
|
||||||
libcockatrice/rng/sfmt/SFMT.c
|
libcockatrice/rng/sfmt/SFMT.c
|
||||||
)
|
)
|
||||||
|
|
||||||
# SFMT is vendored and its own headers trip UBSan's alignment and vptr checks.
|
|
||||||
cockatrice_disable_sanitizers_for(libcockatrice/rng/sfmt/SFMT.c)
|
|
||||||
cockatrice_disable_coverage_for(libcockatrice/rng/sfmt/SFMT.c)
|
|
||||||
|
|
||||||
target_include_directories(libcockatrice_rng PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
target_include_directories(libcockatrice_rng PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
|
|
||||||
target_link_libraries(libcockatrice_rng PUBLIC ${QT_CORE_MODULE})
|
target_link_libraries(libcockatrice_rng PUBLIC ${QT_CORE_MODULE})
|
||||||
|
|
|
||||||
|
|
@ -71,5 +71,3 @@ target_include_directories(
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(libcockatrice_settings PUBLIC libcockatrice_card libcockatrice_utility ${QT_CORE_MODULE})
|
target_link_libraries(libcockatrice_settings PUBLIC libcockatrice_card libcockatrice_utility ${QT_CORE_MODULE})
|
||||||
|
|
||||||
target_precompile_headers(libcockatrice_settings PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,6 @@ void CardDatabaseSettings::saveSets(const QVector<ICardSetPriorityController::Se
|
||||||
s.beginGroup(entry.shortName);
|
s.beginGroup(entry.shortName);
|
||||||
s.setValue("sortKey", entry.sortKey);
|
s.setValue("sortKey", entry.sortKey);
|
||||||
s.setValue("enabled", entry.enabled);
|
s.setValue("enabled", entry.enabled);
|
||||||
s.setValue("isKnown", entry.isKnown);
|
|
||||||
s.endGroup();
|
s.endGroup();
|
||||||
}
|
}
|
||||||
s.endGroup();
|
s.endGroup();
|
||||||
|
|
@ -122,6 +121,5 @@ void CardDatabaseSettings::saveSets(const QVector<ICardSetPriorityController::Se
|
||||||
SetOptions &o = setOptionsCache[entry.shortName];
|
SetOptions &o = setOptionsCache[entry.shortName];
|
||||||
o.sortKey = entry.sortKey;
|
o.sortKey = entry.sortKey;
|
||||||
o.enabled = entry.enabled;
|
o.enabled = entry.enabled;
|
||||||
o.isKnown = entry.isKnown;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2,8 +2,6 @@
|
||||||
|
|
||||||
#include "libcockatrice/settings/settings_manager.h"
|
#include "libcockatrice/settings/settings_manager.h"
|
||||||
|
|
||||||
#include <QDir>
|
|
||||||
#include <QStringList>
|
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
|
|
||||||
class QObject;
|
class QObject;
|
||||||
|
|
@ -123,52 +121,3 @@ void PathsSettings::setSpoilerDatabasePath(const QString &_spoilerDatabasePath)
|
||||||
setValue(_spoilerDatabasePath, "spoilerDatabase");
|
setValue(_spoilerDatabasePath, "spoilerDatabase");
|
||||||
emit cardDatabasePathChanged();
|
emit cardDatabasePathChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
QStringList PathsSettings::pathsInsideDir(const QString &baseDir) const
|
|
||||||
{
|
|
||||||
QStringList offenders;
|
|
||||||
const auto check = [&offenders, &baseDir](const QString &path) {
|
|
||||||
if (!path.isEmpty() && isInsideDir(path, baseDir)) {
|
|
||||||
offenders.append(QDir::cleanPath(path));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
check(getDeckPath());
|
|
||||||
check(getFiltersPath());
|
|
||||||
check(getReplaysPath());
|
|
||||||
check(getPicsPath());
|
|
||||||
check(getCustomPicsPath());
|
|
||||||
check(getThemesPath());
|
|
||||||
check(getCardDatabasePath());
|
|
||||||
check(getCustomCardDatabasePath());
|
|
||||||
check(getTokenDatabasePath());
|
|
||||||
check(getSpoilerCardDatabasePath());
|
|
||||||
check(getRedirectCachePath());
|
|
||||||
|
|
||||||
offenders.removeDuplicates();
|
|
||||||
return offenders;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool PathsSettings::isInsideDir(const QString &path, const QString &baseDir)
|
|
||||||
{
|
|
||||||
#if defined(Q_OS_WIN) || defined(Q_OS_MACOS)
|
|
||||||
// Windows and default macOS volumes are case-insensitive filesystems.
|
|
||||||
constexpr Qt::CaseSensitivity caseSensitivity = Qt::CaseInsensitive;
|
|
||||||
#else
|
|
||||||
constexpr Qt::CaseSensitivity caseSensitivity = Qt::CaseSensitive;
|
|
||||||
#endif
|
|
||||||
const QString base = QDir::cleanPath(baseDir);
|
|
||||||
const QString cleaned = QDir::cleanPath(path);
|
|
||||||
const QString prefix = base.endsWith(QLatin1Char('/')) ? base : base + QLatin1Char('/');
|
|
||||||
return cleaned == base || cleaned.startsWith(prefix, caseSensitivity);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool PathsSettings::getAppDirWarningAcknowledged() const
|
|
||||||
{
|
|
||||||
return getValue("appDirWarningAcknowledged").toBool();
|
|
||||||
}
|
|
||||||
|
|
||||||
void PathsSettings::setAppDirWarningAcknowledged(bool _appDirWarningAcknowledged)
|
|
||||||
{
|
|
||||||
setValue(_appDirWarningAcknowledged, "appDirWarningAcknowledged");
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
#include "settings_manager.h"
|
#include "settings_manager.h"
|
||||||
|
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QStringList>
|
|
||||||
#include <libcockatrice/interfaces/interface_paths_settings_provider.h>
|
#include <libcockatrice/interfaces/interface_paths_settings_provider.h>
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
|
|
||||||
|
|
@ -28,24 +27,6 @@ public:
|
||||||
[[nodiscard]] QString getSpoilerCardDatabasePath() const override;
|
[[nodiscard]] QString getSpoilerCardDatabasePath() const override;
|
||||||
[[nodiscard]] QString getRedirectCachePath() const override;
|
[[nodiscard]] QString getRedirectCachePath() const override;
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns the configured paths that resolve inside baseDir.
|
|
||||||
*
|
|
||||||
* Comparison is lexical (QDir::cleanPath); symlinks are not resolved, so a
|
|
||||||
* path that only reaches baseDir through a symlink is not reported.
|
|
||||||
*/
|
|
||||||
[[nodiscard]] QStringList pathsInsideDir(const QString &baseDir) const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Whether path resolves inside baseDir, as a single-path check.
|
|
||||||
*
|
|
||||||
* A path counts as inside when it equals baseDir or is nested below it;
|
|
||||||
* a sibling sharing the name prefix (baseDir + "2") does not.
|
|
||||||
*/
|
|
||||||
[[nodiscard]] static bool isInsideDir(const QString &path, const QString &baseDir);
|
|
||||||
|
|
||||||
[[nodiscard]] bool getAppDirWarningAcknowledged() const;
|
|
||||||
|
|
||||||
void setDeckPath(const QString &_deckPath);
|
void setDeckPath(const QString &_deckPath);
|
||||||
void setFiltersPath(const QString &_filtersPath);
|
void setFiltersPath(const QString &_filtersPath);
|
||||||
void setReplaysPath(const QString &_replaysPath);
|
void setReplaysPath(const QString &_replaysPath);
|
||||||
|
|
@ -56,17 +37,14 @@ public:
|
||||||
void setCustomCardDatabasePath(const QString &_customCardDatabasePath);
|
void setCustomCardDatabasePath(const QString &_customCardDatabasePath);
|
||||||
void setTokenDatabasePath(const QString &_tokenDatabasePath);
|
void setTokenDatabasePath(const QString &_tokenDatabasePath);
|
||||||
void setSpoilerDatabasePath(const QString &_spoilerDatabasePath);
|
void setSpoilerDatabasePath(const QString &_spoilerDatabasePath);
|
||||||
void setAppDirWarningAcknowledged(bool _appDirWarningAcknowledged);
|
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void cardDatabasePathChanged();
|
void cardDatabasePathChanged();
|
||||||
void picsPathChanged();
|
void picsPathChanged();
|
||||||
void themeChanged();
|
void themeChanged();
|
||||||
|
|
||||||
public:
|
|
||||||
explicit PathsSettings(const QString &settingPath, QObject *parent = nullptr);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
explicit PathsSettings(const QString &settingPath, QObject *parent = nullptr);
|
||||||
PathsSettings(const PathsSettings & /*other*/);
|
PathsSettings(const PathsSettings & /*other*/);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -642,34 +642,6 @@ static void migrateLegacyMessages(const QString &settingsPath)
|
||||||
legacySetting.endGroup();
|
legacySetting.endGroup();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* gamefilters.ini keys renamed to camelCase by #7084. Profiles that migrated from the legacy
|
|
||||||
* native store before that rename still hold the old snake_case keys, which nothing reads
|
|
||||||
* anymore, so their saved filters silently stopped loading.
|
|
||||||
*/
|
|
||||||
static const QMap<QString, QString> &renamedGameFilterKeys()
|
|
||||||
{
|
|
||||||
static const QMap<QString, QString> keyMap = {
|
|
||||||
{"hide_buddies_only_games", "hideBuddiesOnlyGames"},
|
|
||||||
{"hide_full_games", "hideFullGames"},
|
|
||||||
{"hide_games_that_started", "hideGamesThatStarted"},
|
|
||||||
{"hide_password_protected_games", "hidePasswordProtectedGames"},
|
|
||||||
{"hide_ignored_user_games", "hideIgnoredUserGames"},
|
|
||||||
{"hide_not_buddy_created_games", "hideNotBuddyCreatedGames"},
|
|
||||||
{"hide_open_decklist_games", "hideOpenDecklistGames"},
|
|
||||||
{"game_name_filter", "gameNameFilter"},
|
|
||||||
{"creator_name_filter", "hostNameFilter"},
|
|
||||||
{"min_players", "minPlayers"},
|
|
||||||
{"max_players", "maxPlayers"},
|
|
||||||
{"max_game_age_time", "maxGameAgeTime"},
|
|
||||||
{"show_only_if_spectators_can_watch", "showOnlyIfSpectatorsCanWatch"},
|
|
||||||
{"show_spectator_password_protected", "showSpectatorPasswordProtected"},
|
|
||||||
{"show_only_if_spectators_can_chat", "showOnlyIfSpectatorsCanChat"},
|
|
||||||
{"show_only_if_spectators_can_see_hands", "showOnlyIfSpectatorsCanSeeHands"},
|
|
||||||
};
|
|
||||||
return keyMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void migrateLegacyGameFilters(const QString &settingsPath)
|
static void migrateLegacyGameFilters(const QString &settingsPath)
|
||||||
{
|
{
|
||||||
QSettings legacySetting;
|
QSettings legacySetting;
|
||||||
|
|
@ -680,7 +652,24 @@ static void migrateLegacyGameFilters(const QString &settingsPath)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QMap<QString, QString> &filterKeyMap = renamedGameFilterKeys();
|
const QMap<QString, QString> filterKeyMap = {
|
||||||
|
{"hide_buddies_only_games", "hideBuddiesOnlyGames"},
|
||||||
|
{"hide_full_games", "hideFullGames"},
|
||||||
|
{"hide_games_that_started", "hideGamesThatStarted"},
|
||||||
|
{"hide_password_protected_games", "hidePasswordProtectedGames"},
|
||||||
|
{"hide_ignored_user_games", "hideIgnoredUserGames"},
|
||||||
|
{"hide_not_buddy_created_games", "hideNotBuddyCreatedGames"},
|
||||||
|
{"hide_open_decklist_games", "hideOpenDecklistGames"},
|
||||||
|
{"game_name_filter", "gameNameFilter"},
|
||||||
|
{"creator_name_filter", "creatorNameFilter"},
|
||||||
|
{"min_players", "minPlayers"},
|
||||||
|
{"max_players", "maxPlayers"},
|
||||||
|
{"max_game_age_time", "maxGameAgeTime"},
|
||||||
|
{"show_only_if_spectators_can_watch", "showOnlyIfSpectatorsCanWatch"},
|
||||||
|
{"show_spectator_password_protected", "showSpectatorPasswordProtected"},
|
||||||
|
{"show_only_if_spectators_can_chat", "showOnlyIfSpectatorsCanChat"},
|
||||||
|
{"show_only_if_spectators_can_see_hands", "showOnlyIfSpectatorsCanSeeHands"},
|
||||||
|
};
|
||||||
|
|
||||||
QSettings filtersIni(settingsPath + "gamefilters.ini", QSettings::IniFormat);
|
QSettings filtersIni(settingsPath + "gamefilters.ini", QSettings::IniFormat);
|
||||||
for (auto it = filterKeyMap.constBegin(); it != filterKeyMap.constEnd(); ++it) {
|
for (auto it = filterKeyMap.constBegin(); it != filterKeyMap.constEnd(); ++it) {
|
||||||
|
|
@ -697,68 +686,6 @@ static void migrateLegacyGameFilters(const QString &settingsPath)
|
||||||
legacySetting.endGroup();
|
legacySetting.endGroup();
|
||||||
}
|
}
|
||||||
|
|
||||||
static const QString LEGACY_GAME_FILTERS_GAME_TYPE_PREFIX = QStringLiteral("filter_games/game_type/");
|
|
||||||
static const QString GAME_FILTERS_GAME_TYPE_PREFIX = QStringLiteral("filter_games/gameType/");
|
|
||||||
|
|
||||||
static bool gameFiltersCamelCaseMigrationPending(const QString &settingsPath)
|
|
||||||
{
|
|
||||||
if (!QFile::exists(settingsPath + "gamefilters.ini")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
QSettings filtersIni(settingsPath + "gamefilters.ini", QSettings::IniFormat);
|
|
||||||
const QMap<QString, QString> &keyMap = renamedGameFilterKeys();
|
|
||||||
for (auto it = keyMap.constBegin(); it != keyMap.constEnd(); ++it) {
|
|
||||||
if (filtersIni.contains("filter_games/" + it.key()) && !filtersIni.contains("filter_games/" + it.value())) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Orphaned by #7084, which migrated the legacy host-name filter to a key nothing reads.
|
|
||||||
if (filtersIni.contains("filter_games/creatorNameFilter") && !filtersIni.contains("filter_games/hostNameFilter")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
for (const auto &key : filtersIni.allKeys()) {
|
|
||||||
if (key.startsWith(LEGACY_GAME_FILTERS_GAME_TYPE_PREFIX) &&
|
|
||||||
!filtersIni.contains(GAME_FILTERS_GAME_TYPE_PREFIX +
|
|
||||||
key.mid(LEGACY_GAME_FILTERS_GAME_TYPE_PREFIX.size()))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SettingsMigration::migrateGameFiltersToCamelCase(const QString &settingsPath)
|
|
||||||
{
|
|
||||||
if (!gameFiltersCamelCaseMigrationPending(settingsPath)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
QSettings filtersIni(settingsPath + "gamefilters.ini", QSettings::IniFormat);
|
|
||||||
const QMap<QString, QString> &keyMap = renamedGameFilterKeys();
|
|
||||||
for (auto it = keyMap.constBegin(); it != keyMap.constEnd(); ++it) {
|
|
||||||
const QString oldKey = "filter_games/" + it.key();
|
|
||||||
const QString newKey = "filter_games/" + it.value();
|
|
||||||
if (filtersIni.contains(oldKey) && !filtersIni.contains(newKey)) {
|
|
||||||
filtersIni.setValue(newKey, filtersIni.value(oldKey));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (filtersIni.contains("filter_games/creatorNameFilter") && !filtersIni.contains("filter_games/hostNameFilter")) {
|
|
||||||
filtersIni.setValue("filter_games/hostNameFilter", filtersIni.value("filter_games/creatorNameFilter"));
|
|
||||||
}
|
|
||||||
for (const auto &key : filtersIni.allKeys()) {
|
|
||||||
if (key.startsWith(LEGACY_GAME_FILTERS_GAME_TYPE_PREFIX)) {
|
|
||||||
const QString newKey = GAME_FILTERS_GAME_TYPE_PREFIX + key.mid(LEGACY_GAME_FILTERS_GAME_TYPE_PREFIX.size());
|
|
||||||
if (!filtersIni.contains(newKey)) {
|
|
||||||
filtersIni.setValue(newKey, filtersIni.value(key));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// The old keys are deliberately kept: pre-#7084 builds still read them, so removing them
|
|
||||||
// would break downgrades, and nothing in the current code reads them anymore.
|
|
||||||
filtersIni.sync();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SettingsMigration::migrateLegacySettings(const QString &settingsPath)
|
bool SettingsMigration::migrateLegacySettings(const QString &settingsPath)
|
||||||
{
|
{
|
||||||
QSettings personalIni(settingsPath + "personal.ini", QSettings::IniFormat);
|
QSettings personalIni(settingsPath + "personal.ini", QSettings::IniFormat);
|
||||||
|
|
@ -817,10 +744,6 @@ bool SettingsMigration::isMigrationPending(const QString &settingsPath)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (gameFiltersCamelCaseMigrationPending(settingsPath)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
QSettings personalIni(settingsPath + "personal.ini", QSettings::IniFormat);
|
QSettings personalIni(settingsPath + "personal.ini", QSettings::IniFormat);
|
||||||
if (personalIni.value("migration/legacy_complete", false).toBool()) {
|
if (personalIni.value("migration/legacy_complete", false).toBool()) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -9,15 +9,6 @@ public:
|
||||||
static bool migrateSettingsFromGlobalIni(const QString &settingsPath);
|
static bool migrateSettingsFromGlobalIni(const QString &settingsPath);
|
||||||
static bool migrateLegacySettings(const QString &settingsPath);
|
static bool migrateLegacySettings(const QString &settingsPath);
|
||||||
|
|
||||||
/**
|
|
||||||
* Moves pre-camelCase gamefilters.ini keys (snake_case, game_type/<hash>) to the camelCase
|
|
||||||
* keys used since #7084, for profiles that already migrated before that rename and therefore
|
|
||||||
* never pass through migrateLegacySettings again. Copies values only where the new key is
|
|
||||||
* absent, never deletes the old keys, and returns true when anything was copied. Idempotent,
|
|
||||||
* so it runs on every startup.
|
|
||||||
*/
|
|
||||||
static bool migrateGameFiltersToCamelCase(const QString &settingsPath);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true when at least one of the two migrations would still do work, i.e. when a
|
* Returns true when at least one of the two migrations would still do work, i.e. when a
|
||||||
* backup of the settings directory is worth taking before they run.
|
* backup of the settings directory is worth taking before they run.
|
||||||
|
|
|
||||||
|
|
@ -49,5 +49,3 @@ target_link_libraries(libcockatrice_utility PUBLIC libcockatrice_rng OpenSSL::Cr
|
||||||
set(ORACLE_LIBS)
|
set(ORACLE_LIBS)
|
||||||
|
|
||||||
include_directories(${${COCKATRICE_QT_VERSION_NAME}Core_INCLUDE_DIRS})
|
include_directories(${${COCKATRICE_QT_VERSION_NAME}Core_INCLUDE_DIRS})
|
||||||
|
|
||||||
target_precompile_headers(libcockatrice_utility PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
|
||||||
|
|
|
||||||
|
|
@ -79,9 +79,6 @@ if(ZLIB_FOUND)
|
||||||
include_directories(${ZLIB_INCLUDE_DIRS})
|
include_directories(${ZLIB_INCLUDE_DIRS})
|
||||||
add_definitions("-DHAS_ZLIB")
|
add_definitions("-DHAS_ZLIB")
|
||||||
list(APPEND oracle_SOURCES src/zip/unzip.cpp src/zip/zipglobal.cpp)
|
list(APPEND oracle_SOURCES src/zip/unzip.cpp src/zip/zipglobal.cpp)
|
||||||
# minizip and zlib are vendored.
|
|
||||||
cockatrice_disable_sanitizers_for(src/zip/unzip.cpp src/zip/zipglobal.cpp)
|
|
||||||
cockatrice_disable_coverage_for(src/zip/unzip.cpp src/zip/zipglobal.cpp)
|
|
||||||
else()
|
else()
|
||||||
message(STATUS "Oracle: zlib not found; ZIP support disabled")
|
message(STATUS "Oracle: zlib not found; ZIP support disabled")
|
||||||
endif()
|
endif()
|
||||||
|
|
@ -92,9 +89,6 @@ if(LIBLZMA_FOUND)
|
||||||
include_directories(${LIBLZMA_INCLUDE_DIRS})
|
include_directories(${LIBLZMA_INCLUDE_DIRS})
|
||||||
add_definitions("-DHAS_LZMA")
|
add_definitions("-DHAS_LZMA")
|
||||||
list(APPEND oracle_SOURCES src/lzma/decompress.cpp)
|
list(APPEND oracle_SOURCES src/lzma/decompress.cpp)
|
||||||
# The LZMA SDK is vendored.
|
|
||||||
cockatrice_disable_sanitizers_for(src/lzma/decompress.cpp)
|
|
||||||
cockatrice_disable_coverage_for(src/lzma/decompress.cpp)
|
|
||||||
else()
|
else()
|
||||||
message(STATUS "Oracle: LibLZMA not found; xz support disabled")
|
message(STATUS "Oracle: LibLZMA not found; xz support disabled")
|
||||||
endif()
|
endif()
|
||||||
|
|
|
||||||
|
|
@ -85,8 +85,5 @@ int main(int argc, char *argv[])
|
||||||
QTimer::singleShot(0, &wizard, [&wizard]() { wizard.runInBackground(); });
|
QTimer::singleShot(0, &wizard, [&wizard]() { wizard.runInBackground(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
const int execResult = app.exec();
|
return app.exec();
|
||||||
// A cancel only counts when nothing was written yet: closing the wizard
|
|
||||||
// after the card database was already saved on disk is still an update.
|
|
||||||
return (wizard.wasCancelled() && !wizard.hasSavedDatabase()) ? 1 : execResult;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -128,20 +128,12 @@ void OracleWizard::accept()
|
||||||
|
|
||||||
void OracleWizard::reject()
|
void OracleWizard::reject()
|
||||||
{
|
{
|
||||||
cancelled = true;
|
|
||||||
|
|
||||||
// The wizard is being closed while a page may still run a worker on the
|
// The wizard is being closed while a page may still run a worker on the
|
||||||
// importer. Ask it to stop before the wizard (and the importer child) is
|
// importer. Ask it to stop before the wizard (and the importer child) is
|
||||||
// destroyed, so the worker thread never touches freed memory.
|
// destroyed, so the worker thread never touches freed memory.
|
||||||
if (auto *active = dynamic_cast<OracleWizardPage *>(currentPage())) {
|
if (auto *active = dynamic_cast<OracleWizardPage *>(currentPage())) {
|
||||||
active->cancelWork();
|
active->cancelWork();
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto replies = nam->findChildren<QNetworkReply *>();
|
|
||||||
for (QNetworkReply *reply : replies) {
|
|
||||||
reply->abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
QWizard::reject();
|
QWizard::reject();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,19 +20,6 @@ public:
|
||||||
explicit OracleWizard(QWidget *parent = nullptr);
|
explicit OracleWizard(QWidget *parent = nullptr);
|
||||||
void accept() override;
|
void accept() override;
|
||||||
void reject() override;
|
void reject() override;
|
||||||
bool wasCancelled()
|
|
||||||
{
|
|
||||||
return cancelled;
|
|
||||||
}
|
|
||||||
/** @brief Records that the card database was written to disk, so a later cancel still counts as an update. */
|
|
||||||
void markDatabaseSaved()
|
|
||||||
{
|
|
||||||
databaseSaved = true;
|
|
||||||
}
|
|
||||||
bool hasSavedDatabase()
|
|
||||||
{
|
|
||||||
return databaseSaved;
|
|
||||||
}
|
|
||||||
void enableButtons();
|
void enableButtons();
|
||||||
void disableButtons();
|
void disableButtons();
|
||||||
void retranslateUi();
|
void retranslateUi();
|
||||||
|
|
@ -80,8 +67,6 @@ private:
|
||||||
QByteArray tokensData;
|
QByteArray tokensData;
|
||||||
QString cardSourceUrl;
|
QString cardSourceUrl;
|
||||||
QString cardSourceVersion;
|
QString cardSourceVersion;
|
||||||
bool cancelled = false;
|
|
||||||
bool databaseSaved = false;
|
|
||||||
|
|
||||||
void migrateOracleSettings();
|
void migrateOracleSettings();
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -484,10 +484,6 @@ void LoadSetsPage::actDownloadFinishedSetsFile()
|
||||||
{
|
{
|
||||||
// check for a reply
|
// check for a reply
|
||||||
auto *reply = dynamic_cast<QNetworkReply *>(sender());
|
auto *reply = dynamic_cast<QNetworkReply *>(sender());
|
||||||
if (wizard()->wasCancelled()) {
|
|
||||||
reply->deleteLater();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
auto errorCode = reply->error();
|
auto errorCode = reply->error();
|
||||||
if (errorCode != QNetworkReply::NoError) {
|
if (errorCode != QNetworkReply::NoError) {
|
||||||
QMessageBox::critical(this, tr("Error"), tr("Network error: %1.").arg(reply->errorString()));
|
QMessageBox::critical(this, tr("Error"), tr("Network error: %1.").arg(reply->errorString()));
|
||||||
|
|
@ -899,7 +895,6 @@ bool SaveSetsPage::validatePage()
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
wizard()->markDatabaseSaved();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -178,10 +178,6 @@ void SimpleDownloadFilePage::actDownloadFinished()
|
||||||
{
|
{
|
||||||
// check for a reply
|
// check for a reply
|
||||||
auto *reply = dynamic_cast<QNetworkReply *>(sender());
|
auto *reply = dynamic_cast<QNetworkReply *>(sender());
|
||||||
if (wizard()->wasCancelled()) {
|
|
||||||
reply->deleteLater();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
QNetworkReply::NetworkError errorCode = reply->error();
|
QNetworkReply::NetworkError errorCode = reply->error();
|
||||||
if (errorCode != QNetworkReply::NoError) {
|
if (errorCode != QNetworkReply::NoError) {
|
||||||
QMessageBox::critical(this, tr("Error"), tr("Network error: %1.").arg(reply->errorString()));
|
QMessageBox::critical(this, tr("Error"), tr("Network error: %1.").arg(reply->errorString()));
|
||||||
|
|
@ -246,7 +242,6 @@ bool SimpleDownloadFilePage::saveToFile()
|
||||||
|
|
||||||
// clean saved downloadData
|
// clean saved downloadData
|
||||||
downloadData = QByteArray();
|
downloadData = QByteArray();
|
||||||
wizard()->markDatabaseSaved();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ set(servatrice_SOURCES
|
||||||
src/event_loop_watchdog.cpp
|
src/event_loop_watchdog.cpp
|
||||||
src/main.cpp
|
src/main.cpp
|
||||||
src/metrics_registry.cpp
|
src/metrics_registry.cpp
|
||||||
src/ratelimiter.cpp
|
|
||||||
src/servatrice.cpp
|
src/servatrice.cpp
|
||||||
src/servatrice_connection_pool.cpp
|
src/servatrice_connection_pool.cpp
|
||||||
src/servatrice_database_interface.cpp
|
src/servatrice_database_interface.cpp
|
||||||
|
|
@ -28,14 +27,6 @@ set(servatrice_SOURCES
|
||||||
|
|
||||||
set(servatrice_RESOURCES servatrice.qrc)
|
set(servatrice_RESOURCES servatrice.qrc)
|
||||||
|
|
||||||
# Qxt SMTP is vendored.
|
|
||||||
cockatrice_disable_sanitizers_for(
|
|
||||||
src/smtp/qxthmac.cpp src/smtp/qxtmailattachment.cpp src/smtp/qxtmailmessage.cpp src/smtp/qxtsmtp.cpp
|
|
||||||
)
|
|
||||||
cockatrice_disable_coverage_for(
|
|
||||||
src/smtp/qxthmac.cpp src/smtp/qxtmailattachment.cpp src/smtp/qxtmailmessage.cpp src/smtp/qxtsmtp.cpp
|
|
||||||
)
|
|
||||||
|
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
set(servatrice_SOURCES ${servatrice_SOURCES} servatrice.rc)
|
set(servatrice_SOURCES ${servatrice_SOURCES} servatrice.rc)
|
||||||
endif(WIN32)
|
endif(WIN32)
|
||||||
|
|
|
||||||
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