mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 09:33:25 -08:00
Compare commits
5 commits
a506420f33
...
ed65059f5b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed65059f5b | ||
|
|
7d1ad2191a | ||
|
|
5cea39a1fc | ||
|
|
dca6fdf17f | ||
|
|
e82b311c8b |
1476 changed files with 56922 additions and 192055 deletions
|
|
@ -3,13 +3,12 @@ FROM debian:12
|
|||
RUN apt-get update && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
ccache \
|
||||
clang-format \
|
||||
cmake \
|
||||
file \
|
||||
g++ \
|
||||
git \
|
||||
libcurl4-openssl-dev \
|
||||
libgl-dev \
|
||||
liblzma-dev \
|
||||
libmariadb-dev-compat \
|
||||
|
|
@ -19,12 +18,12 @@ RUN apt-get update && \
|
|||
libssl-dev \
|
||||
ninja-build \
|
||||
protobuf-compiler \
|
||||
qt6-declarative-dev \
|
||||
qt6-image-formats-plugins \
|
||||
qt6-l10n-tools \
|
||||
qt6-multimedia-dev \
|
||||
qt6-shadertools-dev \
|
||||
qt6-declarative-dev \
|
||||
qt6-svg-dev \
|
||||
qt6-shadertools-dev \
|
||||
qt6-tools-dev \
|
||||
qt6-tools-dev-tools \
|
||||
qt6-websockets-dev \
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ RUN apt-get update && \
|
|||
build-essential \
|
||||
ca-certificates \
|
||||
ccache \
|
||||
clang-format \
|
||||
cmake \
|
||||
file \
|
||||
g++ \
|
||||
git \
|
||||
libcurl4-openssl-dev \
|
||||
libgl-dev \
|
||||
liblzma-dev \
|
||||
libmariadb-dev-compat \
|
||||
|
|
@ -19,12 +19,12 @@ RUN apt-get update && \
|
|||
libssl-dev \
|
||||
ninja-build \
|
||||
protobuf-compiler \
|
||||
qt6-declarative-dev \
|
||||
qt6-image-formats-plugins \
|
||||
qt6-l10n-tools \
|
||||
qt6-multimedia-dev \
|
||||
qt6-shadertools-dev \
|
||||
qt6-declarative-dev \
|
||||
qt6-svg-dev \
|
||||
qt6-shadertools-dev \
|
||||
qt6-tools-dev \
|
||||
qt6-tools-dev-tools \
|
||||
qt6-websockets-dev \
|
||||
|
|
|
|||
|
|
@ -1,17 +1,15 @@
|
|||
FROM fedora:43
|
||||
|
||||
RUN dnf install -y \
|
||||
ca-certificates \
|
||||
ccache \
|
||||
cmake \
|
||||
gcc-c++ \
|
||||
git \
|
||||
libcurl-devel \
|
||||
mariadb-devel \
|
||||
ninja-build \
|
||||
openssl-devel \
|
||||
protobuf-devel \
|
||||
qt6-{qtdeclarative,qtmultimedia,qtshadertools,qtsvg,qttools,qtwebsockets}-devel \
|
||||
qt6-{qtdeclarative,qtshadertools,qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \
|
||||
qt6-qtimageformats \
|
||||
rpm-build \
|
||||
xz-devel \
|
||||
|
|
|
|||
|
|
@ -1,17 +1,15 @@
|
|||
FROM fedora:44
|
||||
|
||||
RUN dnf install -y \
|
||||
ca-certificates \
|
||||
ccache \
|
||||
cmake \
|
||||
gcc-c++ \
|
||||
git \
|
||||
libcurl-devel \
|
||||
mariadb-devel \
|
||||
ninja-build \
|
||||
openssl-devel \
|
||||
protobuf-devel \
|
||||
qt6-{qtdeclarative,qtmultimedia,qtshadertools,qtsvg,qttools,qtwebsockets}-devel \
|
||||
qt6-{qtdeclarative,qtshadertools,qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \
|
||||
qt6-qtimageformats \
|
||||
rpm-build \
|
||||
xz-devel \
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ RUN apt-get update && \
|
|||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
ccache \
|
||||
clang-format \
|
||||
cmake \
|
||||
file \
|
||||
g++ \
|
||||
|
|
|
|||
|
|
@ -3,13 +3,12 @@ FROM ubuntu:24.04
|
|||
RUN apt-get update && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
ccache \
|
||||
clang-format \
|
||||
cmake \
|
||||
file \
|
||||
g++ \
|
||||
git \
|
||||
libcurl4-openssl-dev \
|
||||
libgl-dev \
|
||||
liblzma-dev \
|
||||
libmariadb-dev-compat \
|
||||
|
|
@ -19,12 +18,12 @@ RUN apt-get update && \
|
|||
libssl-dev \
|
||||
ninja-build \
|
||||
protobuf-compiler \
|
||||
qt6-declarative-dev \
|
||||
qt6-image-formats-plugins \
|
||||
qt6-l10n-tools \
|
||||
qt6-multimedia-dev \
|
||||
qt6-shadertools-dev \
|
||||
qt6-declarative-dev \
|
||||
qt6-svg-dev \
|
||||
qt6-shadertools-dev \
|
||||
qt6-tools-dev \
|
||||
qt6-tools-dev-tools \
|
||||
qt6-websockets-dev \
|
||||
|
|
|
|||
|
|
@ -5,12 +5,11 @@ RUN apt-get update && \
|
|||
build-essential \
|
||||
ca-certificates \
|
||||
ccache \
|
||||
clang-format \
|
||||
cmake \
|
||||
file \
|
||||
g++ \
|
||||
gcovr \
|
||||
git \
|
||||
libcurl4-openssl-dev \
|
||||
libgl-dev \
|
||||
liblzma-dev \
|
||||
libmariadb-dev-compat \
|
||||
|
|
@ -20,12 +19,12 @@ RUN apt-get update && \
|
|||
libssl-dev \
|
||||
ninja-build \
|
||||
protobuf-compiler \
|
||||
qt6-declarative-dev \
|
||||
qt6-image-formats-plugins \
|
||||
qt6-l10n-tools \
|
||||
qt6-multimedia-dev \
|
||||
qt6-shadertools-dev \
|
||||
qt6-declarative-dev \
|
||||
qt6-svg-dev \
|
||||
qt6-shadertools-dev \
|
||||
qt6-tools-dev \
|
||||
qt6-tools-dev-tools \
|
||||
qt6-websockets-dev \
|
||||
|
|
|
|||
514
.ci/compile.sh
514
.ci/compile.sh
|
|
@ -8,18 +8,14 @@
|
|||
# --suffix <suffix> renames package with this suffix, requires arg
|
||||
# --server compiles servatrice
|
||||
# --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
|
||||
# --ccache [<size>] uses ccache and shows stats, optionally provide size
|
||||
# --evict-ccache <age> runs ccache eviction based on given age after build
|
||||
# --dir <dir> sets the name of the build dir, default is "build"
|
||||
# --cmake-generator <generator> sets CMAKE_GENERATOR as used by cmake
|
||||
# --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 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 --sanitize --coverage --ccache <ccache_size> --dir <dir> --profile)
|
||||
# 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
|
||||
# (correspond to args: --debug/--release --install --package <package type> --suffix <suffix> --server --test --ccache <ccache_size> --dir <dir>)
|
||||
# exitcode: 1 for failure, 3 for invalid arguments
|
||||
|
||||
# Read arguments
|
||||
|
|
@ -61,14 +57,6 @@ while [[ $# != 0 ]]; do
|
|||
MAKE_TEST=1
|
||||
shift
|
||||
;;
|
||||
'--sanitize')
|
||||
MAKE_SANITIZE=1
|
||||
shift
|
||||
;;
|
||||
'--coverage')
|
||||
MAKE_COVERAGE=1
|
||||
shift
|
||||
;;
|
||||
'--debug')
|
||||
BUILDTYPE="Debug"
|
||||
shift
|
||||
|
|
@ -116,10 +104,6 @@ while [[ $# != 0 ]]; do
|
|||
export CMAKE_GENERATOR=$1
|
||||
shift
|
||||
;;
|
||||
'--profile')
|
||||
BUILD_PROFILE=1
|
||||
shift
|
||||
;;
|
||||
'--target-macos-version')
|
||||
shift
|
||||
if [[ $# == 0 ]]; then
|
||||
|
|
@ -147,15 +131,6 @@ if [[ ! $BUILD_DIR ]]; then
|
|||
BUILD_DIR="build"
|
||||
fi
|
||||
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"
|
||||
|
||||
# Set minimum CMake Version
|
||||
|
|
@ -172,17 +147,6 @@ fi
|
|||
if [[ $MAKE_TEST ]]; then
|
||||
flags+=("-DTEST=1")
|
||||
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
|
||||
flags+=("-DUSE_CCACHE=1")
|
||||
# PCH-aware caching is required or ccache refuses to cache any TU that
|
||||
|
|
@ -200,20 +164,6 @@ if [[ $USE_VCPKG ]]; then
|
|||
flags+=("-DUSE_VCPKG=1")
|
||||
flags+=("-DVCPKG_INSTALL_OPTIONS=--x-abi-tools-use-exact-versions")
|
||||
fi
|
||||
if [[ ${WITH_CRASH_REPORTING:-} == "ON" ]]; then
|
||||
# Release CI opts into the sentry-native SDK (default is OFF so from-source, distro and
|
||||
# offline package builds never fetch it). Debug builds stay SDK-free so crashes from
|
||||
# developer builds never reach Sentry.
|
||||
flags+=("-DWITH_CRASH_REPORTING=ON")
|
||||
fi
|
||||
if [[ ${BUILDTYPE:-Release} == "Release" && -n ${COCKATRICE_SENTRY_DSN:-} ]]; then
|
||||
# Release-only: Debug builds stay DSN-free so crashes from developer builds
|
||||
# never reach Sentry. Empty (local builds, forks without the secret, and non-tag
|
||||
# CI builds, which never receive the secret) keeps today's mock fallback via the
|
||||
# compiled-in default. CI passes the value from the SENTRY_DSN secret on tag
|
||||
# Release builds only, so pre-release test artifacts never report to Sentry.
|
||||
flags+=("-DCOCKATRICE_SENTRY_DSN=$COCKATRICE_SENTRY_DSN")
|
||||
fi
|
||||
|
||||
# Add cmake --build flags
|
||||
buildflags=(--config "$BUILDTYPE")
|
||||
|
|
@ -228,22 +178,6 @@ function ccachestatsverbose() {
|
|||
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
|
||||
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,
|
||||
|
|
@ -278,14 +212,8 @@ if [[ $RUNNER_OS == macOS ]]; then
|
|||
triplet_source="../vcpkg/triplets/community/$arch-osx.cmake"
|
||||
fi
|
||||
cp "$triplet_source" "$triplet_file"
|
||||
# The triplet is what vcpkg hashes into the ABI hash of every port, so two legs that
|
||||
# agree here reuse each other's binary cache. Targets 14, 15 and 26 on the same Xcode
|
||||
# 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"
|
||||
echo "set(VCPKG_CMAKE_SYSTEM_VERSION $TARGET_MACOS_VERSION)" >>"$triplet_file"
|
||||
echo "set(VCPKG_OSX_DEPLOYMENT_TARGET $TARGET_MACOS_VERSION)" >>"$triplet_file"
|
||||
flags+=("-DVCPKG_OVERLAY_TRIPLETS=$triplets_dir")
|
||||
flags+=("-DVCPKG_HOST_TRIPLET=$triplet_version")
|
||||
flags+=("-DVCPKG_TARGET_TRIPLET=$triplet_version")
|
||||
|
|
@ -328,34 +256,9 @@ if [[ $RUNNER_OS == macOS ]]; then
|
|||
fi
|
||||
|
||||
elif [[ $RUNNER_OS == Windows ]]; then
|
||||
if [[ $CMAKE_GENERATOR == "Visual Studio"* ]]; then
|
||||
# cmake only hands MSBuild a job count when one is asked for, and without it
|
||||
# MSBuild builds a single node: one project at a time, whatever MTT does inside
|
||||
# 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
|
||||
# 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
|
||||
buildflags+=(-- -p:UseMultiToolTask=true -p:EnableClServerMode=true)
|
||||
fi
|
||||
|
||||
if [[ $USE_CCACHE ]]; then
|
||||
|
|
@ -371,19 +274,10 @@ cmake .. "${flags[@]}"
|
|||
echo "::endgroup::"
|
||||
|
||||
echo "::group::Build project"
|
||||
cpuinfo
|
||||
echo "Running cmake --build with flags: ${buildflags[*]}"
|
||||
cmake --build . "${buildflags[@]}"
|
||||
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 [[ $CCACHE_EVICTION_AGE ]]; then
|
||||
echo "::group::evict ccache files older than $CCACHE_EVICTION_AGE"
|
||||
|
|
@ -412,250 +306,7 @@ fi
|
|||
|
||||
if [[ $MAKE_TEST ]]; then
|
||||
echo "::group::Run tests"
|
||||
if [[ $MAKE_SANITIZE ]]; then
|
||||
# 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::"
|
||||
fi
|
||||
|
||||
if [[ $RUNNER_OS == Windows && ! $MAKE_NO_CLIENT ]]; then
|
||||
echo "::group::Probe the installer's holder lookup"
|
||||
# The working directory is still the build dir here: the package steps below are what
|
||||
# leave and re-enter it, and that is what made a $BUILD_DIR-prefixed path name
|
||||
# build/build. Both paths are relative to the dir the probe was built into.
|
||||
probe="./installer-holder-probe.exe"
|
||||
if [[ ! -f $probe ]]; then
|
||||
echo "::error file=$0::installer-holder-probe.exe was not built"
|
||||
exit 1
|
||||
fi
|
||||
# The probe exists to name a holder that a process-and-module scan cannot see: one
|
||||
# that has a file open without having loaded it. Holding a temporary DLL with
|
||||
# FileShare.None is exactly that case, so this asserts the reason the probe was
|
||||
# added instead of only that the Restart Manager answers at all. It needs no NSIS
|
||||
# and no packaging, so it runs on every Windows build that gets this far.
|
||||
# The holder is a process of its own: the probe never names the process that started it,
|
||||
# which is what keeps an in-place uninstaller from being named as a holder of the
|
||||
# directory it is emptying. The same DLL held by the script that starts the probe
|
||||
# therefore has to come back as nothing, and that is the second case below.
|
||||
probe_check="./holder-probe-check.ps1"
|
||||
cat >"$probe_check" <<'POWERSHELL'
|
||||
$ErrorActionPreference = "Stop"
|
||||
$dir = Join-Path ([IO.Path]::GetTempPath()) "cockatrice-holder-probe-$PID"
|
||||
Remove-Item -Recurse -Force $dir -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Force $dir | Out-Null
|
||||
$probe = (Resolve-Path -LiteralPath $env:PROBE_EXE).Path
|
||||
|
||||
function Invoke-Probe([string] $guardDir) {
|
||||
$start = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$start.FileName = $probe
|
||||
$start.UseShellExecute = $false
|
||||
$start.RedirectStandardOutput = $true
|
||||
$start.EnvironmentVariables["COCKATRICE_GUARD_DIR"] = $guardDir
|
||||
$process = [System.Diagnostics.Process]::Start($start)
|
||||
$read = $process.StandardOutput.ReadToEndAsync()
|
||||
if (-not $process.WaitForExit(20000)) {
|
||||
try { $process.Kill() } catch { }
|
||||
throw "the holder probe did not finish within 20 s for $guardDir"
|
||||
}
|
||||
$output = $read.Result
|
||||
$code = $process.ExitCode
|
||||
$process.Dispose()
|
||||
return @{ Code = $code; Output = $output }
|
||||
}
|
||||
|
||||
function Wait-UntilLocked([string] $path) {
|
||||
$deadline = (Get-Date).AddSeconds(15)
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
try {
|
||||
$open = [IO.File]::Open($path, "Open", "ReadWrite", "None")
|
||||
$open.Dispose()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
Start-Sleep -Milliseconds 100
|
||||
}
|
||||
throw "the holder never locked $path"
|
||||
}
|
||||
|
||||
try {
|
||||
$file = Join-Path $dir "locked.dll"
|
||||
[IO.File]::WriteAllText($file, "x")
|
||||
|
||||
# The holder is a process of its own, not this script: the probe never names the process
|
||||
# that started it (the case below asserts that), so a file held here would be left out on
|
||||
# purpose and the check would pass for the wrong reason.
|
||||
$holderScript = Join-Path $dir "hold.ps1"
|
||||
Set-Content -LiteralPath $holderScript -Value @'
|
||||
param([string] $Path)
|
||||
$handle = [IO.File]::Open($Path, "Open", "ReadWrite", "None")
|
||||
Start-Sleep -Seconds 120
|
||||
'@
|
||||
$powershell = Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0\powershell.exe"
|
||||
$holder = Start-Process -FilePath $powershell -PassThru -ArgumentList @(
|
||||
"-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "`"$holderScript`"", "-Path", "`"$file`"")
|
||||
try {
|
||||
Wait-UntilLocked $file
|
||||
$locked = Invoke-Probe $dir
|
||||
if ($locked.Code -ne 1) { throw "expected exit 1 for a locked file, got $($locked.Code) : $($locked.Output)" }
|
||||
if ($locked.Output -notmatch "pid\s+$($holder.Id)\b") {
|
||||
throw "the lookup did not name the process holding the file: $($locked.Output)"
|
||||
}
|
||||
} finally {
|
||||
try { $holder.Kill() } catch { }
|
||||
try { $holder.WaitForExit(5000) | Out-Null } catch { }
|
||||
}
|
||||
|
||||
# The probe is started by the installer, or by an uninstaller that runs in place from the
|
||||
# directory it is emptying, so the asking process holds files there itself. Naming it would
|
||||
# ask the user to close the program that is asking - a question neither the close nor the
|
||||
# end offer can answer, so the guard repeated it until the update was cancelled. A holder
|
||||
# that is only the asking process has to come back as nothing.
|
||||
$handle = [IO.File]::Open($file, "Open", "ReadWrite", "None")
|
||||
try {
|
||||
$self = Invoke-Probe $dir
|
||||
} finally {
|
||||
$handle.Dispose()
|
||||
}
|
||||
if ($self.Code -ne 0) { throw "expected exit 0 when the asking process is the only holder, got $($self.Code) : $($self.Output)" }
|
||||
if ($self.Output -match "pid\s+$PID\b") { throw "the lookup named the process that started it: $($self.Output)" }
|
||||
|
||||
# A payload executable that refuses to be replaced is running, whether or not this probe
|
||||
# may name it: the Restart Manager does not report every mapping, and the image path of a
|
||||
# process under another account cannot be read from here, so both process lookups can
|
||||
# come back empty while the program is still there. Held here by the asking process, so
|
||||
# that it cannot be named - the answer has to be "could not name it", never "nothing is
|
||||
# holding it", which is what sent an update into an uninstall that could only fail
|
||||
# halfway. Exit 3 is the code the guard answers by asking the user to close it and retry.
|
||||
$image = Join-Path $dir "oracle.exe"
|
||||
[IO.File]::WriteAllText($image, "x")
|
||||
$handle = [IO.File]::Open($image, "Open", "ReadWrite", "None")
|
||||
try {
|
||||
$unnameable = Invoke-Probe $dir
|
||||
} finally {
|
||||
$handle.Dispose()
|
||||
}
|
||||
if ($unnameable.Code -ne 3) {
|
||||
throw "expected exit 3 for a payload executable that is in use but cannot be named, got $($unnameable.Code) : $($unnameable.Output)"
|
||||
}
|
||||
if ($unnameable.Output -notmatch "oracle\.exe is in use") {
|
||||
throw "the answer did not name the executable it could not replace: $($unnameable.Output)"
|
||||
}
|
||||
|
||||
$empty = Join-Path $dir "empty"
|
||||
New-Item -ItemType Directory -Force $empty | Out-Null
|
||||
$nothing = Invoke-Probe $empty
|
||||
if ($nothing.Code -ne 0) { throw "expected exit 0 for a directory with nothing open, got $($nothing.Code) : $($nothing.Output)" }
|
||||
# A process running from the directory is a holder even when it has no file open: an
|
||||
# instance holds its payload as mapped images, not as handles, and that is the state
|
||||
# the guard used to miss - it passed, the purge failed, and the install ended in a
|
||||
# warning with no offer to close and try again. The image is a copy of a native
|
||||
# Windows program, so it starts without its usual directory next to it, and it carries
|
||||
# one of the payload's own names because that is what the lookup names.
|
||||
$copy = Join-Path $dir "cockatrice.exe"
|
||||
Copy-Item -LiteralPath (Join-Path $env:SystemRoot "System32\cmd.exe") -Destination $copy
|
||||
$running = Start-Process -FilePath $copy -ArgumentList "/c", "ping -n 20 127.0.0.1 >nul" -PassThru
|
||||
try {
|
||||
Start-Sleep -Milliseconds 1500
|
||||
$byImage = Invoke-Probe $dir
|
||||
} finally {
|
||||
try { $running.Kill() } catch { }
|
||||
# Wait for the image to be released, or the cleanup below leaves the copy behind.
|
||||
try { $running.WaitForExit(5000) | Out-Null } catch { }
|
||||
}
|
||||
if ($byImage.Code -ne 1) { throw "expected exit 1 for a process running from the directory, got $($byImage.Code) : $($byImage.Output)" }
|
||||
if ($byImage.Output -notmatch "cockatrice\.exe \(pid\s+$($running.Id)") {
|
||||
throw "the lookup did not name the process running from the directory: $($byImage.Output)"
|
||||
}
|
||||
|
||||
# A program in a folder below the directory is not an instance of the installation, even
|
||||
# when it carries a payload program's name: the install directory can be a folder the
|
||||
# user keeps other applications in, each in its own subfolder, and naming one of those
|
||||
# asked for a close the probe never sends and an end it refuses to perform, so the guard
|
||||
# could only abort the update. A program that really does hold a payload file is still
|
||||
# named, by the Restart Manager lookup exercised above.
|
||||
$nested = Join-Path $dir "other"
|
||||
New-Item -ItemType Directory -Force $nested | Out-Null
|
||||
$nestedImage = Join-Path $nested "cockatrice.exe"
|
||||
Copy-Item -LiteralPath (Join-Path $env:SystemRoot "System32\cmd.exe") -Destination $nestedImage
|
||||
$nestedRunning = Start-Process -FilePath $nestedImage -ArgumentList "/c", "ping -n 20 127.0.0.1 >nul" -PassThru
|
||||
try {
|
||||
Start-Sleep -Milliseconds 1500
|
||||
$inSubfolder = Invoke-Probe $dir
|
||||
} finally {
|
||||
try { $nestedRunning.Kill() } catch { }
|
||||
try { $nestedRunning.WaitForExit(5000) | Out-Null } catch { }
|
||||
}
|
||||
if ($inSubfolder.Code -ne 0) {
|
||||
throw "expected exit 0 for a program in a folder below the directory, got $($inSubfolder.Code) : $($inSubfolder.Output)"
|
||||
}
|
||||
|
||||
# A program the user keeps in the directory itself is not a payload program either: the
|
||||
# guard ends only what the payload installs, and naming anything else asks for an answer
|
||||
# that cannot free a single file.
|
||||
$foreign = Join-Path $dir "tool.exe"
|
||||
Copy-Item -LiteralPath (Join-Path $env:SystemRoot "System32\cmd.exe") -Destination $foreign
|
||||
$foreignRunning = Start-Process -FilePath $foreign -ArgumentList "/c", "ping -n 20 127.0.0.1 >nul" -PassThru
|
||||
try {
|
||||
Start-Sleep -Milliseconds 1500
|
||||
$ignored = Invoke-Probe $dir
|
||||
} finally {
|
||||
try { $foreignRunning.Kill() } catch { }
|
||||
try { $foreignRunning.WaitForExit(5000) | Out-Null } catch { }
|
||||
}
|
||||
if ($ignored.Code -ne 0) {
|
||||
throw "expected exit 0 for a program the payload does not install, got $($ignored.Code) : $($ignored.Output)"
|
||||
}
|
||||
Write-Host "holder lookup named pid $($holder.Id) for a file another process holds, named nothing for a file only the asking process holds, reported a payload executable it could not name as in use instead of free, named nothing for an empty directory, named pid $($running.Id) for a payload program running from the directory, and named nothing for a payload program in a folder below it or for a program the payload does not install"
|
||||
} finally {
|
||||
Remove-Item -Recurse -Force $dir -ErrorAction SilentlyContinue
|
||||
}
|
||||
POWERSHELL
|
||||
if ! PROBE_EXE="$probe" powershell -NoProfile -File "$probe_check"; then
|
||||
rm -f "$probe_check"
|
||||
exit 1
|
||||
fi
|
||||
rm -f "$probe_check"
|
||||
ctest -C "$BUILDTYPE" --output-on-failure
|
||||
echo "::endgroup::"
|
||||
fi
|
||||
|
||||
|
|
@ -676,153 +327,4 @@ if [[ $MAKE_PACKAGE ]]; then
|
|||
BUILD_DIR="$BUILD_DIR" .ci/name_build.sh "$PACKAGE_SUFFIX"
|
||||
echo "::endgroup::"
|
||||
fi
|
||||
|
||||
if [[ $RUNNER_OS == Windows ]]; then
|
||||
echo "::group::Check installer for build-tree artifacts"
|
||||
cd "$BUILD_DIR"
|
||||
package="$(find . -maxdepth 1 -type f -name 'Cockatrice-*.exe' -print -quit)"
|
||||
if [[ ! $package ]]; then
|
||||
echo "::error file=$0::Could not find installer to inspect"
|
||||
exit 1
|
||||
fi
|
||||
seven_zip="$(command -v 7z || true)"
|
||||
if [[ ! $seven_zip ]]; then
|
||||
seven_zip="/c/Program Files/7-Zip/7z.exe"
|
||||
fi
|
||||
if [[ ! -f $seven_zip ]]; then
|
||||
echo "::warning file=$0::7-Zip not found, skipping installer content check"
|
||||
else
|
||||
echo "Inspecting $package"
|
||||
# Fail the build if the installer contains any path left behind by the MSBuild or
|
||||
# Qt AUTOMOC tooling (build-tree artifacts must live in the build dir, not the install)
|
||||
if "$seven_zip" l "$package" |
|
||||
grep -E "_autogen|\.dir[\\/]|\.tlog|(^|[\\/])x64[\\/]|(^|[\\/])\.qt[\\/]|(^|[\\/])\.qsb[\\/]|(^|[\\/])\.lupdate[\\/]|CMakeFiles"; then
|
||||
echo "::error file=$0::Installer contains build-tree artifacts"
|
||||
exit 1
|
||||
fi
|
||||
echo "Installer content is clean"
|
||||
|
||||
# Fail the build if the installer does not carry the runtime the
|
||||
# applications link against - a file that must be there, or a DLL that must
|
||||
# not be. The runtime DLLs reach the package by globbing every *.dll out of
|
||||
# the build output directories, so anything that build produced can ride
|
||||
# along and nothing else in the build objects to it: a Qt module the client
|
||||
# links statically (Qt6Multimedia.dll, Qt6QuickWidgets.dll) can be left out,
|
||||
# and the test framework's gtest.dll can be shipped. Either only surfaces on
|
||||
# a user's machine after an update has already replaced their install, as
|
||||
# "The code execution cannot proceed because Qt6Multimedia.dll was not
|
||||
# found" or "abseil_dll.dll is either not designed to run on Windows or it
|
||||
# contains an error". required-runtime.txt is written by the top level
|
||||
# CMakeLists.txt from the same module lists the targets are built against
|
||||
# plus the vcpkg bin directory the app-local copies come from.
|
||||
if [[ ! -f required-runtime.txt ]]; then
|
||||
echo "::error file=$0::required-runtime.txt not found in the build dir, cannot verify the runtime"
|
||||
exit 1
|
||||
fi
|
||||
# 7-Zip writes CRLF on Windows, so the \r has to come off before any
|
||||
# whole-line comparison below. Normalize separators to /, and strip the
|
||||
# install-root prefixes an NSIS listing may carry, so the entries can be
|
||||
# compared as exact relative paths. -xF keeps a '.' in a DLL name from
|
||||
# being a wildcard; -i because that is how the installed tree behaves -
|
||||
# the uninstaller clears both $INSTDIR\plugins and $INSTDIR\Plugins.
|
||||
# The archive side and the manifest side have to agree on line endings.
|
||||
# CMake writes required-runtime.txt with the platform's line ending and
|
||||
# 7-Zip writes CRLF on Windows, so a trailing \r survives on $entry and
|
||||
# defeats the whole-line match - every entry then reads as missing even
|
||||
# though the listing holds it verbatim.
|
||||
# [$] rather than \$ so the literal dollar is a bracket expression and the
|
||||
# linter does not read these as unexpanded shell variables (SC2016).
|
||||
installer_paths="$("$seven_zip" l -slt "$package" | tr -d '\r' |
|
||||
sed -n 's/^Path = //p' |
|
||||
sed -e 's|\\|/|g' -e 's|^[$]INSTDIR/||' -e 's|^[$]OUTDIR/||' -e 's|^[$]PLUGINSDIR/||' -e 's|^\./||' -e 's|^/||')"
|
||||
manifest="$(tr -d '\r' <required-runtime.txt)"
|
||||
missing=""
|
||||
checked=0
|
||||
while IFS= read -r entry; do
|
||||
if [[ -z $entry || $entry == \#* ]]; then
|
||||
continue
|
||||
fi
|
||||
checked=$((checked + 1))
|
||||
if ! grep -qxiF "$entry" <<<"$installer_paths"; then
|
||||
missing+=" $entry"
|
||||
fi
|
||||
done <<<"$manifest"
|
||||
if [[ -n $missing ]]; then
|
||||
echo "::error file=$0::Installer is missing required runtime files:$missing"
|
||||
# Print both sides of the comparison. A mismatch here has twice been
|
||||
# caused by the shape of 7-Zip's output rather than by a missing file,
|
||||
# and a bare "missing" line cannot tell those apart.
|
||||
echo "required-runtime.txt asked for $checked path(s); 7-Zip listed $(grep -c '' <<<"$installer_paths") path(s)"
|
||||
# Indent with parameter expansion rather than sed 's/^/ /', which the
|
||||
# linter reports as SC2001.
|
||||
echo "--- required ---"
|
||||
echo "${manifest//$'\n'/$'\n '}"
|
||||
echo "--- first 40 paths 7-Zip lists in the installer ---"
|
||||
listed_head="$(head -40 <<<"$installer_paths")"
|
||||
echo "${listed_head//$'\n'/$'\n '}"
|
||||
exit 1
|
||||
fi
|
||||
echo "Installer carries the Qt runtime of every module the three applications link ($checked files)"
|
||||
|
||||
# Now the other direction: a top level DLL in the package that the manifest
|
||||
# does not name. The missing check above cannot see a runtime that was
|
||||
# never expected.
|
||||
#
|
||||
# Two things are reported here and neither may fail the build.
|
||||
#
|
||||
# The test framework is a fact rather than a guess - no shipped executable
|
||||
# links gtest or gmock, and the build output directories the "*.dll" glob
|
||||
# reads are shared with the test targets, so gtest.dll and gmock.dll are
|
||||
# one filter away from shipping in every release. Even so it is reported
|
||||
# instead of enforced: a check that has never passed on a real package has
|
||||
# not earned the right to block a release, and the first Windows run of an
|
||||
# enforcing version is exactly when its own wrong assumptions show up.
|
||||
#
|
||||
# Everything else is expected to be there. required-runtime.txt names the
|
||||
# Qt modules the three applications link, and the protobuf, Abseil,
|
||||
# OpenSSL, liblzma, ffmpeg and Qt debug DLLs in the package come in on the
|
||||
# build output glob instead, because nothing in the build declares them by
|
||||
# name. So this list is a report of what the package carries, not a verdict.
|
||||
# Read it when a dependency is added; do not fail on it.
|
||||
forbidden=""
|
||||
unexpected=""
|
||||
while IFS= read -r entry; do
|
||||
[[ -z $entry ]] && continue
|
||||
# Top level only. A DLL under Plugins/ or a nested image format plugin is
|
||||
# a plugin the platform loads by name, not a runtime the executable
|
||||
# imports, and requiring one here would list all of them every run.
|
||||
[[ $entry == */* ]] && continue
|
||||
if grep -qxiF "$entry" <<<"$manifest"; then
|
||||
continue
|
||||
fi
|
||||
# NSIS extracts its plugins to $PLUGINSDIR, which the path normalization
|
||||
# above strips, so they arrive here looking like payload files. They are
|
||||
# part of the installer, not of the installed tree.
|
||||
if [[ $entry == [Nn][Ss][Dd][Ii][Aa][Ll][Oo][Gg][Ss].dll ||
|
||||
$entry == [Nn][Ss][Ee][Xx][Ee][Cc].dll ||
|
||||
$entry == [Ss][Yy][Ss][Tt][Ee][Mm].dll ||
|
||||
$entry == [Uu][Ss][Ee][Rr][Ii][Nn][Ff][Oo].dll ||
|
||||
$entry == [Ii][Nn][Ee][Tt].dll ]]; then
|
||||
continue
|
||||
fi
|
||||
if [[ $entry == [Gg][Tt][Ee][Ss][Tt]* || $entry == [Gg][Mm][Oo][Cc][Kk]* ]]; then
|
||||
forbidden+=" $entry"
|
||||
else
|
||||
unexpected+=" $entry"
|
||||
fi
|
||||
done < <(grep -i '\.dll$' <<<"$installer_paths")
|
||||
if [[ -n $forbidden ]]; then
|
||||
echo "::warning file=$0::Installer contains test framework DLLs that no shipped executable links:$forbidden"
|
||||
echo "A build output directory shared with the test targets has leaked into the package."
|
||||
fi
|
||||
if [[ -n $unexpected ]]; then
|
||||
echo "Top level DLLs in the package beyond the linked Qt modules:$unexpected"
|
||||
echo "These arrive on the build output glob, not from required-runtime.txt, so they are reported and not required."
|
||||
fi
|
||||
if [[ -z $forbidden && -z $unexpected ]]; then
|
||||
echo "Installer carries no runtime files beyond the linked Qt modules"
|
||||
fi
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
fi
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -13,17 +13,11 @@
|
|||
# --set-cache <location> sets the location to cache the image or for ccache
|
||||
#
|
||||
# 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)
|
||||
# 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
|
||||
#
|
||||
# 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.
|
||||
# note that the docker container will not inherit any environment variables!
|
||||
#
|
||||
|
|
@ -87,7 +81,7 @@ if ! [[ $NAME ]]; then
|
|||
return 3
|
||||
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"
|
||||
if ! [[ -r $docker_dir/Dockerfile ]]; then
|
||||
|
|
@ -175,12 +169,6 @@ function RUN ()
|
|||
args+=(--mount "type=bind,source=$GITHUB_OUTPUT,target=/gh_output")
|
||||
args+=(--env "GITHUB_OUTPUT=/gh_output")
|
||||
fi
|
||||
# The container does not inherit host env vars: without this, WITH_CRASH_REPORTING=ON set in
|
||||
# the workflow never reaches compile.sh inside the container, so the Linux packages silently
|
||||
# build with crash reporting OFF while the libcurl packages in the Dockerfiles go unused.
|
||||
if [[ ${WITH_CRASH_REPORTING:-} ]]; then
|
||||
args+=(--env "WITH_CRASH_REPORTING=${WITH_CRASH_REPORTING}")
|
||||
fi
|
||||
# shellcheck disable=2086
|
||||
docker run "${args[@]}" $RUN_ARGS "$IMAGE_NAME" bash "$BUILD_SCRIPT" $RUN_OPTS "$@"
|
||||
return $?
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
.git/
|
||||
build/
|
||||
.github/
|
||||
.tx/
|
||||
|
|
|
|||
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
|
|
@ -60,10 +60,8 @@ jobs:
|
|||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
cmake \
|
||||
g++ \
|
||||
libcurl4-openssl-dev \
|
||||
libgl-dev \
|
||||
liblzma-dev \
|
||||
libmariadb-dev-compat \
|
||||
|
|
|
|||
528
.github/workflows/desktop-build.yml
vendored
528
.github/workflows/desktop-build.yml
vendored
|
|
@ -46,11 +46,6 @@ jobs:
|
|||
outputs:
|
||||
tag: ${{ steps.configure.outputs.tag }}
|
||||
sha: ${{ steps.configure.outputs.sha }}
|
||||
# MAJ.MIN.PATCH[-beta[.N]] of a tag build; empty otherwise. Mirrors what
|
||||
# cmake/getversion.cmake derives as PROJECT_VERSION, which the client bakes as its
|
||||
# Sentry release (cmake/CrashReporting.cmake) — symbol uploads must use the same
|
||||
# string so stack traces de-symbolicate against this build's binaries.
|
||||
sentry_release: ${{ steps.configure.outputs.sentry_release }}
|
||||
|
||||
steps:
|
||||
- name: "Configure"
|
||||
|
|
@ -61,9 +56,6 @@ jobs:
|
|||
run: |
|
||||
if [[ "$GITHUB_REF_TYPE" == 'tag' ]]; then # release
|
||||
echo "tag=$GITHUB_REF_NAME" >> "$GITHUB_OUTPUT"
|
||||
# Tag format is YYYY-MM-DD-{Release|Development}-MAJ.MIN.PATCH[-beta[.N]];
|
||||
# PROJECT_VERSION is the tail after the type (see cmake/getversion.cmake).
|
||||
echo "sentry_release=$(sed -E 's/^[0-9]{4}-[0-9]{2}-[0-9]{2}-(Release|Development)-//' <<< "$GITHUB_REF_NAME")" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
echo "sha=$RESOLVED_SHA" >> "$GITHUB_OUTPUT"
|
||||
|
||||
|
|
@ -152,36 +144,17 @@ jobs:
|
|||
|
||||
package: DEB
|
||||
|
||||
# The one leg that runs the suite under AddressSanitizer and
|
||||
# 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) || '' }}
|
||||
name: ${{ matrix.distro }} ${{ matrix.version }}
|
||||
needs: configure
|
||||
runs-on: ubuntu-latest # https://github.com/actions/runner-images
|
||||
continue-on-error: ${{ matrix.allow-failure == 'yes' }}
|
||||
timeout-minutes: 70
|
||||
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_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: 550M # 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'
|
||||
NAME: ${{ matrix.distro }}${{ matrix.version }}
|
||||
IMAGE_SUFFIX: ${{ matrix.variant }}
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
|
|
@ -191,8 +164,8 @@ jobs:
|
|||
id: restore_ccache
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
key: ccache-${{ matrix.distro }}${{ matrix.version }}${{ matrix.variant }}
|
||||
path: ${{ env.CACHE }}/.ccache
|
||||
key: ccache-${{ matrix.distro }}${{ matrix.version }}
|
||||
path: ${{ env.CACHE }}
|
||||
|
||||
- name: "Build ${{ matrix.distro }} ${{ matrix.version }} Docker image"
|
||||
shell: bash
|
||||
|
|
@ -201,9 +174,6 @@ jobs:
|
|||
- name: "Build debug and test"
|
||||
if: matrix.test != 'skip'
|
||||
shell: bash
|
||||
env:
|
||||
sanitize: '${{ matrix.sanitize }}'
|
||||
coverage: '${{ matrix.coverage }}'
|
||||
run: |
|
||||
source .ci/docker.sh
|
||||
args=()
|
||||
|
|
@ -211,33 +181,7 @@ jobs:
|
|||
args+=(--ccache "$CCACHE_SIZE")
|
||||
args+=(--cmake-generator "$CMAKE_GENERATOR")
|
||||
|
||||
extra=()
|
||||
[[ $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@v7
|
||||
with:
|
||||
name: coverage-report
|
||||
path: coverage/
|
||||
if-no-files-found: warn
|
||||
RUN --server --debug --test "${args[@]}"
|
||||
|
||||
- name: "Build release package"
|
||||
id: build
|
||||
|
|
@ -247,12 +191,6 @@ jobs:
|
|||
SUFFIX: '-${{ matrix.distro }}${{ matrix.version }}'
|
||||
package: '${{ matrix.package }}'
|
||||
server_only: '${{ matrix.server_only }}'
|
||||
WITH_CRASH_REPORTING: 'ON'
|
||||
# Tag builds only: artifacts from master pushes and PR branches must not report to
|
||||
# Sentry. Forks get no secrets on pull_request, so they keep the mock fallback either way.
|
||||
# configure.outputs.tag is set for ANY tag, so beta (Development) tags get the DSN
|
||||
# and the symbol upload below just like full releases.
|
||||
COCKATRICE_SENTRY_DSN: ${{ needs.configure.outputs.tag != null && secrets.SENTRY_DSN || '' }}
|
||||
run: |
|
||||
source .ci/docker.sh
|
||||
args=()
|
||||
|
|
@ -261,58 +199,9 @@ jobs:
|
|||
args+=(--ccache "$CCACHE_SIZE")
|
||||
args+=(--cmake-generator "$CMAKE_GENERATOR")
|
||||
args+=(--suffix "$SUFFIX")
|
||||
# The container inherits no environment, so the DSN rides in explicitly. -e without
|
||||
# =value passes it through (possibly empty); compile.sh only consumes it for Release
|
||||
# configures, and empty keeps the mock fallback.
|
||||
export RUN_ARGS="${RUN_ARGS:-} -e COCKATRICE_SENTRY_DSN"
|
||||
|
||||
RUN --server --release --package "$package" "${args[@]}"
|
||||
|
||||
- name: "Install sentry-cli"
|
||||
if: matrix.package != 'skip' && needs.configure.outputs.tag != null
|
||||
# Downloads from Sentry's release registry: a Sentry outage or a broken install under
|
||||
# Git Bash must not take down the release assets below.
|
||||
continue-on-error: true
|
||||
shell: bash
|
||||
env:
|
||||
SENTRY_CLI_VERSION: '3.8.0'
|
||||
run: |
|
||||
export INSTALL_DIR="$GITHUB_WORKSPACE/.sentry-cli"
|
||||
curl -sL https://sentry.io/get-cli/ | bash
|
||||
echo "$GITHUB_WORKSPACE/.sentry-cli" >> "$GITHUB_PATH"
|
||||
|
||||
- name: "Upload debug symbols to Sentry"
|
||||
# Like the DSN above, this runs for every tag — beta (Development) tags included.
|
||||
if: matrix.package != 'skip' && needs.configure.outputs.tag != null
|
||||
# A Sentry outage or rotated token must not take down the release assets below.
|
||||
continue-on-error: true
|
||||
shell: bash
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
run: |
|
||||
paths=()
|
||||
# The binaries only, not the directories: a directory makes sentry-cli walk the
|
||||
# whole build tree, where it finds (and uploads) object files and other fragments
|
||||
# that can never match a stack frame. The packaged copy is split and stripped by
|
||||
# cpack, but keeps this binary's GNU build-id, which is how Sentry matches it.
|
||||
for app in cockatrice oracle servatrice; do
|
||||
binary="build/$app/$app"
|
||||
[[ -f $binary ]] && paths+=("$binary")
|
||||
done
|
||||
if [[ ${#paths[@]} == 0 ]]; then
|
||||
# Fail loudly instead of skipping: see the Windows/macOS leg below — this step
|
||||
# is the only way symbols reach Sentry and `continue-on-error` keeps the job
|
||||
# green either way, so a silent skip looks exactly like a successful upload.
|
||||
echo "::error::No debug symbol files found; nothing would be uploaded"
|
||||
exit 1
|
||||
fi
|
||||
# Project slug on Sentry; must match the project the baked DSN points at.
|
||||
# No --release here: `debug-files upload` has no such option (sentry-cli errors on
|
||||
# the unknown flag and uploads nothing), and debug information files are matched to
|
||||
# events by debug ID via the event's debug_meta, not by release. The release grouping
|
||||
# comes from the client event itself (COCKATRICE_SENTRY_RELEASE).
|
||||
sentry-cli debug-files upload --org cockatrice --project cockatrice-desktop "${paths[@]}"
|
||||
|
||||
# Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342
|
||||
- name: "Delete remote compiler cache (ccache)"
|
||||
if: github.ref == 'refs/heads/master' && steps.restore_ccache.outputs.cache-hit
|
||||
|
|
@ -330,7 +219,7 @@ jobs:
|
|||
uses: actions/cache/save@v6
|
||||
with:
|
||||
key: ${{ steps.restore_ccache.outputs.cache-primary-key }}
|
||||
path: ${{ env.CACHE }}/.ccache
|
||||
path: ${{ env.CACHE }}
|
||||
|
||||
- name: "Upload artifact"
|
||||
id: upload_artifact
|
||||
|
|
@ -383,13 +272,11 @@ jobs:
|
|||
make_package: 1
|
||||
override_target: 13
|
||||
package_suffix: "-macOS13_Intel"
|
||||
qt_version: 6.11.*
|
||||
qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets
|
||||
qt_version: 6.11.1
|
||||
qt_modules: qtimageformats qtmultimedia qtwebsockets qtshadertools
|
||||
soc: Intel
|
||||
type: Release
|
||||
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"
|
||||
|
||||
- os: macOS
|
||||
|
|
@ -401,14 +288,11 @@ jobs:
|
|||
make_package: 1
|
||||
override_target: 14
|
||||
package_suffix: "-macOS14"
|
||||
qt_version: 6.11.*
|
||||
qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets
|
||||
qt_version: 6.11.1
|
||||
qt_modules: qtimageformats qtmultimedia qtwebsockets qtshadertools
|
||||
soc: Apple
|
||||
type: Release
|
||||
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"
|
||||
|
||||
- os: macOS
|
||||
|
|
@ -420,13 +304,11 @@ jobs:
|
|||
make_package: 1
|
||||
override_target: 15
|
||||
package_suffix: "-macOS15"
|
||||
qt_version: 6.11.*
|
||||
qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets
|
||||
qt_version: 6.11.1
|
||||
qt_modules: qtimageformats qtmultimedia qtwebsockets qtshadertools
|
||||
soc: Apple
|
||||
type: Release
|
||||
use_ccache: 1
|
||||
vcpkg_deployment_target: 14
|
||||
vcpkg_scope: 'vcpkg/macos14-arm64-xcode26.6/'
|
||||
xcode: "26.6"
|
||||
|
||||
- os: macOS
|
||||
|
|
@ -435,35 +317,24 @@ jobs:
|
|||
|
||||
ccache_eviction_age: 7d
|
||||
cmake_generator: Ninja
|
||||
qt_version: 6.11.*
|
||||
qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets
|
||||
qt_version: 6.11.1
|
||||
qt_modules: qtimageformats qtmultimedia qtwebsockets qtshadertools
|
||||
soc: Apple
|
||||
type: Debug
|
||||
use_ccache: 1
|
||||
vcpkg_deployment_target: 14
|
||||
vcpkg_scope: 'vcpkg/macos14-arm64-xcode26.6/'
|
||||
xcode: "26.6"
|
||||
|
||||
- os: Windows
|
||||
target: 10
|
||||
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
|
||||
# MSBuild node at a time, runs the compiler itself, and matches the
|
||||
# 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
|
||||
cmake_generator: "Visual Studio 18 2026"
|
||||
cmake_generator_platform: x64
|
||||
make_package: 1
|
||||
package_suffix: "-Win10"
|
||||
qt_version: 6.11.*
|
||||
qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets
|
||||
qt_version: 6.11.1
|
||||
qt_modules: qtimageformats qtmultimedia qtwebsockets qtshadertools
|
||||
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' || '' }}
|
||||
needs: configure
|
||||
|
|
@ -471,7 +342,7 @@ jobs:
|
|||
timeout-minutes: 100
|
||||
env:
|
||||
CCACHE_DIR: ${{ github.workspace }}/.cache/
|
||||
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: 550M # 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
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
|
|
@ -497,20 +368,18 @@ jobs:
|
|||
key: ccache-${{ matrix.runner }}_${{ matrix.override_target }}-Xcode${{ matrix.xcode }}
|
||||
path: ${{ env.CCACHE_DIR }}
|
||||
|
||||
- name: "[macOS] Install aqtinstall"
|
||||
if: matrix.os == 'macOS'
|
||||
- name: "Install aqtinstall"
|
||||
run: pipx install aqtinstall
|
||||
|
||||
# Resolve given wildcard versions (e.g. Qt 6.6.*) to latest version via aqtinstall to avoid stale caches on new releases
|
||||
- name: "[macOS] Resolve latest Qt from ${{ matrix.qt_version }} input"
|
||||
if: matrix.os == 'macOS'
|
||||
- name: "Resolve latest Qt patch version"
|
||||
env:
|
||||
QT_VERSION: ${{ matrix.qt_version }}
|
||||
id: resolve_qt_version
|
||||
shell: bash
|
||||
run: .ci/resolve_latest_aqt_qt_version.sh "$QT_VERSION"
|
||||
|
||||
- name: "[macOS] Restore thin Qt ${{ steps.resolve_qt_version.outputs.version }}"
|
||||
- name: "[macOS] Restore thin Qt ${{ steps.resolve_qt_version.outputs.version }} libraries"
|
||||
if: matrix.os == 'macOS'
|
||||
id: restore_qt
|
||||
uses: actions/cache/restore@v6
|
||||
|
|
@ -520,22 +389,21 @@ jobs:
|
|||
|
||||
# Using jurplel/install-qt-action to install Qt without using brew
|
||||
# Qt build using vcpkg either just fails or takes too long to build
|
||||
- name: "[macOS] Install fat Qt ${{ matrix.qt_version }}"
|
||||
- name: "[macOS] Install fat Qt ${{ steps.resolve_qt_version.outputs.version }}"
|
||||
if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true'
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
cache: false
|
||||
# cache-key-prefix: Qt
|
||||
dir: ${{ github.workspace }} # thinning script depends on this location
|
||||
dir: ${{ github.workspace }}
|
||||
modules: ${{ matrix.qt_modules }}
|
||||
version: ${{ matrix.qt_version }}
|
||||
version: ${{ steps.resolve_qt_version.outputs.version }}
|
||||
|
||||
- name: "[macOS] Create thin Qt libraries"
|
||||
if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true'
|
||||
run: .ci/thin_macos_qtlib.sh
|
||||
|
||||
- name: "[macOS] Cache thin Qt libraries"
|
||||
if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true' && github.ref == 'refs/heads/master'
|
||||
if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@v6
|
||||
with:
|
||||
key: ${{ steps.restore_qt.outputs.cache-primary-key }}
|
||||
|
|
@ -547,55 +415,22 @@ jobs:
|
|||
with:
|
||||
# Qt 6.11.0 only works with aqtinstall directly from git until aqtinstall 3.4 is released
|
||||
aqtsource: git+https://github.com/miurahr/aqtinstall.git
|
||||
cache: ${{ github.ref == 'refs/heads/master' }}
|
||||
cache: true
|
||||
cache-key-prefix: Qt
|
||||
modules: ${{ matrix.qt_modules }}
|
||||
version: ${{ matrix.qt_version }}
|
||||
version: ${{ steps.resolve_qt_version.outputs.version }}
|
||||
|
||||
- name: "[Windows] Install NSIS"
|
||||
if: matrix.os == 'Windows'
|
||||
shell: bash
|
||||
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"
|
||||
id: vcpkg-cache
|
||||
uses: TAServers/vcpkg-cache@v3
|
||||
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 }}
|
||||
|
||||
# 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.
|
||||
# always() covers the cache step itself failing: the action's post save still runs
|
||||
# then, and it should report the real error instead of ENOENT. It cannot rescue a
|
||||
# failure before the cache step, since a post step only runs for an action whose
|
||||
# main step did. The fallback keeps this guard from failing when the cache step was
|
||||
# skipped and left no path output.
|
||||
- name: "Create vcpkg cache directory"
|
||||
if: always()
|
||||
shell: bash
|
||||
env:
|
||||
CACHE_DIR: '${{ steps.vcpkg-cache.outputs.path }}'
|
||||
run: mkdir -p "${CACHE_DIR:-$GITHUB_WORKSPACE/.vcpkg-cache}"
|
||||
|
||||
# Uses environment variables, see compile.sh for more details
|
||||
- name: "Build Cockatrice"
|
||||
id: build
|
||||
|
|
@ -604,11 +439,7 @@ jobs:
|
|||
BUILDTYPE: '${{ matrix.type }}'
|
||||
CCACHE_EVICTION_AGE: ${{ matrix.ccache_eviction_age }}
|
||||
CMAKE_GENERATOR: ${{ matrix.cmake_generator }}
|
||||
# Tag builds only: artifacts from master pushes and PR branches must not report to
|
||||
# Sentry. Forks get no secrets on pull_request, so they keep the mock fallback either way.
|
||||
# configure.outputs.tag is set for ANY tag, so beta (Development) tags get the DSN
|
||||
# and the symbol upload below just like full releases.
|
||||
COCKATRICE_SENTRY_DSN: ${{ needs.configure.outputs.tag != null && secrets.SENTRY_DSN || '' }}
|
||||
CMAKE_GENERATOR_PLATFORM: ${{ matrix.cmake_generator_platform }}
|
||||
DEVELOPER_DIR: '/Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer'
|
||||
GITHUB_TOKEN: ${{ github.token }} # needed for vcpkg dependency graph updates, see VCPKG_FEATURE_FLAGS
|
||||
MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }}
|
||||
|
|
@ -619,206 +450,11 @@ jobs:
|
|||
PACKAGE_SUFFIX: '${{ matrix.package_suffix }}'
|
||||
TARGET_MACOS_VERSION: ${{ matrix.override_target }}
|
||||
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 }},readwrite'
|
||||
VCPKG_DISABLE_METRICS: 1
|
||||
VCPKG_FEATURE_FLAGS: dependencygraph
|
||||
VCPKG_TARGET_MACOS_VERSION: ${{ matrix.vcpkg_deployment_target }}
|
||||
# Release legs compile the sentry-native SDK; Debug legs stay SDK-free.
|
||||
WITH_CRASH_REPORTING: ${{ matrix.type == 'Release' && 'ON' || '' }}
|
||||
run: .ci/compile.sh --server --test --vcpkg
|
||||
|
||||
- name: "Install sentry-cli"
|
||||
if: needs.configure.outputs.tag != null && matrix.make_package == '1'
|
||||
# Same as the Linux leg above: the install downloads from Sentry's registry, so a
|
||||
# failure here must not block the release upload below.
|
||||
continue-on-error: true
|
||||
shell: bash
|
||||
env:
|
||||
SENTRY_CLI_VERSION: '3.8.0'
|
||||
run: |
|
||||
export INSTALL_DIR="$GITHUB_WORKSPACE/.sentry-cli"
|
||||
curl -sL https://sentry.io/get-cli/ | bash
|
||||
echo "$GITHUB_WORKSPACE/.sentry-cli" >> "$GITHUB_PATH"
|
||||
|
||||
- name: "Upload debug symbols to Sentry"
|
||||
# Like the DSN above, this runs for every tag — beta (Development) tags included.
|
||||
if: needs.configure.outputs.tag != null && matrix.make_package == '1'
|
||||
# A Sentry outage or rotated token must not take down the release assets below.
|
||||
continue-on-error: true
|
||||
shell: bash
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
run: |
|
||||
paths=()
|
||||
if [[ '${{ matrix.os }}' == 'Windows' ]]; then
|
||||
# Top-level CMakeLists.txt links /DEBUG even in release, so the linker PDBs
|
||||
# land next to the binaries. Ninja is single-config, so that is
|
||||
# build/<target>/ and NOT build/<target>/Release/ — the same globs the symbol
|
||||
# artifact step below uses.
|
||||
#
|
||||
# The executables are uploaded as well, in the same invocation. On 64-bit
|
||||
# Windows the Call Frame Information (.pdata/.xdata) lives in the PE and is
|
||||
# not copied into the PDB, so a PDB on its own leaves Sentry reporting
|
||||
# "Stack Unwinding: Missing" for the image. sentry-cli also needs to see the
|
||||
# PE and its PDB together: only then does it associate the PE's code
|
||||
# identifier with the PDB's debug identifier.
|
||||
shopt -s nullglob
|
||||
for target in cockatrice oracle servatrice; do
|
||||
paths+=("build/$target"/*.exe "build/$target"/*.pdb)
|
||||
done
|
||||
shopt -u nullglob
|
||||
|
||||
# Qt's own runtime DLLs, taken from required-runtime.txt — the manifest
|
||||
# .ci/compile.sh asserts the installer against — so the set uploaded is
|
||||
# the set an install actually carries. The PE is the only carrier of
|
||||
# unwind information for a 64-bit image, and sending it in the same
|
||||
# invocation as a PDB is what lets sentry-cli associate the PE's code
|
||||
# identifier with the PDB's debug identifier. Qt ships no PDBs with the
|
||||
# SDK, so Qt frames resolve through the export table: names, no lines.
|
||||
qt_dir=
|
||||
qt_core_dir=
|
||||
# Existence guards matter: the step runs under `bash -eo pipefail`, so a
|
||||
# redirection-less sed over a missing file would abort the step outright
|
||||
# and continue-on-error would hide it.
|
||||
if [[ -f build/CMakeCache.txt ]]; then
|
||||
qt_core_dir=$(sed -n 's/^Qt6Core_DIR:PATH=//p' build/CMakeCache.txt | head -1)
|
||||
fi
|
||||
if [[ -n $qt_core_dir ]]; then
|
||||
qt_dir=$(dirname "$(dirname "$(dirname "$qt_core_dir")")")
|
||||
fi
|
||||
if [[ -z $qt_dir || ! -d $qt_dir ]]; then
|
||||
# Warnings, not failures: the upload of our own binaries is what this
|
||||
# step is for, and continue-on-error already means an upload failure
|
||||
# cannot be told apart from a successful one.
|
||||
echo "::warning::Qt runtime directory not resolved from build/CMakeCache.txt; Qt images get no unwind information"
|
||||
elif [[ ! -f build/required-runtime.txt ]]; then
|
||||
echo "::warning::build/required-runtime.txt is missing; cannot tell which Qt DLLs ship"
|
||||
else
|
||||
while IFS= read -r runtime; do
|
||||
name=${runtime##*/}
|
||||
if [[ $runtime == */* ]]; then
|
||||
# A Qt plugin: the install root keeps it as plugins/<type>/<name>.
|
||||
# `|| true`: an unmatched glob makes ls exit non-zero, and pipefail
|
||||
# turns that into a step abort.
|
||||
runtime_path=$(ls "$qt_dir/plugins"/*/"$name" 2>/dev/null | head -1) || true
|
||||
else
|
||||
runtime_path="$qt_dir/bin/$name"
|
||||
fi
|
||||
if [[ -f ${runtime_path:-} ]]; then
|
||||
paths+=("$runtime_path")
|
||||
else
|
||||
echo "::warning::shipped Qt runtime $runtime not found under $qt_dir"
|
||||
fi
|
||||
done < <(grep -E '^(Qt6[^/]*\.dll|Plugins/)' build/required-runtime.txt)
|
||||
fi
|
||||
else
|
||||
# Release builds carry DWARF (-g, kept by CrashReporting.cmake); macOS keeps its
|
||||
# debug info in dSYM bundles, so materialize them before uploading.
|
||||
for app in cockatrice oracle servatrice; do
|
||||
binary="build/$app/$app.app/Contents/MacOS/$app"
|
||||
if [[ -f $binary ]]; then
|
||||
if [[ '${{ matrix.os }}' == 'macOS' ]]; then
|
||||
dsymutil "$binary" -o "$binary.dSYM"
|
||||
paths+=("$binary.dSYM")
|
||||
# The dSYM holds the debug information, but the unwind information
|
||||
# Sentry needs to walk a minidump stack lives in the Mach-O itself.
|
||||
paths+=("$binary")
|
||||
else
|
||||
paths+=("$binary")
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
if [[ ${#paths[@]} == 0 ]]; then
|
||||
# Fail loudly instead of skipping: this step is the only way symbols reach
|
||||
# Sentry, and `continue-on-error` keeps the job green either way, so a
|
||||
# silent skip is indistinguishable from a successful upload. It also hid a
|
||||
# wrong Windows path (build/<target>/Release, which Ninja never creates).
|
||||
echo "::error::No debug symbol files found; nothing would be uploaded"
|
||||
exit 1
|
||||
fi
|
||||
# Project slug on Sentry; must match the project the baked DSN points at.
|
||||
# No --release: see the Linux leg above — the flag does not exist on
|
||||
# `debug-files upload` and DIFs are matched by debug ID, not release.
|
||||
sentry-cli debug-files upload --org cockatrice --project cockatrice-desktop "${paths[@]}"
|
||||
|
||||
# CPack reports a failed makensis run as "Problem running NSIS command"
|
||||
# plus a path to NSISOutput.log, and that log lives only on the runner, so
|
||||
# the actual compile error is otherwise nowhere in the workflow output.
|
||||
# Dump it, or a broken installer script is undiagnosable from CI alone.
|
||||
# shell: bash matters here - the default pwsh resolves `find` to Windows'
|
||||
# text-search find.exe, which errors instead of searching.
|
||||
- name: "Dump NSIS compile log"
|
||||
if: failure() && matrix.os == 'Windows'
|
||||
shell: bash
|
||||
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
|
||||
- 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
|
||||
|
|
@ -894,6 +530,17 @@ jobs:
|
|||
if-no-files-found: error
|
||||
path: ${{ steps.build.outputs.path }}
|
||||
|
||||
- name: "[Windows] Upload PDBs (Program Databases)"
|
||||
if: matrix.os == 'Windows' && github.ref_type != 'tag'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
if-no-files-found: error
|
||||
name: ${{ steps.build.outputs.name }}-PDBs
|
||||
path: |
|
||||
build/cockatrice/Release/*.pdb
|
||||
build/oracle/Release/*.pdb
|
||||
build/servatrice/Release/*.pdb
|
||||
|
||||
- name: "Upload to release"
|
||||
if: needs.configure.outputs.tag != null && matrix.make_package == '1'
|
||||
id: upload_release
|
||||
|
|
@ -905,33 +552,6 @@ jobs:
|
|||
tag_name: ${{ needs.configure.outputs.tag }}
|
||||
run: gh release upload "$tag_name" "$asset_path#$asset_name"
|
||||
|
||||
# Ninja is single-config: binaries and their PDBs land directly in the
|
||||
# target directories instead of a Release subdirectory.
|
||||
#
|
||||
# Archived for tag builds only, and ordered after the release upload so
|
||||
# a missing PDB can never block a release from being published. On a tag
|
||||
# build this artifact is the only durable copy of the symbols: the Sentry
|
||||
# upload is continue-on-error, so without it a failed upload would lose
|
||||
# the ability to symbolicate that release permanently. Other builds upload
|
||||
# no symbols at all, so archiving them there would only cost storage.
|
||||
#
|
||||
# The executables travel with their PDBs: re-uploading the pair together is
|
||||
# what lets sentry-cli associate the PE's code identifier with the PDB's
|
||||
# debug identifier, and the PE is what carries the unwind information.
|
||||
- name: "[Windows] Upload debug symbols (exe + pdb)"
|
||||
if: matrix.os == 'Windows' && needs.configure.outputs.tag != null
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
if-no-files-found: error
|
||||
name: ${{ steps.build.outputs.name }}-Symbols
|
||||
path: |
|
||||
build/cockatrice/*.exe
|
||||
build/cockatrice/*.pdb
|
||||
build/oracle/*.exe
|
||||
build/oracle/*.pdb
|
||||
build/servatrice/*.exe
|
||||
build/servatrice/*.pdb
|
||||
|
||||
- name: "Attest binary provenance"
|
||||
if: steps.upload_release.outcome == 'success'
|
||||
id: attestation
|
||||
|
|
@ -947,67 +567,3 @@ jobs:
|
|||
BUILD_PATH: ${{ steps.build.outputs.path }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
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"
|
||||
|
|
|
|||
35
.github/workflows/docker-release.yml
vendored
35
.github/workflows/docker-release.yml
vendored
|
|
@ -20,22 +20,11 @@ on:
|
|||
release:
|
||||
types:
|
||||
- released # publishing of stable releases
|
||||
workflow_dispatch: # (re)publish the image for an existing stable release
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Existing release tag to build and publish (e.g. 2026-10-03-Release-3.1.0)'
|
||||
required: true
|
||||
type: string
|
||||
latest:
|
||||
description: 'Also update the "latest" tag'
|
||||
required: true
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
# Cancel earlier, unfinished runs of this workflow on the same branch (unless publishing)
|
||||
# Cancel earlier, unfinished runs of this workflow on the same branch (unless on release)
|
||||
concurrency:
|
||||
group: "${{ github.workflow }} @ ${{ inputs.tag || github.ref_name }}"
|
||||
cancel-in-progress: ${{ github.event_name != 'release' && github.event_name != 'workflow_dispatch' }}
|
||||
group: "${{ github.workflow }} @ ${{ github.ref_name }}"
|
||||
cancel-in-progress: ${{ github.event_name != 'release' }}
|
||||
|
||||
env:
|
||||
GHCR_IMAGE: ghcr.io/cockatrice/servatrice
|
||||
|
|
@ -68,14 +57,12 @@ jobs:
|
|||
steps:
|
||||
- name: "Checkout"
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ case(github.event_name == 'workflow_dispatch', format('refs/tags/{0}', inputs.tag), '') }}
|
||||
|
||||
- name: "Set up Docker buildx"
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: "Login to GitHub Container Registry (GHCR)"
|
||||
if: (github.event_name == 'release' && github.event.release.prerelease == false) || github.event_name == 'workflow_dispatch'
|
||||
if: github.event_name == 'release' && github.event.release.prerelease == false
|
||||
id: login
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
|
|
@ -83,13 +70,13 @@ jobs:
|
|||
username: ${{ github.actor }}
|
||||
password: ${{ github.token }}
|
||||
|
||||
# Don't push for non-publishing triggers
|
||||
# Don't push for non-release triggers
|
||||
- name: "Build image"
|
||||
if: steps.login.outcome != 'success'
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
cache-from: type=gha,scope=${{ env.CACHE_SCOPE }}
|
||||
cache-to: ${{ case(github.ref == 'refs/heads/master', format('type=gha,mode=max,scope={0}', env.CACHE_SCOPE), '') }}
|
||||
cache-to: type=gha,mode=max,scope=${{ env.CACHE_SCOPE }}
|
||||
context: .
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: false
|
||||
|
|
@ -124,6 +111,7 @@ jobs:
|
|||
if: steps.login.outcome == 'success'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
archive: false
|
||||
if-no-files-found: error
|
||||
name: digest-${{ matrix.label }}
|
||||
path: ${{ runner.temp }}/digests/*
|
||||
|
|
@ -133,7 +121,7 @@ jobs:
|
|||
# Create an OCI image index from the platform-specific image manifests
|
||||
index:
|
||||
name: "Publish multi-platform Servatrice image"
|
||||
if: github.repository_owner == 'Cockatrice' && ((github.event_name == 'release' && github.event.release.prerelease == false) || github.event_name == 'workflow_dispatch')
|
||||
if: github.repository_owner == 'Cockatrice' && github.event_name == 'release' && github.event.release.prerelease == false
|
||||
needs: build
|
||||
runs-on: ubuntu-slim # https://github.com/actions/runner-images/blob/main/images/ubuntu-slim/ubuntu-slim-Readme.md
|
||||
|
||||
|
|
@ -159,11 +147,8 @@ jobs:
|
|||
images: ${{ env.GHCR_IMAGE }}
|
||||
flavor: |
|
||||
latest=auto
|
||||
# if semver, also: type=semver,pattern={{version}} / {{major}}.{{minor}}
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
type=raw,value=${{ inputs.tag }},enable=${{ github.event_name == 'workflow_dispatch' }}
|
||||
type=raw,value=latest,enable=${{ github.event_name == 'workflow_dispatch' && inputs.latest }}
|
||||
type=ref,event=tag # if semver, also: type=semver,pattern={{version}} / {{major}}.{{minor}}
|
||||
|
||||
# Add OCI annotations to image index and publish tags
|
||||
- name: "Create image index"
|
||||
|
|
@ -191,7 +176,7 @@ jobs:
|
|||
|
||||
- name: "Inspect images"
|
||||
env:
|
||||
GITHUB_TAG: ${{ inputs.tag || github.ref_name }}
|
||||
GITHUB_TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
docker buildx imagetools inspect "$GHCR_IMAGE:latest"
|
||||
docker buildx imagetools inspect "$GHCR_IMAGE:$GITHUB_TAG"
|
||||
|
|
|
|||
14
.github/workflows/documentation-build.yml
vendored
14
.github/workflows/documentation-build.yml
vendored
|
|
@ -28,13 +28,12 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: "Install Graphviz"
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get install -y graphviz
|
||||
dot -V
|
||||
|
|
@ -42,10 +41,9 @@ jobs:
|
|||
- name: "Install Doxygen"
|
||||
uses: ssciwr/doxygen-install@v2
|
||||
with:
|
||||
version: "1.18.0"
|
||||
version: "1.16.1"
|
||||
|
||||
- name: "Update Doxygen Configuration"
|
||||
shell: bash
|
||||
run: |
|
||||
git diff Doxyfile
|
||||
doxygen -u Doxyfile
|
||||
|
|
@ -60,10 +58,9 @@ jobs:
|
|||
|
||||
- name: "Generate Documentation"
|
||||
if: always()
|
||||
shell: bash
|
||||
run: doxygen Doxyfile
|
||||
|
||||
- name: "Deploy to cockatrice.github.io/docs"
|
||||
- name: "Deploy to cockatrice.github.io"
|
||||
if: github.event_name == 'release' || github.event_name == 'workflow_dispatch'
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
with:
|
||||
|
|
@ -72,8 +69,3 @@ jobs:
|
|||
external_repository: Cockatrice/cockatrice.github.io
|
||||
publish_branch: master
|
||||
publish_dir: ./docs/html
|
||||
|
||||
- name: Link to Documentation Page
|
||||
if: github.event_name == 'release' || github.event_name == 'workflow_dispatch'
|
||||
shell: bash
|
||||
run: echo "::notice title=New documentation published::Shortly available at https://cockatrice.github.io/docs/"
|
||||
|
|
|
|||
2
.github/workflows/translations-pull.yml
vendored
2
.github/workflows/translations-pull.yml
vendored
|
|
@ -1,7 +1,7 @@
|
|||
name: Update Translations
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
|
|
|
|||
2
.github/workflows/translations-push.yml
vendored
2
.github/workflows/translations-push.yml
vendored
|
|
@ -1,7 +1,7 @@
|
|||
name: Update Translation Source
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
|
|
|
|||
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 }}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
# Integration Docs: https://help.transifex.com/en/articles/6265125-github-installation-and-configuration#h_de29ef5cec
|
||||
# Config Parameter: https://help.transifex.com/en/articles/6265125-github-installation-and-configuration#h_a734c3e219
|
||||
# Supported Files: https://help.transifex.com/en/collections/3519116-file-formats
|
||||
|
||||
filters:
|
||||
- filter_type: file
|
||||
file_format: QT
|
||||
source_language: en
|
||||
source_file: cockatrice/cockatrice_en@source.ts
|
||||
translation_files_expression: cockatrice/translations/cockatrice_<lang>.ts
|
||||
|
||||
- filter_type: file
|
||||
file_format: QT
|
||||
source_language: en
|
||||
source_file: oracle/oracle_en@source.ts
|
||||
translation_files_expression: oracle/translations/oracle_<lang>.ts
|
||||
100
CMakeLists.txt
100
CMakeLists.txt
|
|
@ -22,10 +22,6 @@ option(WITH_ORACLE "Build Cockatrice card database tool (Oracle)" ON)
|
|||
option(WITH_SERVER "Build Cockatrice server (Servatrice)" OFF)
|
||||
# Compile tests
|
||||
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
|
||||
option(USE_VCPKG "Use vcpkg regardless of OS" OFF)
|
||||
|
||||
|
|
@ -90,11 +86,11 @@ endif()
|
|||
|
||||
# A project name is needed for CPack
|
||||
# Version can be overriden by git tags, see cmake/getversion.cmake
|
||||
project("Cockatrice" VERSION 3.1.2)
|
||||
project("Cockatrice" VERSION 3.1.0)
|
||||
|
||||
# Set release name if not provided via env/cmake var
|
||||
if(NOT DEFINED GIT_TAG_RELEASENAME)
|
||||
set(GIT_TAG_RELEASENAME "Reality Check")
|
||||
set(GIT_TAG_RELEASENAME "Graduation Day")
|
||||
endif()
|
||||
|
||||
# Use c++20 for all targets
|
||||
|
|
@ -215,13 +211,6 @@ else()
|
|||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pipe")
|
||||
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)
|
||||
include(CrashReporting)
|
||||
|
||||
# GNU systems need to define the Mersenne exponent for the RNG to compile w/o warning
|
||||
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
||||
add_definitions("-DSFMT_MEXP=19937")
|
||||
|
|
@ -250,15 +239,6 @@ message(STATUS "Update Translations: ${UPDATE_TRANSLATIONS}")
|
|||
|
||||
include(FindQtRuntime)
|
||||
|
||||
# Reject APIs that were deprecated before the oldest Qt release we support
|
||||
# (6.4). Qt 6.5 renamed the switch, so use whichever spelling the found
|
||||
# version understands while still compiling everywhere.
|
||||
if(Qt6_VERSION VERSION_GREATER_EQUAL 6.5)
|
||||
add_compile_definitions(QT_DISABLE_DEPRECATED_UP_TO=0x060400)
|
||||
else()
|
||||
add_compile_definitions(QT_DISABLE_DEPRECATED_BEFORE=0x060400)
|
||||
endif()
|
||||
|
||||
set(CMAKE_AUTOMOC TRUE)
|
||||
|
||||
# Find other needed libraries
|
||||
|
|
@ -313,7 +293,7 @@ if(UNIX)
|
|||
if(CPACK_GENERATOR STREQUAL "RPM")
|
||||
set(CPACK_RPM_PACKAGE_LICENSE "GPLv2")
|
||||
set(CPACK_RPM_MAIN_COMPONENT "cockatrice")
|
||||
set(CPACK_RPM_PACKAGE_REQUIRES "protobuf, qt6-qtimageformats, qt6-qtmultimedia, qt6-qtsvg, qt6-qttools")
|
||||
set(CPACK_RPM_PACKAGE_REQUIRES "protobuf, qt6-qttools, qt6-qtsvg, qt6-qtmultimedia, qt6-qtimageformats")
|
||||
set(CPACK_RPM_PACKAGE_GROUP "Amusements/Games")
|
||||
set(CPACK_RPM_PACKAGE_URL "http://github.com/Cockatrice/Cockatrice")
|
||||
# stop directories from making package conflicts
|
||||
|
|
@ -331,13 +311,13 @@ if(UNIX)
|
|||
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
|
||||
set(CPACK_DEBIAN_PACKAGE_SECTION "games")
|
||||
set(CPACK_DEBIAN_PACKAGE_HOMEPAGE "http://github.com/Cockatrice/Cockatrice")
|
||||
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt6multimedia6, libqt6svg6, qt6-image-formats-plugins, qt6-qpa-plugins")
|
||||
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt6multimedia6, libqt6svg6, qt6-qpa-plugins, qt6-image-formats-plugins")
|
||||
set(CPACK_DEBIAN_PACKAGE_RECOMMENDS "libqt6sql6-mysql") # for connecting servatrice to a mysql db
|
||||
endif()
|
||||
endif()
|
||||
elseif(WIN32)
|
||||
set(CPACK_GENERATOR NSIS ${CPACK_GENERATOR})
|
||||
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||
if("${CMAKE_GENERATOR_PLATFORM}" MATCHES "(x64)")
|
||||
set(TRICE_IS_64_BIT 1)
|
||||
else()
|
||||
set(TRICE_IS_64_BIT 0)
|
||||
|
|
@ -346,64 +326,6 @@ elseif(WIN32)
|
|||
# Configure file with custom definitions for NSIS.
|
||||
configure_file("${COCKATRICE_CMAKE_PATH}/NSIS.definitions.nsh.in" "${PROJECT_BINARY_DIR}/NSIS.definitions.nsh")
|
||||
|
||||
# Installer-log sender for the AbortInstall Sentry hook (values parsed out of COCKATRICE_SENTRY_DSN in
|
||||
# cmake/CrashReporting.cmake). Generated only when a DSN was parsed: cmake/NSIS.template.in embeds it under
|
||||
# the same CPACK_SENTRY_ENABLED check, so a build without reporting never references or carries the helper.
|
||||
if(CPACK_SENTRY_ENABLED)
|
||||
configure_file(
|
||||
"${COCKATRICE_CMAKE_PATH}/SendInstallerLog.ps1.in" "${PROJECT_BINARY_DIR}/SendInstallerLog.ps1" @ONLY
|
||||
)
|
||||
endif()
|
||||
|
||||
# Every runtime DLL a Windows install has to contain, as paths relative to the
|
||||
# install root. .ci/compile.sh reads this and fails the build when the packaged
|
||||
# installer is missing one of them, or carries a runtime it does not know about.
|
||||
#
|
||||
# All three applications are packaged into the same directory on Windows, so
|
||||
# this is the union of their Qt module lists, not just the client's: Servatrice
|
||||
# contributes Qt6Sql.dll and Oracle contributes nothing the client lacks, and a
|
||||
# client-only manifest leaves Qt6Sql.dll unasserted even though servatrice.exe
|
||||
# is installed next to cockatrice.exe and cannot start without it.
|
||||
#
|
||||
# A statically linked Qt module ships no runtime DLL, so requiring one would
|
||||
# fail the package check on a static build for no reason. cockatrice/CMakeLists.txt
|
||||
# installs the runtime of every module listed here, and skips static ones the
|
||||
# same way, so the two can never disagree.
|
||||
#
|
||||
# Only the Qt modules, and that is deliberate. protobuf, Abseil, liblzma, zlib
|
||||
# and OpenSSL also ride in on the "*.dll" glob, and a client whose runtime is
|
||||
# absent, stale or of the wrong architecture fails to start with "abseil_dll.dll
|
||||
# is either not designed to run on Windows or it contains an error", so they
|
||||
# are exactly the files worth asserting.
|
||||
#
|
||||
# Their names cannot be derived from the vcpkg bin directory, which is what a
|
||||
# first attempt did and what a Windows build immediately refuted: bin holds
|
||||
# everything vcpkg installed, not everything the applications import. It also
|
||||
# contains gmock.dll, libprotobuf-lite.dll, libprotoc.dll, legacy.dll and three
|
||||
# pthreadVC*.dll FindThreads probes, none of which any shipped executable
|
||||
# links, so requiring them failed a build over a package that was correct.
|
||||
#
|
||||
# Deriving them from the executables' own import tables is the real answer,
|
||||
# and fixup_bundle already walks it to decide what to copy. Until that result
|
||||
# is available to CMake, requiring the vcpkg half would mean guessing names
|
||||
# again, which is what produced this bug report in the first place. So assert
|
||||
# what is derived from the build's own link lists and leave the rest to the
|
||||
# purge in cmake/NSIS.template.in, which removes every DLL regardless of name.
|
||||
set(_required_runtime "")
|
||||
foreach(_qt_module IN LISTS COCKATRICE_QT_MODULES SERVATRICE_QT_MODULES ORACLE_QT_MODULES)
|
||||
get_target_property(_qt_module_type ${_qt_module} TYPE)
|
||||
if(_qt_module_type STREQUAL "SHARED_LIBRARY")
|
||||
string(REGEX REPLACE "^Qt6::" "" _qt_component "${_qt_module}")
|
||||
list(APPEND _required_runtime "Qt6${_qt_component}.dll")
|
||||
endif()
|
||||
endforeach()
|
||||
list(APPEND _required_runtime "Plugins/platforms/qwindows.dll")
|
||||
|
||||
list(REMOVE_DUPLICATES _required_runtime)
|
||||
list(SORT _required_runtime)
|
||||
string(REPLACE ";" "\n" _required_runtime "${_required_runtime}")
|
||||
file(WRITE "${PROJECT_BINARY_DIR}/required-runtime.txt" "${_required_runtime}\n")
|
||||
|
||||
# include vcredist into the package; NSIS will take care of running it
|
||||
if(VCREDISTRUNTIME_FOUND)
|
||||
install(FILES "${VCREDISTRUNTIME_FILE}" DESTINATION ./)
|
||||
|
|
@ -429,9 +351,6 @@ if(WITH_ORACLE OR WITH_CLIENT)
|
|||
add_subdirectory(${CMAKE_SOURCE_DIR}/libcockatrice_models ${CMAKE_BINARY_DIR}/libcockatrice_models)
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/libcockatrice_filters ${CMAKE_BINARY_DIR}/libcockatrice_filters)
|
||||
endif()
|
||||
if(WITH_CLIENT)
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/libcockatrice_game ${CMAKE_BINARY_DIR}/libcockatrice_game)
|
||||
endif()
|
||||
|
||||
if(WITH_SERVER)
|
||||
add_subdirectory(servatrice)
|
||||
|
|
@ -443,13 +362,6 @@ if(WITH_CLIENT)
|
|||
set(CPACK_INSTALL_CMAKE_PROJECTS "Cockatrice;Cockatrice;ALL;/" ${CPACK_INSTALL_CMAKE_PROJECTS})
|
||||
endif()
|
||||
|
||||
if(WIN32 AND WITH_CLIENT)
|
||||
# Runs from the installer's $PLUGINSDIR when a purge fails, and is never installed
|
||||
# (see LogProcessesUsingInstallDir in cmake/NSIS.template.in), so it has no install
|
||||
# rules and is not part of the client's staged tree.
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/tools/installer-holder-probe ${CMAKE_BINARY_DIR}/tools/installer-holder-probe)
|
||||
endif()
|
||||
|
||||
if(WITH_ORACLE)
|
||||
add_subdirectory(oracle)
|
||||
set(CPACK_INSTALL_CMAKE_PROJECTS "Oracle;Oracle;ALL;/" ${CPACK_INSTALL_CMAKE_PROJECTS})
|
||||
|
|
@ -462,5 +374,5 @@ endif()
|
|||
|
||||
if(Qt6_FOUND AND Qt6_VERSION_MINOR GREATER_EQUAL 3)
|
||||
# Qt6.3+ requires project finalization to support translations
|
||||
qt_finalize_project()
|
||||
qt6_finalize_project()
|
||||
endif()
|
||||
|
|
|
|||
113
Doxyfile
113
Doxyfile
|
|
@ -1,4 +1,4 @@
|
|||
# Doxyfile 1.18.0
|
||||
# Doxyfile 1.16.1
|
||||
|
||||
# This file describes the settings to be used by the documentation system
|
||||
# Doxygen (www.doxygen.org) for a project.
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
# TAG = value [value, ...]
|
||||
# For lists, items can also be appended using:
|
||||
# TAG += value [value, ...]
|
||||
# Values that contain spaces should be placed between quotes (" ").
|
||||
# Values that contain spaces should be placed between quotes (\" \").
|
||||
#
|
||||
# Note:
|
||||
#
|
||||
|
|
@ -81,7 +81,7 @@ OUTPUT_DIRECTORY = docs
|
|||
# and will distribute the generated files over these directories. Enabling this
|
||||
# option can be useful when feeding Doxygen a huge amount of source files, where
|
||||
# putting all generated files in the same directory would otherwise cause
|
||||
# performance problems for the file system. Adjust CREATE_SUBDIRS_LEVEL to
|
||||
# performance problems for the file system. Adapt CREATE_SUBDIRS_LEVEL to
|
||||
# control the number of sub-directories.
|
||||
# The default value is: NO.
|
||||
|
||||
|
|
@ -802,7 +802,7 @@ GENERATE_REQUIREMENTS = YES
|
|||
|
||||
# The REQ_TRACEABILITY_INFO tag controls if traceability information is shown on
|
||||
# the requirements page (only relevant when using \requirement comment blocks).
|
||||
# The setting NO will disable the traceability information altogether. The
|
||||
# The setting NO will disable the traceablility information altogether. The
|
||||
# setting UNSATISFIED_ONLY will show a list of requirements that are missing a
|
||||
# satisfies relation (through the command: \satisfies). Similarly the setting
|
||||
# UNVERIFIED_ONLY will show a list of requirements that are missing a verifies
|
||||
|
|
@ -855,8 +855,8 @@ SHOW_NAMESPACES = YES
|
|||
# The FILE_VERSION_FILTER tag can be used to specify a program or script that
|
||||
# Doxygen should invoke to get the current version for each file (typically from
|
||||
# the version control system). Doxygen will invoke the program by executing (via
|
||||
# popen()) the command command input_file, where command is the value of the
|
||||
# FILE_VERSION_FILTER tag, and input_file is the name of an input file provided
|
||||
# popen()) the command command input-file, where command is the value of the
|
||||
# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided
|
||||
# by Doxygen. Whatever the program writes to standard output is used as the file
|
||||
# version. For an example see the documentation.
|
||||
|
||||
|
|
@ -1066,11 +1066,25 @@ INPUT_FILE_ENCODING =
|
|||
# provided as Doxygen C comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08,
|
||||
# *.f18, *.f, *.for, *.vhd, *.vhdl, *.ucf, *.qsf and *.ice.
|
||||
|
||||
FILE_PATTERNS = *.cpp \
|
||||
FILE_PATTERNS = *.cc \
|
||||
*.cxx \
|
||||
*.cxxm \
|
||||
*.cpp \
|
||||
*.cppm \
|
||||
*.ccm \
|
||||
*.c++ \
|
||||
*.c++m \
|
||||
*.ii \
|
||||
*.ixx \
|
||||
*.ipp \
|
||||
*.i++ \
|
||||
*.inl \
|
||||
*.h \
|
||||
*.hh \
|
||||
*.hxx \
|
||||
*.hpp \
|
||||
*.h++ \
|
||||
*.markdown \
|
||||
*.md \
|
||||
*.dox \
|
||||
*.proto
|
||||
|
|
@ -1629,8 +1643,8 @@ DOCSET_PUBLISHER_NAME = Publisher
|
|||
# a.o. the download links, offline (the HTML help workshop was already many
|
||||
# years in maintenance mode). You can download the HTML help workshop from the
|
||||
# web archives at Installation executable (see:
|
||||
# http://web.archive.org/web/20160201063255/https://download.microsoft.com/downl
|
||||
# oad/0/A/9/0A939EF6-E31C-430F-A3DF-DFAE7960D564/htmlhelp.exe).
|
||||
# http://web.archive.org/web/20160201063255/http://download.microsoft.com/downlo
|
||||
# ad/0/A/9/0A939EF6-E31C-430F-A3DF-DFAE7960D564/htmlhelp.exe).
|
||||
#
|
||||
# The HTML Help Workshop contains a compiler that can convert all HTML output
|
||||
# generated by Doxygen into a single compiled HTML file (.chm). Compiled HTML
|
||||
|
|
@ -1937,7 +1951,7 @@ MATHJAX_FORMAT = HTML-CSS
|
|||
# output directory using the MATHJAX_RELPATH option. For Mathjax version 2 the
|
||||
# destination directory should contain the MathJax.js script. For instance, if
|
||||
# the mathjax directory is located at the same level as the HTML output
|
||||
# directory, then MATHJAX_RELPATH should be ../mathjax. For Mathjax versions 3
|
||||
# directory, then MATHJAX_RELPATH should be ../mathjax.s For Mathjax versions 3
|
||||
# and 4 the destination directory should contain the tex-<format>.js script
|
||||
# (where <format> is either chtml or svg). The default value points to the
|
||||
# MathJax Content Delivery Network so you can quickly see the result without
|
||||
|
|
@ -2158,8 +2172,9 @@ EXTRA_PACKAGES =
|
|||
#
|
||||
# Note: Only use a user-defined header if you know what you are doing!
|
||||
# Note: The header is subject to change so you typically have to regenerate the
|
||||
# default header when upgrading to a newer version of Doxygen. For a description
|
||||
# of the possible markers and block names see the documentation.
|
||||
# default header when upgrading to a newer version of Doxygen. The following
|
||||
# commands have a special meaning inside the header (and footer): For a
|
||||
# description of the possible markers and block names see the documentation.
|
||||
# This tag requires that the tag GENERATE_LATEX is set to YES.
|
||||
|
||||
LATEX_HEADER =
|
||||
|
|
@ -2642,19 +2657,6 @@ HAVE_DOT = YES
|
|||
|
||||
DOT_NUM_THREADS = 0
|
||||
|
||||
# The DOT_BATCH_SIZE specifies the number of dot graphs Doxygen is allowed to
|
||||
# compile in a single invocation of dot. When set to 1 Doxygen will invoke dot
|
||||
# for each graph separately, which can cause significant process creation
|
||||
# overhead especially on systems with many CPU cores. Together with
|
||||
# DOT_NUM_THREADS this setting can be used to optimise the dot processing speed
|
||||
# for a particular system. Doxygen will try to give each thread a balanced batch
|
||||
# of work. If the total number of graphs to process exceeds DOT_NUM_THREADS *
|
||||
# DOT_BATCH_SIZE then additional batches will be created for dot to process.
|
||||
# Minimum value: 1, maximum value: 1000, default value: 50.
|
||||
# This tag requires that the tag HAVE_DOT is set to YES.
|
||||
|
||||
DOT_BATCH_SIZE = 50
|
||||
|
||||
# DOT_COMMON_ATTR is common attributes for nodes, edges and labels of
|
||||
# subgraphs. When you want a differently looking font in the dot files that
|
||||
# Doxygen generates you can specify fontname, fontcolor and fontsize attributes.
|
||||
|
|
@ -2946,58 +2948,6 @@ PLANTUML_INCLUDE_PATH =
|
|||
|
||||
PLANTUMLFILE_DIRS =
|
||||
|
||||
# When using Mermaid diagrams with CLI rendering, the MERMAID_PATH tag should be
|
||||
# used to specify the directory where the mmdc (Mermaid CLI) executable can be
|
||||
# found. If left blank, CLI-based rendering is disabled. For HTML output,
|
||||
# client-side rendering via JavaScript is used by default and does not require
|
||||
# mmdc. For LaTeX/PDF output, mmdc is required to pre-generate images. Doxygen
|
||||
# will generate a warning when CLI rendering is needed but mmdc is not
|
||||
# available.
|
||||
|
||||
MERMAID_PATH =
|
||||
|
||||
# When using Mermaid diagrams, the MERMAID_CONFIG_FILE tag can be used to
|
||||
# specify a JSON configuration file for the Mermaid CLI tool (mmdc). This file
|
||||
# can contain theme settings and other Mermaid configuration options.
|
||||
|
||||
MERMAID_CONFIG_FILE =
|
||||
|
||||
# The MERMAID_RENDER_MODE tag selects how Mermaid diagrams are rendered.
|
||||
# Possible values are: AUTO (use client-side rendering for HTML and mmdc for
|
||||
# LaTeX/PDF and other formats. If MERMAID_PATH is not set, non-HTML diagrams
|
||||
# will produce a warning), CLI (use the mmdc tool to pre-generate images
|
||||
# (requires Node.js and mermaid-js/mermaid-cli). Works for all output formats)
|
||||
# and CLIENT_SIDE (embed mermaid.js in HTML output for client-side rendering.
|
||||
# Does not require mmdc but only works for HTML output).
|
||||
# The default value is: AUTO.
|
||||
|
||||
MERMAID_RENDER_MODE = AUTO
|
||||
|
||||
# The MERMAID_JS_URL tag specifies the URL to load mermaid.js from when using
|
||||
# client-side rendering (MERMAID_RENDER_MODE is CLIENT_SIDE or AUTO). The
|
||||
# default points to the latest Mermaid v11 release on the jsDelivr CDN.
|
||||
#
|
||||
# The default CDN URL requires internet access when viewing the generated
|
||||
# documentation. For offline use, download mermaid.esm.min.mjs and set this to a
|
||||
# relative path, or use MERMAID_RENDER_MODE=CLI to pre-generate images instead.
|
||||
# Examples:
|
||||
# - Latest v11 (default):
|
||||
# https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs
|
||||
# - Pinned version:
|
||||
# https://cdn.jsdelivr.net/npm/mermaid@11.3.0/dist/mermaid.esm.min.mjs
|
||||
# - Local copy: ./mermaid.esm.min.mjs (user must place file in HTML output
|
||||
# directory)
|
||||
# The default value is:
|
||||
# https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs.
|
||||
|
||||
MERMAID_JS_URL = https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs
|
||||
|
||||
# The MERMAIDFILE_DIRS tag can be used to specify one or more directories that
|
||||
# contain Mermaid files that are included in the documentation (see the
|
||||
# \mermaidfile command).
|
||||
|
||||
MERMAIDFILE_DIRS =
|
||||
|
||||
# The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of nodes
|
||||
# that will be shown in the graph. If the number of nodes in a graph becomes
|
||||
# larger than this value, Doxygen will truncate the graph, which is visualized
|
||||
|
|
@ -3022,6 +2972,15 @@ DOT_GRAPH_MAX_NODES = 50
|
|||
|
||||
MAX_DOT_GRAPH_DEPTH = 0
|
||||
|
||||
# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output
|
||||
# files in one run (i.e. multiple -o and -T options on the command line). This
|
||||
# makes dot run faster, but since only newer versions of dot (>1.8.10) support
|
||||
# this, this feature is disabled by default.
|
||||
# The default value is: NO.
|
||||
# This tag requires that the tag HAVE_DOT is set to YES.
|
||||
|
||||
DOT_MULTI_TARGETS = NO
|
||||
|
||||
# If the GENERATE_LEGEND tag is set to YES Doxygen will generate a legend page
|
||||
# explaining the meaning of the various boxes and arrows in the dot generated
|
||||
# graphs.
|
||||
|
|
|
|||
39
README.md
39
README.md
|
|
@ -15,10 +15,9 @@
|
|||
---
|
||||
|
||||
<br><pre><p align='center'>
|
||||
<b>To get started with Cockatrice ⇢ [view our webpage](https://cockatrice.github.io/)</b>
|
||||
<b>To get support, or suggest changes to the app ⇢ [file an issue](https://github.com/Cockatrice/Cockatrice/issues) ([How?](https://github.com/Cockatrice/Cockatrice/wiki/How-to-Create-a-GitHub-Ticket-Regarding-Cockatrice))</b><br>
|
||||
<b>To learn about the app and its code ⇢ [read our code documentation](https://cockatrice.github.io/docs/)</b>
|
||||
<b>To help with development ⇢ learn [how to contribute](#contribute)</b>
|
||||
<b>To get started with Cockatrice ⇢ [view our webpage](https://cockatrice.github.io/)</b><br>
|
||||
<b>To get support, or suggest changes to the app ⇢ [file an issue](https://github.com/Cockatrice/Cockatrice/issues) ([How?](https://github.com/Cockatrice/Cockatrice/wiki/How-to-Create-a-GitHub-Ticket-Regarding-Cockatrice))</b>
|
||||
<b>To help with development ⇢ learn [how to contribute](#contribute-)</b>
|
||||
</pre><p><br>
|
||||
|
||||
|
||||
|
|
@ -84,7 +83,7 @@ Join our [Discord community](https://discord.gg/3Z9yzmA) to connect with other p
|
|||
|
||||
### Code
|
||||
|
||||
To commit code to the project, please review our [Contribution Guidelines](https://github.com/Cockatrice/Cockatrice/blob/master/.github/CONTRIBUTING.md) first.<br>
|
||||
To contribute code to the project, please review our [guidelines](https://github.com/Cockatrice/Cockatrice/blob/master/.github/CONTRIBUTING.md) first.<br>
|
||||
We maintain two tags for contributors to easier find issues to potentially work on:
|
||||
- [](https://github.com/Cockatrice/Cockatrice/issues?utf8=%E2%9C%93&q=is%3Aopen%20is%3Aissue%20label%3A%22Good%20first%20issue%22%20)<br>
|
||||
Issues tagged in this way provide a simple way to get started. They don't require much experience to be worked on.
|
||||
|
|
@ -96,17 +95,15 @@ We'll happily advice on how best to implement a feature, or we can show you wher
|
|||
|
||||
You can also have a look at our `Todo List` in our [Code Documentation](https://cockatrice.github.io/docs) or search the repo for [`\todo` comments](https://github.com/search?q=repo%3ACockatrice%2FCockatrice%20%5Ctodo&type=code).
|
||||
|
||||
### Documentation [](https://github.com/Cockatrice/Cockatrice/actions/workflows/documentation-build.yml?query=event%3Arelease+event%3Aworkflow_dispatch)
|
||||
### Documentation [](https://github.com/Cockatrice/Cockatrice/actions/workflows/documentation-build.yml?query=event%3Apush)
|
||||
|
||||
There are various places where useful information for different needs are maintained:
|
||||
- [Official Code Documentation](https://cockatrice.github.io/docs/)
|
||||
- [Official Wiki](https://github.com/Cockatrice/Cockatrice/wiki) `Community supported`
|
||||
- [Official Webpage](https://cockatrice.github.io/)
|
||||
- [Official README](https://github.com/Cockatrice/Cockatrice/blob/master/README.md) `This file`
|
||||
- [Official CONTRIBUTING](https://github.com/Cockatrice/Cockatrice/blob/master/.github/CONTRIBUTING.md)
|
||||
|
||||
Cockatrice tries to use the [Google Developer Documentation Style Guide](https://developers.google.com/style/) to ensure consistent documentation.
|
||||
We encourage everybody to improve the documentation by suggesting edits based on this guide.
|
||||
Cockatrice tries to use the [Google Developer Documentation Style Guide](https://developers.google.com/style/) to ensure consistent documentation. We encourage you to improve the documentation by suggesting edits based on this guide.
|
||||
|
||||
### Translation [](https://explore.transifex.com/cockatrice/cockatrice/)
|
||||
|
||||
|
|
@ -152,21 +149,15 @@ You can then
|
|||
<br>
|
||||
|
||||
The following flags (with their non-default values) can be passed to `cmake`:
|
||||
| Flag | Description |
|
||||
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `-DWITH_SERVER=1` | Build <kbd>Servatrice</kbd> server |
|
||||
| `-DWITH_CLIENT=0` | Don't build <kbd>Cockatrice</kbd> client |
|
||||
| `-DWITH_ORACLE=0` | Don't build <kbd>Oracle</kbd> card database tool |
|
||||
| `-DCMAKE_BUILD_TYPE=Debug` | Compile in debug mode<br> Enables extra logging output, debug symbols, and much more verbose compiler warnings |
|
||||
| `-DWARNING_AS_ERROR=0` | Don't treat compilation warnings as errors in debug mode |
|
||||
| `-DUPDATE_TRANSLATIONS=1` | Configure `make` to update the translation .ts files for new strings in the source code<br> **Note:** `make clean` will remove the .ts files |
|
||||
| `-DTEST=1` | Enable regression tests<br> **Note:** `make test` to run tests, *googletest* will be downloaded if not available |
|
||||
|
||||
> [!TIP]
|
||||
> Build code documentation ([Doxygen](https://www.doxygen.nl)) according to our [configuration](https://github.com/Cockatrice/Cockatrice/blob/master/Doxyfile) into `/docs`:
|
||||
> ```bash
|
||||
> doxygen Doxyfile
|
||||
> ```
|
||||
| Flag | Description |
|
||||
| --- | --- |
|
||||
| `-DWITH_SERVER=1` | Build <kbd>Servatrice</kbd> server |
|
||||
| `-DWITH_CLIENT=0` | Don't build <kbd>Cockatrice</kbd> client |
|
||||
| `-DWITH_ORACLE=0` | Don't build <kbd>Oracle</kbd> card database tool |
|
||||
| `-DCMAKE_BUILD_TYPE=Debug` | Compile in debug mode<br> Enables extra logging output, debug symbols, and much more verbose compiler warnings |
|
||||
| `-DWARNING_AS_ERROR=0` | Don't treat compilation warnings as errors in debug mode |
|
||||
| `-DUPDATE_TRANSLATIONS=1` | Configure `make` to update the translation .ts files for new strings in the source code<br> **Note:** `make clean` will remove the .ts files |
|
||||
| `-DTEST=1` | Enable regression tests<br> **Note:** `make test` to run tests, *googletest* will be downloaded if not available |
|
||||
|
||||
|
||||
# Run
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -1,212 +0,0 @@
|
|||
# Crash reporting via sentry-native (Sentry protocol, MIT).
|
||||
# Included once from the root CMakeLists.txt, right after the sanitizer/coverage helpers.
|
||||
#
|
||||
# Inputs:
|
||||
# WITH_CRASH_REPORTING -- build the client with the sentry-native SDK (default OFF, our release
|
||||
# CI passes ON; packagers and from-source builds stay SDK-free until this
|
||||
# ships something real, so offline package builds never fetch the SDK)
|
||||
# COCKATRICE_SENTRY_DSN -- Sentry DSN baked into the build (default empty = SDK init skipped at runtime)
|
||||
# COCKATRICE_SENTRY_TAG -- sentry-native release tag to vendor via FetchContent
|
||||
# ENABLE_SANITIZERS -- forces the sentry backend to "none" (crashpad vs ASan conflict)
|
||||
# Defines: cockatrice_target_enable_crash_reporting(<target>)
|
||||
|
||||
option(WITH_CRASH_REPORTING "Build with crash reporting via sentry-native" OFF)
|
||||
set(COCKATRICE_SENTRY_DSN
|
||||
""
|
||||
CACHE STRING "Sentry DSN baked into the client; empty disables crash-report upload at runtime"
|
||||
)
|
||||
# Pinned stable release; tag verified at https://github.com/getsentry/sentry-native/tags.
|
||||
set(COCKATRICE_SENTRY_TAG
|
||||
"0.17.3"
|
||||
CACHE STRING "sentry-native release tag to vendor via FetchContent"
|
||||
)
|
||||
|
||||
# Wire the SDK (or the no-op fallback) into a client target. Always callable: with the
|
||||
# option OFF the compile definitions below make crash_reporter.cpp compile to no-ops.
|
||||
function(cockatrice_target_enable_crash_reporting _target)
|
||||
if(WITH_CRASH_REPORTING AND TARGET sentry::sentry)
|
||||
target_link_libraries(${_target} PRIVATE sentry::sentry)
|
||||
target_compile_definitions(${_target} PRIVATE COCKATRICE_WITH_CRASH_REPORTING=1)
|
||||
else()
|
||||
target_compile_definitions(${_target} PRIVATE COCKATRICE_WITH_CRASH_REPORTING=0)
|
||||
endif()
|
||||
# Always baked in (possibly empty): main() prefers the COCKATRICE_SENTRY_DSN env var at runtime.
|
||||
target_compile_definitions(${_target} PRIVATE "COCKATRICE_SENTRY_DSN=\"${COCKATRICE_SENTRY_DSN}\"")
|
||||
# Sentry release for events from this target: PROJECT_VERSION (getversion.cmake ran before this
|
||||
# file was included). Deliberately not PROJECT_VERSION_FRIENDLY / VERSION_STRING — that carries
|
||||
# a build date, so every build of the same version would land in a different Sentry release.
|
||||
# PROJECT_VERSION matches what the installer reports in its own log and what CPack derives the
|
||||
# package version from, so client and installer events group under one release.
|
||||
target_compile_definitions(${_target} PRIVATE "COCKATRICE_SENTRY_RELEASE=\"${PROJECT_VERSION}\"")
|
||||
endfunction()
|
||||
|
||||
if(WITH_CRASH_REPORTING AND WITH_CLIENT)
|
||||
include(FetchContent)
|
||||
|
||||
if(ENABLE_SANITIZERS)
|
||||
# The crash handler cannot run under ASan/UBSan, so there is no backend to capture with.
|
||||
set(_sentry_backend "none")
|
||||
else()
|
||||
# Breakpad on every OS (increment 3 probe, 2026-10-14): the crashpad sources
|
||||
# do vendor friction-free (FetchContent inits all submodules recursively by
|
||||
# default, verified populated in a local build tree), but the crashpad backend
|
||||
# would need its out-of-process crashpad_handler binary installed next to the
|
||||
# exe, into the macOS bundle, and through NSIS, plus handler-path wiring and
|
||||
# notarization care. Breakpad is in-process, needs no extra binary and no
|
||||
# packaging on any OS sentry-native builds, so simplicity wins.
|
||||
set(_sentry_backend "breakpad")
|
||||
endif()
|
||||
# FORCE so toggling ENABLE_SANITIZERS on a reused build dir cannot leave a stale backend behind.
|
||||
set(SENTRY_BACKEND
|
||||
"${_sentry_backend}"
|
||||
CACHE STRING "Crash handler backend for sentry-native" FORCE
|
||||
)
|
||||
unset(_sentry_backend)
|
||||
|
||||
if(NOT MSVC)
|
||||
# Symbol upload needs debug info in Release binaries: the top-level flags build GCC
|
||||
# Release as -s -O2 (stripped, no DWARF) and Clang/Apple Release as plain -O2 (no DWARF),
|
||||
# both of which upload nothing useful. Keep -g and drop the strip flag while the SDK is
|
||||
# on; the OFF default leaves local, distro and Debug builds untouched. MSVC already
|
||||
# links /DEBUG with PDBs in release (see top-level CMakeLists.txt), so it needs nothing.
|
||||
string(REGEX REPLACE "(^| )-s( |$)" "\\1\\2" CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE}")
|
||||
if(NOT CMAKE_CXX_FLAGS_RELEASE MATCHES "(^| )-g( |$)")
|
||||
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -g")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# No Qt integration: it runs find_package(Qt6) inside the vendored build, which
|
||||
# cannot see our thin/aqt Qt layouts (macOS CI) under foreign toolchains, and its
|
||||
# breadcrumbs could carry unredacted log lines into native crash events that bypass
|
||||
# before_send. Our attached redacted log preview already carries the log context.
|
||||
set(SENTRY_INTEGRATION_QT
|
||||
OFF
|
||||
CACHE BOOL "Qt log-message breadcrumbs for sentry-native (off: discovery + secret surface)" FORCE
|
||||
)
|
||||
# The transport stays at the SDK default (curl on Apple/Linux, winhttp on Windows).
|
||||
set(SENTRY_BUILD_TESTS
|
||||
OFF
|
||||
CACHE BOOL "Build sentry-native tests" FORCE
|
||||
)
|
||||
set(SENTRY_BUILD_EXAMPLES
|
||||
OFF
|
||||
CACHE BOOL "Build sentry-native examples" FORCE
|
||||
)
|
||||
|
||||
fetchcontent_declare(
|
||||
sentry
|
||||
GIT_REPOSITORY https://github.com/getsentry/sentry-native.git
|
||||
GIT_TAG ${COCKATRICE_SENTRY_TAG}
|
||||
GIT_SHALLOW TRUE
|
||||
)
|
||||
fetchcontent_makeavailable(sentry)
|
||||
|
||||
# Vendored sentry-native (and its breakpad/crashpad trees) warn under our -Wall -Wextra
|
||||
# (e.g. breakpad's -Wmissing-field-initializers), and WARNING_AS_ERROR would fail the build
|
||||
# on code we cannot fix. Those findings are not actionable for us, so opt the whole vendored
|
||||
# tree out of warnings-as-errors while keeping the warnings themselves visible; our own
|
||||
# targets keep -Werror. Interface libraries compile nothing, so they are skipped outright:
|
||||
# an INTERFACE -Wno-error would propagate to our consumers and weaken their build.
|
||||
function(_cockatrice_sentry_opt_out_of_werror _dir)
|
||||
get_property(
|
||||
_sentry_targets
|
||||
DIRECTORY "${_dir}"
|
||||
PROPERTY BUILDSYSTEM_TARGETS
|
||||
)
|
||||
foreach(_sentry_target IN LISTS _sentry_targets)
|
||||
get_target_property(_sentry_type ${_sentry_target} TYPE)
|
||||
if(_sentry_type STREQUAL "INTERFACE_LIBRARY")
|
||||
continue()
|
||||
elseif(_sentry_type MATCHES "LIBRARY|EXECUTABLE")
|
||||
get_target_property(_sentry_imported ${_sentry_target} IMPORTED)
|
||||
if(NOT _sentry_imported)
|
||||
target_compile_options(${_sentry_target} PRIVATE -Wno-error)
|
||||
endif()
|
||||
endif()
|
||||
endforeach()
|
||||
get_property(
|
||||
_sentry_subdirs
|
||||
DIRECTORY "${_dir}"
|
||||
PROPERTY SUBDIRECTORIES
|
||||
)
|
||||
foreach(_sentry_subdir IN LISTS _sentry_subdirs)
|
||||
_cockatrice_sentry_opt_out_of_werror("${_sentry_subdir}")
|
||||
endforeach()
|
||||
endfunction()
|
||||
if(NOT MSVC)
|
||||
_cockatrice_sentry_opt_out_of_werror("${sentry_SOURCE_DIR}")
|
||||
endif()
|
||||
|
||||
message(STATUS "Crash reporting: sentry-native ${COCKATRICE_SENTRY_TAG} (backend: ${SENTRY_BACKEND})")
|
||||
else()
|
||||
message(STATUS "Crash reporting: SDK not vendored "
|
||||
"(WITH_CRASH_REPORTING=${WITH_CRASH_REPORTING}, WITH_CLIENT=${WITH_CLIENT})"
|
||||
)
|
||||
endif()
|
||||
|
||||
# Packaged release binaries must not ship the DWARF that the symbol upload needs: it is
|
||||
# 80-90% of an unstripped binary and nobody downloads a 500 MB installer to get it. The
|
||||
# build tree keeps -g (CI uploads symbols from there); the *packaged copy* is split at
|
||||
# install time by a CPack install script, which extracts the debug info next to the staging
|
||||
# dir and strips the binary. Only ELF (Linux/BSD) needs this: Windows links /DEBUG with
|
||||
# separate PDBs, and macOS keeps its dSYM extracted from the bundle.
|
||||
option(COCKATRICE_SPLIT_DEBUG_SYMBOLS "Strip DWARF out of packaged release binaries" ON)
|
||||
set(COCKATRICE_DEBUG_SYMBOL_DIR
|
||||
"${CMAKE_BINARY_DIR}/debug-symbols"
|
||||
CACHE PATH "Where the packaged binaries' split debug information is written (kept out of the package)"
|
||||
)
|
||||
if(WITH_CRASH_REPORTING
|
||||
AND WITH_CLIENT
|
||||
AND COCKATRICE_SPLIT_DEBUG_SYMBOLS
|
||||
AND CMAKE_BUILD_TYPE STREQUAL "Release"
|
||||
AND UNIX
|
||||
AND NOT APPLE
|
||||
)
|
||||
find_program(COCKATRICE_OBJCOPY NAMES objcopy)
|
||||
find_program(COCKATRICE_STRIP_TOOL NAMES strip)
|
||||
if(COCKATRICE_OBJCOPY AND COCKATRICE_STRIP_TOOL)
|
||||
configure_file(
|
||||
"${CMAKE_CURRENT_LIST_DIR}/SplitDebugSymbols.cmake.in" "${CMAKE_BINARY_DIR}/cmake/SplitDebugSymbols.cmake" @ONLY
|
||||
)
|
||||
# PRE_BUILD runs after CPack stages the install tree and before the package is produced.
|
||||
# CPACK_INSTALL_SCRIPTS would run too early (staging is still empty); the script walks
|
||||
# CPACK_TEMPORARY_DIRECTORY, never CMAKE_INSTALL_PREFIX, and refuses to run otherwise.
|
||||
list(APPEND CPACK_PRE_BUILD_SCRIPTS "${CMAKE_BINARY_DIR}/cmake/SplitDebugSymbols.cmake")
|
||||
message(STATUS "Crash reporting: packaged binaries stripped, debug info split to ${COCKATRICE_DEBUG_SYMBOL_DIR}")
|
||||
else()
|
||||
message(WARNING "COCKATRICE_SPLIT_DEBUG_SYMBOLS is ON but objcopy/strip were not found; "
|
||||
"packaged release binaries will keep their debug information"
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Installer failure reporting (increment 4): parse COCKATRICE_SENTRY_DSN into the parts the Windows installer
|
||||
# needs. These are CPACK_* so CPack snapshots them at include(CPack) time and substitutes them into
|
||||
# cmake/NSIS.template.in at package time; on non-Windows they are inert (only the NSIS template consumes them).
|
||||
# Set from COCKATRICE_SENTRY_DSN on every configure, so a changed or removed DSN can never leave a stale value
|
||||
# behind. Plain variables, not CACHE: they only need to be in scope when include(CPack) later snapshots every
|
||||
# CPACK_* variable into CPackConfig.cmake.
|
||||
set(CPACK_SENTRY_ENABLED "0")
|
||||
set(CPACK_SENTRY_KEY "")
|
||||
set(CPACK_SENTRY_ENVELOPE_URL "")
|
||||
set(CPACK_SENTRY_DSN_FULL "")
|
||||
# Deliberately PROJECT_VERSION: that is what the client bakes as its Sentry release and what CI passes to
|
||||
# `sentry-cli debug-files upload`, so installer and client events group under one release. It is not the
|
||||
# CPACK_PACKAGE_VERSION the installer logs in its own log line — on Development tags PROJECT_VERSION carries a
|
||||
# "-<label>" suffix (getversion.cmake) while CPACK_PACKAGE_VERSION is only MAJOR.MINOR.PATCH.
|
||||
set(CPACK_SENTRY_RELEASE "${PROJECT_VERSION}")
|
||||
|
||||
# Canonical <scheme>://<key>@<host>/<project-id>. Deliberately unanchored, so a trailing slash still matches.
|
||||
# A self-hosted DSN with a path prefix (e.g. GlitchTip behind a subpath) does not match and falls through to the
|
||||
# disabled-and-warn branch instead of deriving a wrong endpoint from the tail of the path.
|
||||
if(COCKATRICE_SENTRY_DSN MATCHES "(https?)://([^@]+)@([^/]+)/([0-9]+)")
|
||||
set(CPACK_SENTRY_ENABLED "1")
|
||||
set(CPACK_SENTRY_KEY "${CMAKE_MATCH_2}")
|
||||
# Scheme comes from the DSN, not an assumption: self-hosted (GlitchTip) DSNs may be plain http.
|
||||
set(CPACK_SENTRY_ENVELOPE_URL "${CMAKE_MATCH_1}://${CMAKE_MATCH_3}/api/${CMAKE_MATCH_4}/envelope/")
|
||||
set(CPACK_SENTRY_DSN_FULL "${COCKATRICE_SENTRY_DSN}")
|
||||
elseif(NOT COCKATRICE_SENTRY_DSN STREQUAL "")
|
||||
message(WARNING "COCKATRICE_SENTRY_DSN is set but malformed "
|
||||
"(expected <scheme>://<key>@<host>/<project-id>); installer log reporting disabled"
|
||||
)
|
||||
endif()
|
||||
|
|
@ -7,12 +7,6 @@
|
|||
# Output: ORACLE_QT_MODULES
|
||||
# Output: TEST_QT_MODULES
|
||||
|
||||
# Every Qt module an application links against, as the DLL names a Windows
|
||||
# install has to contain. Core is listed explicitly rather than left to arrive
|
||||
# transitively: it is the one module nothing can do without, and required-runtime.txt
|
||||
# is built from these lists, so a module missing here is a runtime the package
|
||||
# check never asserts. That is how Qt6Core.dll went unasserted while every other
|
||||
# Qt module was checked.
|
||||
set(REQUIRED_QT_COMPONENTS Core)
|
||||
if(WITH_SERVER)
|
||||
set(_SERVATRICE_NEEDED Network Sql WebSockets)
|
||||
|
|
@ -20,7 +14,6 @@ endif()
|
|||
if(WITH_CLIENT)
|
||||
set(_COCKATRICE_NEEDED
|
||||
Concurrent
|
||||
Core
|
||||
Gui
|
||||
Multimedia
|
||||
Network
|
||||
|
|
@ -35,14 +28,12 @@ if(WITH_CLIENT)
|
|||
)
|
||||
endif()
|
||||
if(WITH_ORACLE)
|
||||
set(_ORACLE_NEEDED Concurrent Network Svg Widgets Xml)
|
||||
set(_ORACLE_NEEDED Concurrent Network Svg Widgets)
|
||||
endif()
|
||||
if(TEST)
|
||||
# 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.
|
||||
# Test is QtTest: it brings QAbstractItemModelTester and QSignalSpy, which are how the Qt model
|
||||
# and signal contracts get verified without standing up a GUI.
|
||||
set(_TEST_NEEDED Concurrent Network Svg Widgets Test)
|
||||
set(_TEST_NEEDED Concurrent Network Svg Widgets)
|
||||
endif()
|
||||
|
||||
set(REQUIRED_QT_COMPONENTS ${REQUIRED_QT_COMPONENTS} ${_SERVATRICE_NEEDED} ${_COCKATRICE_NEEDED} ${_ORACLE_NEEDED}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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()
|
||||
|
|
@ -1,146 +0,0 @@
|
|||
# Sends the NSIS installer log to Sentry as an envelope carrying one error
|
||||
# event plus the log as an attachment.
|
||||
#
|
||||
# Generated at configure time from this template by CMake configure_file: the
|
||||
# CPACK_SENTRY_* placeholders are baked in then, so the real values only ever
|
||||
# exist in the build directory and the packaged installer, never in the repo.
|
||||
# Runs on stock Windows PowerShell 5.1 with no extra modules.
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$LogPath
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$EnvelopeUrl = '@CPACK_SENTRY_ENVELOPE_URL@'
|
||||
$SentryKey = '@CPACK_SENTRY_KEY@'
|
||||
$Release = '@CPACK_SENTRY_RELEASE@'
|
||||
$Dsn = '@CPACK_SENTRY_DSN_FULL@'
|
||||
|
||||
try {
|
||||
# The envelope endpoint is TLS-only; stock powershell.exe on older systems
|
||||
# still negotiates TLS 1.0 otherwise, so opt into 1.2 explicitly.
|
||||
try {
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
|
||||
} catch {
|
||||
}
|
||||
|
||||
# --- Log payload, tail-capped at 128 KiB ---------------------------------
|
||||
$MaxLogBytes = 128 * 1024
|
||||
$TruncNotice = '[log truncated to last 128 KB]'
|
||||
if (Test-Path -LiteralPath $LogPath) {
|
||||
$raw = [System.IO.File]::ReadAllBytes($LogPath)
|
||||
} else {
|
||||
$raw = [System.Text.Encoding]::UTF8.GetBytes('(installer log not found: ' + $LogPath + ')')
|
||||
}
|
||||
if ($raw.Length -gt $MaxLogBytes) {
|
||||
$tail = New-Object byte[] $MaxLogBytes
|
||||
[System.Buffer]::BlockCopy($raw, $raw.Length - $MaxLogBytes, $tail, 0, $MaxLogBytes)
|
||||
$prefix = [System.Text.Encoding]::UTF8.GetBytes($TruncNotice + "`r`n")
|
||||
$logBytes = New-Object byte[] ($prefix.Length + $tail.Length)
|
||||
[System.Buffer]::BlockCopy($prefix, 0, $logBytes, 0, $prefix.Length)
|
||||
[System.Buffer]::BlockCopy($tail, 0, $logBytes, $prefix.Length, $tail.Length)
|
||||
} else {
|
||||
$logBytes = $raw
|
||||
}
|
||||
|
||||
# --- Abort reason, read from the log itself --------------------------------
|
||||
# AbortInstall logs "ABORT: ..." first, so the reason travels inside the file
|
||||
# and nothing reason-shaped crosses the command line (no quoting hazards).
|
||||
# Select-String yields the matching line alone, so the reason is already one
|
||||
# physical line for the message; anything else stays in the attached log.
|
||||
# Non-ASCII paths may garble here; the attachment is byte-exact regardless.
|
||||
$Reason = 'unknown (no ABORT line in log)'
|
||||
if (Test-Path -LiteralPath $LogPath) {
|
||||
$abortLine = Select-String -LiteralPath $LogPath -Pattern '^ABORT: ' | Select-Object -Last 1
|
||||
if ($abortLine) {
|
||||
$Reason = ($abortLine.Line -replace '^ABORT:\s*', '')
|
||||
}
|
||||
}
|
||||
|
||||
# --- What was holding the files, read from the log as well -------------------
|
||||
# The guard and the close attempt each log one line naming every holder and what
|
||||
# happened to it. Carrying the last of each as event data keeps an abort caused by
|
||||
# a running instance actionable without opening the attachment. Kept out of the
|
||||
# message so the issue still groups on the reason alone.
|
||||
$Holders = ''
|
||||
$CloseResult = ''
|
||||
$EndResult = ''
|
||||
if (Test-Path -LiteralPath $LogPath) {
|
||||
$holderLine = Select-String -LiteralPath $LogPath -Pattern 'guard: still in use by (.*)$' | Select-Object -Last 1
|
||||
if ($holderLine) {
|
||||
$Holders = $holderLine.Matches[0].Groups[1].Value
|
||||
}
|
||||
$closeLine = Select-String -LiteralPath $LogPath -Pattern '(close-holders: .*)$' | Select-Object -Last 1
|
||||
if ($closeLine) {
|
||||
$CloseResult = $closeLine.Matches[0].Groups[1].Value
|
||||
}
|
||||
$endLine = Select-String -LiteralPath $LogPath -Pattern '(end-holders: .*)$' | Select-Object -Last 1
|
||||
if ($endLine) {
|
||||
$EndResult = $endLine.Matches[0].Groups[1].Value
|
||||
}
|
||||
}
|
||||
|
||||
# --- Event ---------------------------------------------------------------
|
||||
$eventId = [guid]::NewGuid().ToString('N')
|
||||
$now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
|
||||
# $Event is an automatic variable (Microsoft.PowerShell.Utility\Register-ObjectEvent),
|
||||
# so the local is named $sentryEvent to avoid shadowing it.
|
||||
$sentryEvent = [ordered]@{
|
||||
event_id = $eventId
|
||||
timestamp = $now
|
||||
platform = 'other'
|
||||
level = 'error'
|
||||
logger = 'cockatrice-installer'
|
||||
release = $Release
|
||||
environment = 'installer'
|
||||
sdk = @{ name = 'cockatrice.installer'; version = $Release }
|
||||
message = @{ formatted = 'Installer aborted: ' + $Reason }
|
||||
extra = @{
|
||||
reason = $Reason
|
||||
os = [System.Environment]::OSVersion.VersionString
|
||||
holders = $Holders
|
||||
close_result = $CloseResult
|
||||
end_result = $EndResult
|
||||
}
|
||||
}
|
||||
$eventJson = $sentryEvent | ConvertTo-Json -Compress -Depth 6
|
||||
$eventBytes = [System.Text.Encoding]::UTF8.GetBytes($eventJson)
|
||||
|
||||
# --- Envelope, assembled as bytes (the log is opaque binary) -------------
|
||||
$sentAt = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
|
||||
$headerLine = '{"event_id":"' + $eventId + '","sent_at":"' + $sentAt + '","dsn":"' + $Dsn + '"}'
|
||||
$eventHeader = '{"type":"event","length":' + $eventBytes.Length + '}'
|
||||
$attachHeader = '{"type":"attachment","filename":"Cockatrice-installer.log","content_type":"text/plain","length":' + $logBytes.Length + '}'
|
||||
$enc = [System.Text.Encoding]::UTF8
|
||||
$lf = [byte[]]@(10)
|
||||
$body = New-Object System.IO.MemoryStream
|
||||
foreach ($chunk in @($enc.GetBytes($headerLine), $lf, $enc.GetBytes($eventHeader), $lf, $eventBytes, $lf, $enc.GetBytes($attachHeader), $lf, $logBytes)) {
|
||||
$body.Write($chunk, 0, $chunk.Length)
|
||||
}
|
||||
$bodyBytes = $body.ToArray()
|
||||
$body.Close()
|
||||
|
||||
# --- Send ----------------------------------------------------------------
|
||||
# The key travels in the auth header only; stdout below never contains it.
|
||||
$auth = 'Sentry sentry_version=7, sentry_client=cockatrice.installer/' + $Release + ', sentry_key=' + $SentryKey
|
||||
Invoke-RestMethod -Uri $EnvelopeUrl -Method Post -TimeoutSec 45 -ContentType 'application/x-sentry-envelope' -Headers @{ 'X-Sentry-Auth' = $auth } -Body $bodyBytes | Out-Null
|
||||
Write-Output ('SENT ' + $eventId)
|
||||
exit 0
|
||||
} catch {
|
||||
# Scrub the key/DSN out of the failure text in case a transport error echoes them.
|
||||
$msg = $_.Exception.Message
|
||||
if ($msg -and $SentryKey) {
|
||||
$msg = $msg.Replace($SentryKey, '<redacted>')
|
||||
}
|
||||
if ($msg -and $Dsn) {
|
||||
$msg = $msg.Replace($Dsn, '<redacted>')
|
||||
}
|
||||
# Flatten to one line: this text is echoed into the installer log, which is
|
||||
# itself uploaded as an attachment, so a multi-line transport error must not
|
||||
# be able to forge extra lines in the artifact we send.
|
||||
if ($msg) {
|
||||
$msg = ($msg -replace '\s+', ' ').Trim()
|
||||
}
|
||||
Write-Output ('FAILED: ' + $msg)
|
||||
exit 1
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
# Splits DWARF out of the packaged ELF binaries, then strips them.
|
||||
#
|
||||
# This is a CPack pre-build script (CPACK_PRE_BUILD_SCRIPTS, wired in CrashReporting.cmake):
|
||||
# CPack runs it after staging the install tree and before producing the package, so it only
|
||||
# ever touches the copy that becomes the release artifact. The build tree is left alone: -g
|
||||
# stays on, and CI uploads symbols to Sentry straight from build/.
|
||||
#
|
||||
# For every ELF file under CPACK_TEMPORARY_DIRECTORY the script extracts the debug information
|
||||
# to COCKATRICE_DEBUG_SYMBOL_DIR and removes the debug sections from the packaged binary.
|
||||
# COCKATRICE_DEBUG_SYMBOL_DIR is deliberately outside the staging tree, so sidecars never end
|
||||
# up in the package; CI can archive/upload them if wanted (it normally uploads straight from
|
||||
# the build tree). The only thing that must survive stripping is .eh_frame (allocatable, used
|
||||
# for unwinding), which neither --only-keep-debug nor --strip-debug removes.
|
||||
#
|
||||
# Tool paths, the symbol directory and the tree to walk are baked in / provided at configure /
|
||||
# run time; the guards below refuse to run on anything that is not a CPack staging directory,
|
||||
# so a missing variable can never turn the glob into a filesystem-wide walk.
|
||||
|
||||
set(_objcopy "@COCKATRICE_OBJCOPY@")
|
||||
set(_strip "@COCKATRICE_STRIP_TOOL@")
|
||||
set(_symbolDir "@COCKATRICE_DEBUG_SYMBOL_DIR@")
|
||||
|
||||
if(_objcopy STREQUAL "" OR _strip STREQUAL "")
|
||||
message(WARNING "split debug symbols: objcopy/strip not found at configure time, packaging unstripped binaries")
|
||||
return()
|
||||
endif()
|
||||
if(_symbolDir STREQUAL "")
|
||||
message(FATAL_ERROR "split debug symbols: COCKATRICE_DEBUG_SYMBOL_DIR is empty")
|
||||
endif()
|
||||
|
||||
# CPack sets CPACK_TEMPORARY_DIRECTORY to the staging tree; CMAKE_INSTALL_PREFIX is still the
|
||||
# configured /usr prefix here, so globbing it would walk system directories. Refuse to run
|
||||
# unless the path exists and really is a CPack staging tree.
|
||||
if(NOT CPACK_TEMPORARY_DIRECTORY OR NOT IS_DIRECTORY "${CPACK_TEMPORARY_DIRECTORY}")
|
||||
message(STATUS "split debug symbols: no CPack staging directory, skipping")
|
||||
return()
|
||||
endif()
|
||||
if(NOT CPACK_TEMPORARY_DIRECTORY MATCHES "_CPack_Packages")
|
||||
message(WARNING "split debug symbols: ${CPACK_TEMPORARY_DIRECTORY} is not a CPack staging tree, skipping")
|
||||
return()
|
||||
endif()
|
||||
|
||||
file(MAKE_DIRECTORY "${_symbolDir}")
|
||||
|
||||
file(
|
||||
GLOB_RECURSE _entries
|
||||
LIST_DIRECTORIES true
|
||||
"${CPACK_TEMPORARY_DIRECTORY}/*"
|
||||
)
|
||||
set(_processed 0)
|
||||
foreach(_file IN LISTS _entries)
|
||||
if(IS_DIRECTORY "${_file}" OR IS_SYMLINK "${_file}")
|
||||
continue()
|
||||
endif()
|
||||
# Only ELF: objcopy would fail on the icons, translations and Qt resources next to it.
|
||||
file(
|
||||
READ "${_file}" _magic
|
||||
OFFSET 0
|
||||
LIMIT 4
|
||||
HEX
|
||||
)
|
||||
if(NOT _magic STREQUAL "7f454c46")
|
||||
continue()
|
||||
endif()
|
||||
get_filename_component(_name "${_file}" NAME)
|
||||
set(_debug "${_symbolDir}/${_name}.debug")
|
||||
execute_process(COMMAND "${_objcopy}" --only-keep-debug "${_file}" "${_debug}" RESULT_VARIABLE _rc)
|
||||
if(NOT _rc EQUAL 0)
|
||||
message(WARNING "split debug symbols: objcopy --only-keep-debug failed for ${_file}")
|
||||
continue()
|
||||
endif()
|
||||
execute_process(COMMAND "${_strip}" --strip-debug --strip-unneeded "${_file}" RESULT_VARIABLE _rc)
|
||||
if(NOT _rc EQUAL 0)
|
||||
message(WARNING "split debug symbols: strip failed for ${_file}")
|
||||
continue()
|
||||
endif()
|
||||
math(EXPR _processed "${_processed} + 1")
|
||||
endforeach()
|
||||
|
||||
message(STATUS "Split debug symbols for ${_processed} packaged ELF file(s); sidecars in ${_symbolDir}")
|
||||
|
|
@ -3,53 +3,22 @@
|
|||
*
|
||||
* Safe for every target that links Qt Core, including the headless
|
||||
* 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 <QByteArray>
|
||||
#include <QCommandLineParser>
|
||||
#include <QCoreApplication>
|
||||
#include <QCryptographicHash>
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QElapsedTimer>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QHash>
|
||||
#include <QItemSelectionModel>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QList>
|
||||
#include <QLoggingCategory>
|
||||
#include <QMap>
|
||||
#include <QMetaEnum>
|
||||
#include <QMetaObject>
|
||||
#include <QMutex>
|
||||
#include <QMutexLocker>
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QPropertyAnimation>
|
||||
#include <QRandomGenerator>
|
||||
#include <QReadWriteLock>
|
||||
#include <QRegularExpression>
|
||||
#include <QSet>
|
||||
#include <QSettings>
|
||||
#include <QSharedPointer>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QTemporaryDir>
|
||||
#include <QThread>
|
||||
#include <QTimer>
|
||||
#include <QUuid>
|
||||
#include <QVariant>
|
||||
#include <QXmlStreamReader>
|
||||
#include <QXmlStreamWriter>
|
||||
|
|
|
|||
|
|
@ -4,56 +4,27 @@
|
|||
* Includes the Qt Core precompiled header plus the heavy Gui, Widgets and
|
||||
* Network layers that virtually every client translation unit re-parses.
|
||||
* 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 <QAction>
|
||||
#include <QApplication>
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QCompleter>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDockWidget>
|
||||
#include <QFileDialog>
|
||||
#include <QFormLayout>
|
||||
#include <QFrame>
|
||||
#include <QGraphicsItem>
|
||||
#include <QGraphicsScene>
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
#include <QGraphicsView>
|
||||
#include <QGridLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QImage>
|
||||
#include <QImageReader>
|
||||
#include <QInputDialog>
|
||||
#include <QLabel>
|
||||
#include <QLayout>
|
||||
#include <QListWidget>
|
||||
#include <QMainWindow>
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QPainter>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QPushButton>
|
||||
#include <QScrollArea>
|
||||
#include <QSpinBox>
|
||||
#include <QSplitter>
|
||||
#include <QStandardItemModel>
|
||||
#include <QTabWidget>
|
||||
#include <QTableWidget>
|
||||
#include <QTableWidgetItem>
|
||||
#include <QToolBar>
|
||||
#include <QToolButton>
|
||||
#include <QTreeWidget>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ set(cockatrice_SOURCES
|
|||
src/client/network/update/client/client_update_checker.cpp
|
||||
src/client/network/update/client/release_channel.cpp
|
||||
src/client/network/update/card_spoiler/spoiler_background_updater.cpp
|
||||
src/client/crash/crash_reporter.cpp
|
||||
src/client/latency_graph_widget.cpp
|
||||
src/client/latency_status_widget.cpp
|
||||
src/client/lag_monitor.cpp
|
||||
|
|
@ -25,11 +24,12 @@ set(cockatrice_SOURCES
|
|||
src/client/settings/shortcut_treeview.cpp
|
||||
src/client/settings/shortcuts_settings.cpp
|
||||
src/interface/deck_loader/card_node_function.cpp
|
||||
src/interface/deck_loader/deck_file_format.cpp
|
||||
src/interface/deck_loader/deck_loader.cpp
|
||||
src/interface/deck_loader/loaded_deck.cpp
|
||||
src/interface/widgets/dialogs/dlg_connect.cpp
|
||||
src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.cpp
|
||||
src/interface/widgets/dialogs/dlg_create_game.cpp
|
||||
src/interface/widgets/dialogs/dlg_crash_report.cpp
|
||||
src/interface/widgets/dialogs/dlg_default_tags_editor.cpp
|
||||
src/interface/widgets/dialogs/dlg_edit_avatar.cpp
|
||||
src/interface/widgets/dialogs/dlg_edit_password.cpp
|
||||
|
|
@ -45,51 +45,52 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/dialogs/dlg_load_deck_from_website.cpp
|
||||
src/interface/widgets/dialogs/dlg_load_remote_deck.cpp
|
||||
src/interface/widgets/dialogs/dlg_local_game_options.cpp
|
||||
src/interface/widgets/dialogs/dlg_login_prompt.cpp
|
||||
src/interface/widgets/dialogs/dlg_manage_sets.cpp
|
||||
src/interface/widgets/dialogs/dlg_my_reports.cpp
|
||||
src/interface/widgets/dialogs/dlg_register.cpp
|
||||
src/interface/widgets/dialogs/dlg_report_user.cpp
|
||||
src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp
|
||||
src/interface/widgets/dialogs/dlg_share_deck.cpp
|
||||
src/interface/widgets/dialogs/dlg_shared_decks_preview.cpp
|
||||
src/interface/widgets/dialogs/dlg_settings.cpp
|
||||
src/interface/widgets/dialogs/dlg_startup_card_check.cpp
|
||||
src/interface/widgets/dialogs/dlg_tip_of_the_day.cpp
|
||||
src/interface/widgets/dialogs/dlg_update.cpp
|
||||
src/interface/widgets/dialogs/dlg_view_log.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/tip_of_the_day.cpp
|
||||
src/interface/widgets/deck_share/deck_share_utils.cpp
|
||||
src/interface/widgets/deck_share/shared_deck_preview_widget.cpp
|
||||
src/interface/widgets/deck_share/share_bar_widget.cpp
|
||||
src/filters/deck_filter_string.cpp
|
||||
src/filters/filter_builder.cpp
|
||||
src/filters/filter_tree_model.cpp
|
||||
src/filters/syntax_help.cpp
|
||||
src/game/abstract_game.cpp
|
||||
src/game/arrow_registry.cpp
|
||||
src/game_graphics/board/abstract_card_drag_item.cpp
|
||||
src/game_graphics/board/abstract_card_item.cpp
|
||||
src/game_graphics/board/abstract_counter.cpp
|
||||
src/game/board/arrow_data.cpp
|
||||
src/game_graphics/board/arrow_item.cpp
|
||||
src/game_graphics/board/arrow_registry.cpp
|
||||
src/game_graphics/board/arrow_target.cpp
|
||||
src/game_graphics/board/card_drag_item.cpp
|
||||
src/game_graphics/board/card_item.cpp
|
||||
src/game/board/card_list.cpp
|
||||
src/game/board/card_state.cpp
|
||||
src/game_graphics/board/counter_general.cpp
|
||||
src/game/board/counter_state.cpp
|
||||
src/game_graphics/board/translate_counter_name.cpp
|
||||
src/game_graphics/deckview/deck_view.cpp
|
||||
src/game_graphics/deckview/deck_view_container.cpp
|
||||
src/game_graphics/deckview/tabbed_deck_view_container.cpp
|
||||
src/game_graphics/dialogs/dlg_annotation.cpp
|
||||
src/game_graphics/dialogs/dlg_create_token.cpp
|
||||
src/game_graphics/dialogs/dlg_move_top_cards_until.cpp
|
||||
src/game_graphics/dialogs/dlg_roll_dice.cpp
|
||||
src/game/game.cpp
|
||||
src/game/game_event_handler.cpp
|
||||
src/game/game_meta_info.cpp
|
||||
src/game_graphics/game_scene.cpp
|
||||
src/game/game_state.cpp
|
||||
src/game_graphics/game_view.cpp
|
||||
src/game_graphics/hand_counter.cpp
|
||||
src/game_graphics/log/message_log_widget.cpp
|
||||
src/game/phase.cpp
|
||||
src/game_graphics/phases_toolbar.cpp
|
||||
src/game_graphics/player/menu/card_menu.cpp
|
||||
src/game_graphics/player/menu/custom_zone_menu.cpp
|
||||
|
|
@ -107,11 +108,23 @@ set(cockatrice_SOURCES
|
|||
src/game_graphics/tally/stats_tally.cpp
|
||||
src/game_graphics/tally/subtype_tally.cpp
|
||||
src/game_graphics/tally/tally.cpp
|
||||
src/game/player/player_actions.cpp
|
||||
src/game_graphics/player/player_area.cpp
|
||||
src/game_graphics/player/player_dialogs.cpp
|
||||
src/game/player/player_event_handler.cpp
|
||||
src/game_graphics/player/player_graphics_item.cpp
|
||||
src/game/player/player_info.cpp
|
||||
src/game_graphics/player/player_list_widget.cpp
|
||||
src/game/player/player_logic.cpp
|
||||
src/game/player/player_manager.cpp
|
||||
src/game_graphics/player/player_target.cpp
|
||||
src/game/replay.cpp
|
||||
src/game/zones/card_zone_logic.cpp
|
||||
src/game/zones/hand_zone_logic.cpp
|
||||
src/game/zones/pile_zone_logic.cpp
|
||||
src/game/zones/stack_zone_logic.cpp
|
||||
src/game/zones/table_zone_logic.cpp
|
||||
src/game/zones/view_zone_logic.cpp
|
||||
src/game_graphics/zones/card_zone.cpp
|
||||
src/game_graphics/zones/hand_zone.cpp
|
||||
src/game_graphics/zones/pile_zone.cpp
|
||||
|
|
@ -121,12 +134,6 @@ set(cockatrice_SOURCES
|
|||
src/game_graphics/zones/view_zone.cpp
|
||||
src/game_graphics/zones/view_zone_widget.cpp
|
||||
src/game_graphics/board/abstract_graphics_item.cpp
|
||||
src/interface/at_risk_data_warning.cpp
|
||||
src/interface/at_risk_data_warning.h
|
||||
src/interface/card_database_update/card_database_update_status_bar.cpp
|
||||
src/interface/card_database_update/card_database_update_status_bar.h
|
||||
src/interface/card_database_update/card_update_progress.cpp
|
||||
src/interface/card_database_update/card_update_progress.h
|
||||
src/interface/card_picture_loader/card_picture_loader.cpp
|
||||
src/interface/card_picture_loader/card_picture_loader_local.cpp
|
||||
src/interface/card_picture_loader/card_picture_loader_request_status_display_widget.cpp
|
||||
|
|
@ -156,11 +163,9 @@ set(cockatrice_SOURCES
|
|||
src/interface/palette_editor/palette_grid_widget.cpp
|
||||
src/interface/palette_editor/palette_editor_dialog.cpp
|
||||
src/interface/widgets/cards/additional_info/color_identity_widget.cpp
|
||||
src/interface/widgets/cards/additional_info/deck_color_identity.cpp
|
||||
src/interface/widgets/cards/additional_info/mana_cost_widget.cpp
|
||||
src/interface/widgets/cards/additional_info/mana_symbol_widget.cpp
|
||||
src/interface/widgets/cards/art_crop_attribution.cpp
|
||||
src/interface/widgets/cards/card_art_utils.cpp
|
||||
src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp
|
||||
src/interface/widgets/cards/card_group_display_widgets/flat_card_group_display_widget.cpp
|
||||
src/interface/widgets/cards/card_group_display_widgets/overlapped_card_group_display_widget.cpp
|
||||
|
|
@ -311,8 +316,6 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.cpp
|
||||
src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_item_widget.cpp
|
||||
src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp
|
||||
src/interface/widgets/visual_deck_storage/deck_preview/public_deck_preview_widget.cpp
|
||||
src/interface/widgets/visual_deck_storage/remote_public_decks_model.cpp
|
||||
src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.cpp
|
||||
src/interface/widgets/visual_deck_storage/visual_deck_storage_model.cpp
|
||||
src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.cpp
|
||||
|
|
@ -322,7 +325,6 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.cpp
|
||||
src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp
|
||||
src/interface/window_main.cpp
|
||||
src/installer_close_server.cpp
|
||||
src/main.cpp
|
||||
src/single_instance_manager.cpp
|
||||
src/interface/widgets/tabs/abstract_tab_deck_editor.cpp
|
||||
|
|
@ -379,13 +381,11 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/tabs/tab_card_art_rules.cpp
|
||||
src/interface/widgets/tabs/tab_deck_editor.cpp
|
||||
src/interface/widgets/tabs/tab_deck_storage.cpp
|
||||
src/interface/widgets/tabs/tab_developer.cpp
|
||||
src/interface/widgets/tabs/tab_game.cpp
|
||||
src/interface/widgets/tabs/tab_home.cpp
|
||||
src/interface/widgets/tabs/tab_logs.cpp
|
||||
src/interface/widgets/tabs/tab_message.cpp
|
||||
src/interface/widgets/tabs/tab_moderation.cpp
|
||||
src/interface/widgets/tabs/tab_public_decks.cpp
|
||||
src/interface/widgets/tabs/tab_report.cpp
|
||||
src/interface/widgets/tabs/tab_replays.cpp
|
||||
src/interface/widgets/tabs/tab_room.cpp
|
||||
|
|
@ -401,7 +401,6 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp
|
||||
src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp
|
||||
src/interface/widgets/onboarding/banner_shader_config.h
|
||||
src/interface/widgets/onboarding/brand_colors.h
|
||||
src/interface/widgets/onboarding/first_run_wizard.cpp
|
||||
src/interface/widgets/onboarding/first_run_wizard.h
|
||||
src/interface/widgets/onboarding/first_run_wizard_page.cpp
|
||||
|
|
@ -428,7 +427,6 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.h
|
||||
src/interface/widgets/utility/compact_push_button.cpp
|
||||
src/interface/widgets/utility/compact_push_button.h
|
||||
src/installer_close_server.h
|
||||
src/single_instance_manager.h
|
||||
src/client/url_scheme_event_filter.h
|
||||
src/interface/intents/intent_connect_to_server.cpp
|
||||
|
|
@ -443,8 +441,6 @@ set(cockatrice_SOURCES
|
|||
src/interface/intents/intent_login.h
|
||||
src/interface/intents/intent_open_server_room_by_name.cpp
|
||||
src/interface/intents/intent_open_server_room_by_name.h
|
||||
src/interface/intents/intent_open_shared_deck.cpp
|
||||
src/interface/intents/intent_open_shared_deck.h
|
||||
src/interface/intents/url_parser.cpp
|
||||
src/interface/intents/url_parser.h
|
||||
src/interface/widgets/server/user/user_info_popup.cpp
|
||||
|
|
@ -490,7 +486,7 @@ if(APPLE)
|
|||
set(cockatrice_SOURCES ${cockatrice_SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/resources/appicon.icns)
|
||||
endif(APPLE)
|
||||
|
||||
qt_add_resources(cockatrice_RESOURCES_RCC ${cockatrice_RESOURCES})
|
||||
qt6_add_resources(cockatrice_RESOURCES_RCC ${cockatrice_RESOURCES})
|
||||
|
||||
# Declare path variables
|
||||
set(ICONDIR
|
||||
|
|
@ -511,7 +507,7 @@ set(COCKATRICE_MAC_QM_INSTALL_DIR "cockatrice.app/Contents/Resources/translation
|
|||
set(COCKATRICE_UNIX_QM_INSTALL_DIR "share/cockatrice/translations")
|
||||
set(COCKATRICE_WIN32_QM_INSTALL_DIR "translations")
|
||||
|
||||
qt_add_executable(
|
||||
qt6_add_executable(
|
||||
cockatrice
|
||||
WIN32
|
||||
MACOSX_BUNDLE
|
||||
|
|
@ -523,7 +519,7 @@ qt_add_executable(
|
|||
|
||||
target_precompile_headers(cockatrice PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtwidgets_pch.h")
|
||||
|
||||
qt_add_shaders(
|
||||
qt6_add_shaders(
|
||||
cockatrice
|
||||
"onboarding_shaders"
|
||||
PREFIX
|
||||
|
|
@ -532,10 +528,9 @@ qt_add_shaders(
|
|||
"src/interface/widgets/onboarding/shaders"
|
||||
FILES
|
||||
src/interface/widgets/onboarding/shaders/brand_banner.frag
|
||||
src/interface/widgets/onboarding/shaders/brand_plate.frag
|
||||
)
|
||||
|
||||
qt_add_resources(
|
||||
qt6_add_resources(
|
||||
cockatrice
|
||||
"onboarding_qml"
|
||||
PREFIX
|
||||
|
|
@ -551,7 +546,6 @@ target_link_libraries(
|
|||
PUBLIC libcockatrice_card
|
||||
libcockatrice_deck_list
|
||||
libcockatrice_filters
|
||||
libcockatrice_game
|
||||
libcockatrice_utility
|
||||
libcockatrice_network
|
||||
libcockatrice_models
|
||||
|
|
@ -560,8 +554,6 @@ target_link_libraries(
|
|||
${COCKATRICE_QT_MODULES}
|
||||
)
|
||||
|
||||
cockatrice_target_enable_crash_reporting(cockatrice)
|
||||
|
||||
if(UNIX)
|
||||
if(APPLE)
|
||||
set(MACOSX_BUNDLE_INFO_STRING "${PROJECT_NAME}")
|
||||
|
|
@ -660,53 +652,18 @@ if(WIN32)
|
|||
set(qtconf_dest_dir .)
|
||||
|
||||
install(
|
||||
DIRECTORY "$<TARGET_FILE_DIR:cockatrice>/"
|
||||
DIRECTORY "${CMAKE_BINARY_DIR}/${PROJECT_NAME}/${CMAKE_BUILD_TYPE}/"
|
||||
DESTINATION ./
|
||||
FILES_MATCHING
|
||||
PATTERN "*.dll"
|
||||
PATTERN "*.pdb" EXCLUDE
|
||||
PATTERN "*.dir*" EXCLUDE
|
||||
PATTERN "*_autogen*" EXCLUDE
|
||||
PATTERN "*.tlog*" EXCLUDE
|
||||
PATTERN "CMakeFiles*" EXCLUDE
|
||||
PATTERN "x64*" EXCLUDE
|
||||
PATTERN ".qt*" EXCLUDE
|
||||
PATTERN ".qsb*" EXCLUDE
|
||||
PATTERN ".lupdate*" EXCLUDE
|
||||
)
|
||||
|
||||
# fixup_bundle further down copies only the DLLs it can find in Cockatrice.exe's
|
||||
# import table, and the glob above only sees DLLs that were already copied next
|
||||
# to the executable. Qt6::Concurrent is linked but never imported - QtConcurrent::run
|
||||
# and QFutureWatcher are header-only in Qt 6 - so neither step ships
|
||||
# Qt6Concurrent.dll, even though required-runtime.txt lists it and the
|
||||
# Windows package check then fails on a package that is fine to run.
|
||||
# Qt6::ShaderTools is skipped for the same reason.
|
||||
#
|
||||
# Install the runtime of every linked module explicitly. An unused DLL costs a
|
||||
# few hundred kilobytes; a missing one is what stops the client starting after
|
||||
# an update, and it stays invisible until then.
|
||||
foreach(_qt_module IN LISTS COCKATRICE_QT_MODULES)
|
||||
get_target_property(_qt_module_type ${_qt_module} TYPE)
|
||||
if(_qt_module_type STREQUAL "SHARED_LIBRARY")
|
||||
install(FILES "$<TARGET_FILE:${_qt_module}>" DESTINATION ./)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
install(
|
||||
DIRECTORY "${CMAKE_BINARY_DIR}/cockatrice/"
|
||||
DESTINATION ./
|
||||
FILES_MATCHING
|
||||
PATTERN "CMakeFiles" EXCLUDE
|
||||
PATTERN "*.ini"
|
||||
PATTERN "CMakeFiles*" EXCLUDE
|
||||
PATTERN "*.dir*" EXCLUDE
|
||||
PATTERN "*_autogen*" EXCLUDE
|
||||
PATTERN "*.tlog*" EXCLUDE
|
||||
PATTERN "*.pdb" EXCLUDE
|
||||
PATTERN "x64*" EXCLUDE
|
||||
PATTERN ".qt*" EXCLUDE
|
||||
PATTERN ".qsb*" EXCLUDE
|
||||
PATTERN ".lupdate*" EXCLUDE
|
||||
)
|
||||
|
||||
# Qt plugins: audio, iconengines, imageformats, multimedia, platforms, printsupport, styles, tls
|
||||
|
|
@ -739,15 +696,11 @@ if(WIN32)
|
|||
PATTERN "tls/qschannelbackend.dll"
|
||||
)
|
||||
|
||||
# The Windows install has no Resources directory: the .qm files go to
|
||||
# ${COCKATRICE_WIN32_QM_INSTALL_DIR} and main.cpp looks for them at
|
||||
# applicationDirPath() + "/translations". The macOS bundle above keeps
|
||||
# Resources/translations, which is correct for it.
|
||||
install(
|
||||
CODE "
|
||||
file(WRITE \"\${CMAKE_INSTALL_PREFIX}/${qtconf_dest_dir}/qt.conf\" \"[Paths]
|
||||
Plugins = Plugins
|
||||
Translations = translations
|
||||
Translations = Resources/translations
|
||||
Data = Resources\")
|
||||
"
|
||||
COMPONENT Runtime
|
||||
|
|
@ -763,12 +716,16 @@ Data = Resources\")
|
|||
"
|
||||
COMPONENT Runtime
|
||||
)
|
||||
|
||||
if(OPENSSL_FOUND)
|
||||
install(FILES ${OPENSSL_INCLUDE_DIRS} DESTINATION ./)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(Qt6LinguistTools_FOUND)
|
||||
#Qt6 Translations happen after the executable is built up
|
||||
if(UPDATE_TRANSLATIONS)
|
||||
qt_add_translations(
|
||||
qt6_add_translations(
|
||||
cockatrice
|
||||
TS_FILES
|
||||
${cockatrice_TS}
|
||||
|
|
@ -778,7 +735,7 @@ if(Qt6LinguistTools_FOUND)
|
|||
cockatrice_QM
|
||||
)
|
||||
else()
|
||||
qt_add_translations(cockatrice TS_FILES ${cockatrice_TS} QM_FILES_OUTPUT_VARIABLE cockatrice_QM)
|
||||
qt6_add_translations(cockatrice TS_FILES ${cockatrice_TS} QM_FILES_OUTPUT_VARIABLE cockatrice_QM)
|
||||
endif()
|
||||
|
||||
if(UNIX)
|
||||
|
|
@ -790,18 +747,6 @@ if(Qt6LinguistTools_FOUND)
|
|||
elseif(WIN32)
|
||||
install(FILES ${cockatrice_QM} DESTINATION ${COCKATRICE_WIN32_QM_INSTALL_DIR})
|
||||
endif()
|
||||
|
||||
# Qt attaches the .qm files to the default build target only, so building the executable on its own
|
||||
# (CLion, --target cockatrice) leaves it without any translations: nothing is translated and the
|
||||
# language selector stays empty.
|
||||
# Depend on Qt's own lrelease target instead of attaching the .qm custom commands to a second
|
||||
# target: two independent targets owning the same outputs race on MSBuild/Makefiles and are an
|
||||
# error on Xcode. Older Qt versions already add this dependency themselves.
|
||||
foreach(_lrelease_target Cockatrice_lrelease cockatrice_lrelease)
|
||||
if(TARGET ${_lrelease_target})
|
||||
add_dependencies(cockatrice ${_lrelease_target})
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
qt_finalize_target(cockatrice)
|
||||
qt6_finalize_target(cockatrice)
|
||||
|
|
|
|||
|
|
@ -6,4 +6,5 @@ Name=Cockatrice
|
|||
Exec=cockatrice %U
|
||||
Icon=cockatrice
|
||||
Categories=Game;CardGame;
|
||||
MimeType=application/x-cockatrice;x-scheme-handler/cockatrice;
|
||||
MimeType=application/x-cockatrice;
|
||||
X-Scheme-Handler/cockatrice=true
|
||||
|
|
|
|||
|
|
@ -62,9 +62,7 @@
|
|||
<file>resources/icons/mana/U.svg</file>
|
||||
<file>resources/icons/mana/W.svg</file>
|
||||
|
||||
<file>resources/backgrounds/home.jpg</file>
|
||||
<file>resources/backgrounds/home-dark.jpg</file>
|
||||
<file>resources/backgrounds/home-light.jpg</file>
|
||||
<file>resources/backgrounds/home.png</file>
|
||||
<file>resources/backgrounds/card_triplet.svg</file>
|
||||
<file>resources/backgrounds/placeholder_printing_selector.svg</file>
|
||||
|
||||
|
|
@ -365,8 +363,6 @@
|
|||
|
||||
<file>resources/usericons/pawn_single.svg</file>
|
||||
<file>resources/usericons/pawn_double.svg</file>
|
||||
<file>resources/usericons/pawn_dev_single.svg</file>
|
||||
<file>resources/usericons/pawn_dev_double.svg</file>
|
||||
<file>resources/usericons/pawn_donator_single.svg</file>
|
||||
<file>resources/usericons/pawn_donator_double.svg</file>
|
||||
<file>resources/usericons/pawn_judge_single.svg</file>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Binary file not shown.
|
Before Width: | Height: | Size: 2.7 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.9 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.9 MiB |
BIN
cockatrice/resources/backgrounds/home.png
Normal file
BIN
cockatrice/resources/backgrounds/home.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 MiB |
|
|
@ -1,343 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="100"
|
||||
height="100"
|
||||
id="svg5322"
|
||||
version="1.1"
|
||||
inkscape:version="1.4.2 (ebf0e940, 2025-05-08)"
|
||||
sodipodi:docname="pawn_dev_double.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<defs
|
||||
id="defs3">
|
||||
<inkscape:perspective
|
||||
sodipodi:type="inkscape:persp3d"
|
||||
inkscape:vp_x="0 : 526.18109 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_z="744.09448 : 526.18109 : 1"
|
||||
inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
|
||||
id="perspective5328"/>
|
||||
<inkscape:perspective
|
||||
id="perspective5305"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d"/>
|
||||
<linearGradient
|
||||
id="linearGradient5181">
|
||||
<stop
|
||||
style="stop-color:#0fbb00;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop5183"/>
|
||||
<stop
|
||||
style="stop-color:#064400;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop5185"/>
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3600-2"
|
||||
id="radialGradient3606-7"
|
||||
cx="324.32715"
|
||||
cy="131.40274"
|
||||
fx="324.32715"
|
||||
fy="131.40274"
|
||||
r="25.501276"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"/>
|
||||
<linearGradient
|
||||
id="linearGradient3600-2">
|
||||
<stop
|
||||
style="stop-color:#ffc33d;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3602-4"/>
|
||||
<stop
|
||||
style="stop-color:#e09900;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop3604-9"/>
|
||||
</linearGradient>
|
||||
<inkscape:perspective
|
||||
id="perspective5478"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d"/>
|
||||
<linearGradient
|
||||
id="linearGradient5189">
|
||||
<stop
|
||||
style="stop-color:#000ec9;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop5191"/>
|
||||
<stop
|
||||
style="stop-color:#000657;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop5193"/>
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3600-4"
|
||||
id="radialGradient3606-1"
|
||||
cx="324.32715"
|
||||
cy="131.40274"
|
||||
fx="324.32715"
|
||||
fy="131.40274"
|
||||
r="25.501276"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"/>
|
||||
<linearGradient
|
||||
id="linearGradient3600-4">
|
||||
<stop
|
||||
style="stop-color:#ffc33d;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3602-3"/>
|
||||
<stop
|
||||
style="stop-color:#e09900;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop3604-5"/>
|
||||
</linearGradient>
|
||||
<inkscape:perspective
|
||||
id="perspective5559"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d"/>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient5173"
|
||||
id="linearGradient5179"
|
||||
x1="167.33386"
|
||||
y1="178.83276"
|
||||
x2="244.78181"
|
||||
y2="178.83276"
|
||||
gradientUnits="userSpaceOnUse"/>
|
||||
<linearGradient
|
||||
id="linearGradient5173">
|
||||
<stop
|
||||
style="stop-color:#f50000;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop5175"/>
|
||||
<stop
|
||||
style="stop-color:#950000;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop5177"/>
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3600"
|
||||
id="radialGradient5169"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"
|
||||
cx="324.32715"
|
||||
cy="131.40274"
|
||||
fx="324.32715"
|
||||
fy="131.40274"
|
||||
r="25.501276"/>
|
||||
<linearGradient
|
||||
id="linearGradient3600">
|
||||
<stop
|
||||
style="stop-color:#ffc13d;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3602"/>
|
||||
<stop
|
||||
style="stop-color:#e09900;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop3604"/>
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
r="25.501276"
|
||||
fy="131.40274"
|
||||
fx="324.32715"
|
||||
cy="131.40274"
|
||||
cx="324.32715"
|
||||
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="radialGradient5574"
|
||||
xlink:href="#linearGradient3600"
|
||||
inkscape:collect="always"/>
|
||||
<inkscape:perspective
|
||||
id="perspective5663"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d"/>
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3600-7"
|
||||
id="radialGradient3606-8"
|
||||
cx="324.32715"
|
||||
cy="131.40274"
|
||||
fx="324.32715"
|
||||
fy="131.40274"
|
||||
r="25.501276"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"/>
|
||||
<linearGradient
|
||||
id="linearGradient3600-7">
|
||||
<stop
|
||||
style="stop-color:#ffc13d;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3602-7"/>
|
||||
<stop
|
||||
style="stop-color:#e09900;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop3604-6"/>
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3600-7"
|
||||
id="radialGradient5254"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"
|
||||
cx="324.32715"
|
||||
cy="131.40274"
|
||||
fx="324.32715"
|
||||
fy="131.40274"
|
||||
r="25.501276"/>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient5189-1"
|
||||
id="linearGradient5394"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="385.03503"
|
||||
y1="180.09546"
|
||||
x2="462.48297"
|
||||
y2="180.09546"
|
||||
gradientTransform="matrix(0.96839241,0,0,0.96839241,-360.365,847.52359)"/>
|
||||
<linearGradient
|
||||
id="linearGradient5189-1">
|
||||
<stop
|
||||
style="stop-color:#000ec9;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop5191-0"/>
|
||||
<stop
|
||||
style="stop-color:#000657;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop5193-4"/>
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient5173-1"
|
||||
id="linearGradient5581"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="167.33386"
|
||||
y1="178.83276"
|
||||
x2="244.78181"
|
||||
y2="178.83276"
|
||||
gradientTransform="matrix(0.96839241,0,0,0.96839241,-149.54484,848.74636)"/>
|
||||
<linearGradient
|
||||
id="linearGradient5173-1">
|
||||
<stop
|
||||
style="stop-color:#f50000;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop5175-5"/>
|
||||
<stop
|
||||
style="stop-color:#950000;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop5177-3"/>
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient5181-9"
|
||||
id="linearGradient5782"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="282.50455"
|
||||
y1="181.61069"
|
||||
x2="359.95248"
|
||||
y2="181.61069"
|
||||
gradientTransform="matrix(0.96839241,0,0,0.96839241,-261.07526,846.05625)"/>
|
||||
<linearGradient
|
||||
id="linearGradient5181-9">
|
||||
<stop
|
||||
style="stop-color:#80d600;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop5183-3"/>
|
||||
<stop
|
||||
style="stop-color:#80d600;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop5185-0"/>
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
y2="181.61069"
|
||||
x2="359.95248"
|
||||
y1="181.61069"
|
||||
x1="282.50455"
|
||||
gradientTransform="matrix(0.96839241,0,0,0.96839241,-175.71812,893.2775)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="linearGradient5799"
|
||||
xlink:href="#linearGradient5181-9"
|
||||
inkscape:collect="always"/>
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
inkscape:document-units="mm"
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="6.0735294"
|
||||
inkscape:cx="53.757869"
|
||||
inkscape:cy="53.840194"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1696"
|
||||
inkscape:window-height="1051"
|
||||
inkscape:window-x="98"
|
||||
inkscape:window-y="1118"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showguides="true">
|
||||
<sodipodi:guide
|
||||
position="49.829627,61.114263"
|
||||
orientation="1,0"
|
||||
id="guide1"
|
||||
inkscape:locked="false"/>
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata4">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||
<dc:title/>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-952.36218)">
|
||||
<path
|
||||
style="fill-opacity:1;stroke:black;stroke-width:2.78220296;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
|
||||
d="M 49.84375 1.71875 C 36.719738 1.71875 26.0625 12.375988 26.0625 25.5 C 26.0625 32.977454 29.538325 39.612734 34.9375 43.96875 C 24.439951 49.943698 17.919149 62.196126 14.3125 75.65625 C 9.0380874 95.34065 30.224013 98.21875 49.84375 98.21875 C 69.463486 98.21875 90.549327 94.96715 85.375 75.65625 C 81.693381 61.916246 75.224585 49.827177 64.8125 43.9375 C 70.181573 39.580662 73.59375 32.953205 73.59375 25.5 C 73.59375 12.375988 62.967762 1.71875 49.84375 1.71875 z "
|
||||
transform="translate(0,952.36218)"
|
||||
id="left"/>
|
||||
<path
|
||||
style="opacity:1;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.73577702;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 51.28696,1001.834 0,-46.98372 1.434151,0.16768 c 5.155008,0.60274 9.462857,2.72154 12.938257,6.36366 4.74393,4.9715 6.87913,11.35611 6.16464,18.43328 -0.53702,5.31935 -3.09008,10.59498 -6.83833,14.13074 l -1.94072,1.83069 3.04083,2.20427 c 3.58084,2.5957 7.18975,6.4912 9.55296,10.3116 4.89572,7.9144 9.23593,21.4918 8.50487,26.6055 -0.81312,5.6877 -5.43872,9.6977 -13.62216,11.8093 -3.80822,0.9826 -7.68056,1.4713 -14.763321,1.8633 l -4.471177,0.2474 0,-46.9837 z"
|
||||
id="right"
|
||||
inkscape:connector-curvature="0"/>
|
||||
<path
|
||||
d="m 478.409,116.617 c -0.368,-4.271 -3.181,-7.94 -7.2,-9.403 -4.029,-1.472 -8.539,-0.47 -11.57,2.556 l -62.015,62.011 -68.749,-21.768 -21.768,-68.748 62.016,-62.016 c 3.035,-3.032 4.025,-7.543 2.563,-11.565 -1.477,-4.03 -5.137,-6.837 -9.417,-7.207 -37.663,-3.245 -74.566,10.202 -101.247,36.887 -36.542,36.545 -46.219,89.911 -29.083,135.399 -1.873,1.578 -3.721,3.25 -5.544,5.053 L 19.386,373.152 c -0.073,0.071 -0.145,0.149 -0.224,0.219 -24.345,24.346 -24.345,63.959 0,88.309 24.349,24.344 63.672,24.048 88.013,-0.298 0.105,-0.098 0.201,-0.196 0.297,-0.305 L 301.104,252.456 c 1.765,-1.773 3.404,-3.628 4.949,-5.532 45.5,17.167 98.9,7.513 135.474,-29.056 26.675,-26.687 40.131,-63.593 36.882,-101.251 z M 75.98,435.38 c -8.971,8.969 -23.5,8.963 -32.47,0 -8.967,-8.961 -8.967,-23.502 0,-32.466 8.97,-8.963 23.499,-8.963 32.47,0 8.967,8.964 8.967,23.505 0,32.466 z"
|
||||
id="path1"
|
||||
style="display:inline;fill:#A1A1A1;stroke-width:9.87059588;stroke-dasharray:none;fill-opacity:1;stroke:#000000;stroke-opacity:1"
|
||||
transform="matrix(0.19145387,0,0,0.19145387,4.0816072,956.99677)"
|
||||
inkscape:label="wrench" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 12 KiB |
|
|
@ -1,211 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="100"
|
||||
height="100"
|
||||
id="svg5322"
|
||||
version="1.1"
|
||||
inkscape:version="1.4.2 (ebf0e940, 2025-05-08)"
|
||||
sodipodi:docname="pawn_dev_single.svg"
|
||||
xml:space="preserve"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"><defs
|
||||
id="defs3"><inkscape:perspective
|
||||
sodipodi:type="inkscape:persp3d"
|
||||
inkscape:vp_x="0 : 526.18109 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_z="744.09448 : 526.18109 : 1"
|
||||
inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
|
||||
id="perspective5328" /><inkscape:perspective
|
||||
id="perspective5305"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" /><linearGradient
|
||||
id="linearGradient5181"><stop
|
||||
style="stop-color:#0fbb00;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop5183" /><stop
|
||||
style="stop-color:#064400;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop5185" /></linearGradient><radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3600-2"
|
||||
id="radialGradient3606-7"
|
||||
cx="324.32715"
|
||||
cy="131.40274"
|
||||
fx="324.32715"
|
||||
fy="131.40274"
|
||||
r="25.501276"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)" /><linearGradient
|
||||
id="linearGradient3600-2"><stop
|
||||
style="stop-color:#ffc33d;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3602-4" /><stop
|
||||
style="stop-color:#e09900;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop3604-9" /></linearGradient><inkscape:perspective
|
||||
id="perspective5478"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" /><linearGradient
|
||||
id="linearGradient5189"><stop
|
||||
style="stop-color:#000ec9;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop5191" /><stop
|
||||
style="stop-color:#000657;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop5193" /></linearGradient><radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3600-4"
|
||||
id="radialGradient3606-1"
|
||||
cx="324.32715"
|
||||
cy="131.40274"
|
||||
fx="324.32715"
|
||||
fy="131.40274"
|
||||
r="25.501276"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)" /><linearGradient
|
||||
id="linearGradient3600-4"><stop
|
||||
style="stop-color:#ffc33d;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3602-3" /><stop
|
||||
style="stop-color:#e09900;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop3604-5" /></linearGradient><inkscape:perspective
|
||||
id="perspective5559"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" /><linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient5173"
|
||||
id="linearGradient5179"
|
||||
x1="167.33386"
|
||||
y1="178.83276"
|
||||
x2="244.78181"
|
||||
y2="178.83276"
|
||||
gradientUnits="userSpaceOnUse" /><linearGradient
|
||||
id="linearGradient5173"><stop
|
||||
style="stop-color:#f50000;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop5175" /><stop
|
||||
style="stop-color:#950000;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop5177" /></linearGradient><radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3600"
|
||||
id="radialGradient5169"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"
|
||||
cx="324.32715"
|
||||
cy="131.40274"
|
||||
fx="324.32715"
|
||||
fy="131.40274"
|
||||
r="25.501276" /><linearGradient
|
||||
id="linearGradient3600"><stop
|
||||
style="stop-color:#ffc13d;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3602" /><stop
|
||||
style="stop-color:#e09900;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop3604" /></linearGradient><radialGradient
|
||||
r="25.501276"
|
||||
fy="131.40274"
|
||||
fx="324.32715"
|
||||
cy="131.40274"
|
||||
cx="324.32715"
|
||||
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="radialGradient5574"
|
||||
xlink:href="#linearGradient3600"
|
||||
inkscape:collect="always" /><inkscape:perspective
|
||||
id="perspective5663"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" /><radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3600-7"
|
||||
id="radialGradient3606-8"
|
||||
cx="324.32715"
|
||||
cy="131.40274"
|
||||
fx="324.32715"
|
||||
fy="131.40274"
|
||||
r="25.501276"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)" /><linearGradient
|
||||
id="linearGradient3600-7"><stop
|
||||
style="stop-color:#ffc13d;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3602-7" /><stop
|
||||
style="stop-color:#e09900;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop3604-6" /></linearGradient><radialGradient
|
||||
r="25.501276"
|
||||
fy="131.40274"
|
||||
fx="324.32715"
|
||||
cy="131.40274"
|
||||
cx="324.32715"
|
||||
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="radialGradient5676"
|
||||
xlink:href="#linearGradient3600-7"
|
||||
inkscape:collect="always" /></defs><sodipodi:namedview
|
||||
inkscape:document-units="mm"
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="3.1582091"
|
||||
inkscape:cx="104.64791"
|
||||
inkscape:cy="56.04442"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1027"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showguides="true"><sodipodi:guide
|
||||
position="50.002551,111.99556"
|
||||
orientation="1,0"
|
||||
id="guide3"
|
||||
inkscape:locked="false" /></sodipodi:namedview><metadata
|
||||
id="metadata4"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-952.36218)"
|
||||
style="display:inline">
|
||||
<path
|
||||
style="display:inline;opacity:1;fill-opacity:1;stroke:#000000;stroke-width:2.7822;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 49.84375,1.71875 C 36.719738,1.71875 26.0625,12.375988 26.0625,25.5 c 0,7.477454 3.475825,14.112734 8.875,18.46875 -10.497549,5.974948 -17.018351,18.227376 -20.625,31.6875 -5.2744126,19.6844 15.911513,22.5625 35.53125,22.5625 19.619736,0 40.705577,-3.2516 35.53125,-22.5625 C 81.693381,61.916246 75.224585,49.827177 64.8125,43.9375 70.181573,39.580662 73.59375,32.953205 73.59375,25.5 c 0,-13.124012 -10.625988,-23.78125 -23.75,-23.78125 z"
|
||||
id="left"
|
||||
sodipodi:insensitive="true"
|
||||
transform="translate(0,952.36218)" />
|
||||
<path
|
||||
d="m 478.409,116.617 c -0.368,-4.271 -3.181,-7.94 -7.2,-9.403 -4.029,-1.472 -8.539,-0.47 -11.57,2.556 l -62.015,62.011 -68.749,-21.768 -21.768,-68.748 62.016,-62.016 c 3.035,-3.032 4.025,-7.543 2.563,-11.565 -1.477,-4.03 -5.137,-6.837 -9.417,-7.207 -37.663,-3.245 -74.566,10.202 -101.247,36.887 -36.542,36.545 -46.219,89.911 -29.083,135.399 -1.873,1.578 -3.721,3.25 -5.544,5.053 L 19.386,373.152 c -0.073,0.071 -0.145,0.149 -0.224,0.219 -24.345,24.346 -24.345,63.959 0,88.309 24.349,24.344 63.672,24.048 88.013,-0.298 0.105,-0.098 0.201,-0.196 0.297,-0.305 L 301.104,252.456 c 1.765,-1.773 3.404,-3.628 4.949,-5.532 45.5,17.167 98.9,7.513 135.474,-29.056 26.675,-26.687 40.131,-63.593 36.882,-101.251 z M 75.98,435.38 c -8.971,8.969 -23.5,8.963 -32.47,0 -8.967,-8.961 -8.967,-23.502 0,-32.466 8.97,-8.963 23.499,-8.963 32.47,0 8.967,8.964 8.967,23.505 0,32.466 z"
|
||||
id="path1"
|
||||
style="display:inline;fill:#A1A1A1;stroke-width:9.87059588;stroke-dasharray:none;fill-opacity:1;stroke:#000000;stroke-opacity:1"
|
||||
transform="matrix(0.19145387,0,0,0.19145387,4.0816072,956.99677)"
|
||||
inkscape:label="wrench" /></g></svg>
|
||||
|
Before Width: | Height: | Size: 9.1 KiB |
|
|
@ -1,616 +0,0 @@
|
|||
#include "crash_reporter.h"
|
||||
|
||||
#if COCKATRICE_WITH_CRASH_REPORTING
|
||||
#include <sentry.h>
|
||||
#endif
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QLockFile>
|
||||
#include <QLoggingCategory>
|
||||
#include <QRegularExpression>
|
||||
#include <QThread>
|
||||
#include <QUrl>
|
||||
#include <cstring>
|
||||
#include <libcockatrice/settings/personal_settings.h>
|
||||
#include <string>
|
||||
|
||||
Q_LOGGING_CATEGORY(CrashReportLog, "crash_report");
|
||||
|
||||
#if COCKATRICE_WITH_CRASH_REPORTING
|
||||
namespace
|
||||
{
|
||||
|
||||
constexpr char CockatriceUrlScheme[] = "cockatrice://";
|
||||
|
||||
/**
|
||||
* @brief Qt-free cockatrice:// secret scrubber for the before_send hook.
|
||||
*
|
||||
* before_send does not run in async-signal context on our backends (breakpad invokes it on the
|
||||
* crashing thread outside the signal handler, and crashpad/native crashes bypass the hook
|
||||
* entirely), but it still avoids QString/QRegularExpression so it stays safe to call from the
|
||||
* crash-handler thread. Truncates each URL at the first terminator, keeping scheme://host like
|
||||
* redactCockatriceUrls does. (std::string allocates, so this is not strictly signal-safe.)
|
||||
*/
|
||||
std::string scrubCockatriceUrls(const char *input)
|
||||
{
|
||||
std::string out;
|
||||
const char *pos = input;
|
||||
const char *scheme = CockatriceUrlScheme;
|
||||
const size_t schemeLen = std::strlen(scheme);
|
||||
while (*pos != '\0') {
|
||||
const char *found = std::strstr(pos, scheme);
|
||||
if (found == nullptr) {
|
||||
out.append(pos);
|
||||
break;
|
||||
}
|
||||
out.append(pos, found);
|
||||
const char *hostEnd = found + schemeLen;
|
||||
while (*hostEnd != '\0' && *hostEnd != '/' && *hostEnd != '?' && *hostEnd != '#' && *hostEnd != ' ' &&
|
||||
*hostEnd != '\t' && *hostEnd != '\n' && *hostEnd != '\'' && *hostEnd != '"' && *hostEnd != '<' &&
|
||||
*hostEnd != '>') {
|
||||
++hostEnd;
|
||||
}
|
||||
out.append(found, hostEnd);
|
||||
pos = hostEnd;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
void scrubStringValue(sentry_value_t holder, const char *key)
|
||||
{
|
||||
sentry_value_t current = sentry_value_get_by_key(holder, key);
|
||||
if (sentry_value_get_type(current) != SENTRY_VALUE_TYPE_STRING) {
|
||||
return;
|
||||
}
|
||||
const char *raw = sentry_value_as_string(current);
|
||||
if (raw == nullptr || std::strstr(raw, CockatriceUrlScheme) == nullptr) {
|
||||
return;
|
||||
}
|
||||
sentry_value_set_by_key(holder, key, sentry_value_new_string(scrubCockatriceUrls(raw).c_str()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Scrub trivially reachable string values inside a breadcrumb data object.
|
||||
*/
|
||||
int scrubDataEntry(const char *key, sentry_value_t value, void *userdata)
|
||||
{
|
||||
if (sentry_value_get_type(value) != SENTRY_VALUE_TYPE_STRING) {
|
||||
return 0;
|
||||
}
|
||||
const char *raw = sentry_value_as_string(value);
|
||||
if (raw == nullptr || std::strstr(raw, CockatriceUrlScheme) == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
auto *holder = static_cast<sentry_value_t *>(userdata);
|
||||
sentry_value_set_by_key(*holder, key, sentry_value_new_string(scrubCockatriceUrls(raw).c_str()));
|
||||
return 0;
|
||||
}
|
||||
|
||||
void scrubDataObject(sentry_value_t data)
|
||||
{
|
||||
if (sentry_value_get_type(data) != SENTRY_VALUE_TYPE_OBJECT) {
|
||||
return;
|
||||
}
|
||||
sentry_value_foreach_key_value(data, scrubDataEntry, &data);
|
||||
}
|
||||
|
||||
void scrubCrumbList(sentry_value_t crumbs)
|
||||
{
|
||||
const size_t count = sentry_value_get_length(crumbs);
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
sentry_value_t crumb = sentry_value_get_by_index(crumbs, i);
|
||||
if (sentry_value_get_type(crumb) != SENTRY_VALUE_TYPE_OBJECT) {
|
||||
continue;
|
||||
}
|
||||
scrubStringValue(crumb, "message");
|
||||
scrubDataObject(sentry_value_get_by_key(crumb, "data"));
|
||||
}
|
||||
}
|
||||
|
||||
void scrubBreadcrumbs(sentry_value_t event)
|
||||
{
|
||||
// The SDK merges scope breadcrumbs onto the event as a plain list (sentry__value_merge_breadcrumbs
|
||||
// in sentry_scope.c); the { "values": [...] } envelope shape only exists on the wire. Accept both
|
||||
// so the scrubbing runs on the actual merged form.
|
||||
sentry_value_t breadcrumbs = sentry_value_get_by_key(event, "breadcrumbs");
|
||||
if (sentry_value_get_type(breadcrumbs) == SENTRY_VALUE_TYPE_LIST) {
|
||||
scrubCrumbList(breadcrumbs);
|
||||
return;
|
||||
}
|
||||
if (sentry_value_get_type(breadcrumbs) == SENTRY_VALUE_TYPE_OBJECT) {
|
||||
sentry_value_t values = sentry_value_get_by_key(breadcrumbs, "values");
|
||||
if (sentry_value_get_type(values) == SENTRY_VALUE_TYPE_LIST) {
|
||||
scrubCrumbList(values);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief before_send hook: scrub cockatrice:// secrets from the event.
|
||||
*
|
||||
* Defense in depth — payloads we build are pre-redacted. No breadcrumbs are recorded today (the Qt
|
||||
* integration stays off), so this currently covers the top-level message and logentry.formatted,
|
||||
* plus any breadcrumb messages/data that reach the merged event in the future. Native crash
|
||||
* minidumps bypass this hook regardless of backend: breakpad captures them in-process without
|
||||
* invoking before_send, and fast-fail crashes on Windows skip the local handler, so before_send is
|
||||
* never invoked for those.
|
||||
*/
|
||||
sentry_value_t scrubEventBeforeSend(sentry_value_t event, sentry_hint_t *hint, void *userData)
|
||||
{
|
||||
Q_UNUSED(hint);
|
||||
Q_UNUSED(userData);
|
||||
scrubStringValue(event, "message");
|
||||
sentry_value_t logentry = sentry_value_get_by_key(event, "logentry");
|
||||
if (sentry_value_get_type(logentry) == SENTRY_VALUE_TYPE_OBJECT) {
|
||||
scrubStringValue(logentry, "formatted");
|
||||
}
|
||||
scrubBreadcrumbs(event);
|
||||
return event;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief DSN matcher shared by isDsnWellFormed() and describeDsn().
|
||||
*
|
||||
* Kept unanchored on purpose, mirroring the MATCHES in cmake/CrashReporting.cmake (CMake searches
|
||||
* rather than anchoring), so the client never rejects a DSN the build itself baked in. Compiled once
|
||||
* because QRegularExpression construction is not free and both helpers are called per pending
|
||||
* envelope.
|
||||
*/
|
||||
const QRegularExpression &dsnPattern()
|
||||
{
|
||||
static const QRegularExpression pattern(QStringLiteral("(https?)://([^@]+)@([^/]+)/([0-9]+)"));
|
||||
return pattern;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool CrashReporter::isDsnWellFormed(const QString &dsn)
|
||||
{
|
||||
return dsnPattern().match(dsn).hasMatch();
|
||||
}
|
||||
|
||||
QString CrashReporter::describeDsn(const QString &dsn)
|
||||
{
|
||||
const QRegularExpressionMatch match = dsnPattern().match(dsn);
|
||||
if (!match.hasMatch()) {
|
||||
return QString();
|
||||
}
|
||||
// Host and project id only: the key in group 2 must never reach a log line.
|
||||
return QStringLiteral("%1 (project %2)").arg(match.captured(3), match.captured(4));
|
||||
}
|
||||
|
||||
QString CrashReporter::describeOriginMismatch(const QString &envelopePath, const QString ¤tDsn)
|
||||
{
|
||||
const QString destination = describeDsn(currentDsn);
|
||||
if (destination.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
QFile file(envelopePath);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
return QString();
|
||||
}
|
||||
// The envelope header is the first line and is small; the cap only keeps a corrupt file from
|
||||
// being read in full.
|
||||
const QByteArray headerLine = file.readLine(4096).trimmed();
|
||||
if (headerLine.isEmpty() || !headerLine.startsWith('{')) {
|
||||
return QString();
|
||||
}
|
||||
const QString originDsn = QJsonDocument::fromJson(headerLine).object().value(QStringLiteral("dsn")).toString();
|
||||
const QString origin = describeDsn(originDsn);
|
||||
// Equal destinations are not a mismatch: a different key for the same host and project still
|
||||
// uploads where the crash was captured.
|
||||
if (origin.isEmpty() || origin == destination) {
|
||||
return QString();
|
||||
}
|
||||
return QStringLiteral("was captured for %1 but this session uploads with %2").arg(origin, destination);
|
||||
}
|
||||
|
||||
bool CrashReporter::isReportingEnabled(const QString &dsn, PersonalSettings::CrashReportingMode consentMode)
|
||||
{
|
||||
return !dsn.isEmpty() && consentMode != PersonalSettings::CrashReportingMode::NeverSend;
|
||||
}
|
||||
|
||||
void CrashReporter::init(const QString &dsn,
|
||||
const QString &release,
|
||||
const QString &databasePath,
|
||||
PersonalSettings::CrashReportingMode consentMode)
|
||||
{
|
||||
shutdown();
|
||||
sentryDatabasePath = databasePath;
|
||||
if (!isReportingEnabled(dsn, consentMode)) {
|
||||
if (!dsn.isEmpty()) {
|
||||
// A deliberate choice rather than a misconfiguration, so it is not a warning. The
|
||||
// no-DSN case is reported by the caller, which is the only place that knows whether
|
||||
// the environment or the build supplied the DSN.
|
||||
qCInfo(CrashReportLog) << "crash reporting disabled by preference (Never send)";
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Only created when the SDK will actually run: no-DSN launches stay side-effect free.
|
||||
#if COCKATRICE_WITH_CRASH_REPORTING
|
||||
// A crash captured by an earlier run is uploaded by THIS session's transport, which posts every
|
||||
// envelope with the DSN it was armed with (sentry-native keeps the DSN in the transport state,
|
||||
// and envelopes restored from disk are raw, so their own "dsn" header is never read). When the
|
||||
// two disagree the report lands in a project nobody chose, which no other log line reveals.
|
||||
const QStringList pendingEnvelopes = pendingEnvelopePaths(sentryDatabasePath);
|
||||
for (const QString &envelopePath : pendingEnvelopes) {
|
||||
const QString mismatch = describeOriginMismatch(envelopePath, dsn);
|
||||
if (!mismatch.isEmpty()) {
|
||||
qCWarning(CrashReportLog) << "pending crash envelope" << envelopePath << mismatch;
|
||||
}
|
||||
}
|
||||
qCInfo(CrashReportLog) << "crash reporting armed for" << describeDsn(dsn);
|
||||
QDir().mkpath(sentryDatabasePath);
|
||||
if (consentMode != PersonalSettings::CrashReportingMode::AlwaysSend) {
|
||||
// sentry-native persists consent to <database>/user-consent and reloads it in sentry_init
|
||||
// before old runs are processed, so a stale "given" from an earlier AlwaysSend run (or an
|
||||
// earlier Send in Ask mode) would upload the next crash before the dialog asks. Our
|
||||
// CrashReportingMode is the source of truth, so clear the persisted state first; asking
|
||||
// afterwards re-establishes it via giveConsent()/revokeConsent().
|
||||
QFile::remove(sentryDatabasePath + QStringLiteral("/user-consent"));
|
||||
}
|
||||
sentry_options_t *options = sentry_options_new();
|
||||
sentry_options_set_dsn(options, dsn.toUtf8().constData());
|
||||
sentry_options_set_release(options, release.toUtf8().constData());
|
||||
sentry_options_set_database_path(options, sentryDatabasePath.toUtf8().constData());
|
||||
// Consent-gated uploads: crashes are captured into the local database but
|
||||
// nothing leaves the machine until giveConsent(). cache_keep holds the previous run's
|
||||
// crash envelope in <database>/cache instead of dropping it at init, so the Ask dialog
|
||||
// decides its fate: giveConsent() flushes it via the transport retry, revokeConsent()
|
||||
// deletes it below. This also covers the first crash after switching to AlwaysSend,
|
||||
// which giveConsent() (running after init) would otherwise miss. http_retry is required
|
||||
// for the flush: without it the held envelope is written with a plain filename the retry
|
||||
// scanner skips, and giveConsent()'s sentry_transport_retry() sends nothing.
|
||||
sentry_options_set_require_user_consent(options, 1);
|
||||
sentry_options_set_cache_keep(options, SENTRY_CACHE_KEEP_OFFLINE);
|
||||
sentry_options_set_http_retry(options, 1);
|
||||
sentry_options_set_before_send(options, scrubEventBeforeSend, nullptr);
|
||||
ready = (sentry_init(options) == 0);
|
||||
#else
|
||||
Q_UNUSED(release);
|
||||
// Not a misconfiguration like an empty DSN: this build cannot report whatever the DSN says, so
|
||||
// a tester forcing a crash needs to know the difference.
|
||||
qCInfo(CrashReportLog) << "crash reporting unavailable: built without Sentry support";
|
||||
ready = false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool CrashReporter::isReady() const
|
||||
{
|
||||
return ready;
|
||||
}
|
||||
|
||||
void CrashReporter::shutdown()
|
||||
{
|
||||
#if COCKATRICE_WITH_CRASH_REPORTING
|
||||
if (ready) {
|
||||
sentry_close();
|
||||
}
|
||||
#endif
|
||||
ready = false;
|
||||
}
|
||||
|
||||
CrashReporter::~CrashReporter() = default;
|
||||
CrashReporter::CrashReporter() = default;
|
||||
|
||||
bool CrashReporter::beginSession(const QString &dir)
|
||||
{
|
||||
// Restartable: releasing first keeps begin->end->begin round-trips clean.
|
||||
endSession();
|
||||
|
||||
QDir().mkpath(dir);
|
||||
const QDir sessionDir(dir);
|
||||
const QString selfName = QStringLiteral("session-%1").arg(QCoreApplication::applicationPid());
|
||||
|
||||
bool unclean = false;
|
||||
const QStringList leftovers = sessionDir.entryList({QStringLiteral("session-*")}, QDir::Files);
|
||||
for (const QString &name : leftovers) {
|
||||
if (name == selfName) {
|
||||
continue;
|
||||
}
|
||||
const QString path = sessionDir.filePath(name);
|
||||
QLockFile probe(path);
|
||||
// Long-lived session lock: stale means "owner dead", regardless of file age, so a
|
||||
// crash followed by an immediate relaunch (younger than the 30 s default) is still
|
||||
// detected. tryLock() only deletes the file when it finds it stale, so a live
|
||||
// concurrent instance's lock is never touched here. (The static
|
||||
// QLockFile::removeStaleLockFile(path) form does not exist in Qt5/Qt6, and the
|
||||
// member removeStaleLockFile() force-removes without a staleness check, so neither
|
||||
// is usable for this scan.)
|
||||
probe.setStaleLockTime(0);
|
||||
if (probe.tryLock(0)) {
|
||||
unclean = true;
|
||||
probe.unlock(); // deletes the stale file
|
||||
continue;
|
||||
}
|
||||
// tryLock failed: either a live concurrent instance owns the file, or it is
|
||||
// unreadable (garbage/empty) and therefore has no live owner. Only the latter is
|
||||
// cleaned up; a live instance is left alone. On Unix QLockFile creates the file
|
||||
// before writing the pid, so a second instance starting at the same moment can
|
||||
// briefly observe an empty (or half-written) file for a live owner: re-read once
|
||||
// after a beat so a live owner's fresh lock is never mistaken for garbage and deleted.
|
||||
qint64 pid = 0;
|
||||
QString hostname, appname;
|
||||
if (!probe.getLockInfo(&pid, &hostname, &appname)) {
|
||||
QThread::msleep(100);
|
||||
if (!probe.getLockInfo(&pid, &hostname, &appname)) {
|
||||
QFile::remove(path);
|
||||
unclean = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sessionLockPath = sessionDir.filePath(selfName);
|
||||
sessionLock = std::make_unique<QLockFile>(sessionLockPath);
|
||||
sessionLock->setStaleLockTime(0);
|
||||
if (!sessionLock->tryLock(0)) {
|
||||
qCWarning(CrashReportLog) << "Failed to claim session lock at" << sessionLockPath;
|
||||
}
|
||||
return unclean;
|
||||
}
|
||||
|
||||
void CrashReporter::endSession()
|
||||
{
|
||||
if (sessionLock) {
|
||||
// unlock() removes the lock file; nothing left to clean up.
|
||||
sessionLock->unlock();
|
||||
sessionLock.reset();
|
||||
} else if (!sessionLockPath.isEmpty()) {
|
||||
// The lock was never claimed (a live concurrent instance owns its file): only our
|
||||
// own unclaimed file, if tryLock left one behind, is removed.
|
||||
QFile::remove(sessionLockPath);
|
||||
}
|
||||
sessionLockPath.clear();
|
||||
}
|
||||
|
||||
void CrashReporter::giveConsent()
|
||||
{
|
||||
#if COCKATRICE_WITH_CRASH_REPORTING
|
||||
if (ready) {
|
||||
sentry_user_consent_give();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void CrashReporter::revokeConsent()
|
||||
{
|
||||
#if COCKATRICE_WITH_CRASH_REPORTING
|
||||
// Revoke (not reset): an explicit refusal blocks uploads; reset would
|
||||
// return to unknown, which blocks uploads too but loses the decision.
|
||||
if (ready) {
|
||||
sentry_user_consent_revoke();
|
||||
// Drop the held crash envelope: without this it would sit in <database>/cache and
|
||||
// upload on the next give (e.g. after switching to AlwaysSend), after the user
|
||||
// explicitly refused it. The SDK recreates the directory as needed.
|
||||
QDir(sentryDatabasePath + QStringLiteral("/cache")).removeRecursively();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool CrashReporter::sendReport(const QString &redactedPreview, const QString &userNote)
|
||||
{
|
||||
#if COCKATRICE_WITH_CRASH_REPORTING
|
||||
if (!ready) {
|
||||
return false;
|
||||
}
|
||||
// Local scope (not the global one): the log preview rides only on this event. A global
|
||||
// sentry_attach_bytes would linger and ride along on every later event in the session,
|
||||
// including a real native crash later on — and with the crashpad backend byte attachments
|
||||
// are written to disk for exactly that. In-memory bytes also mean no report files land in
|
||||
// the Sentry DB dir, so there is no disk growth and no same-second filename collision.
|
||||
sentry_scope_t *scope = sentry_scope_new();
|
||||
if (scope == nullptr) {
|
||||
qCWarning(CrashReportLog) << "Could not create scope for crash report";
|
||||
return false;
|
||||
}
|
||||
const QByteArray previewBytes = redactedPreview.toUtf8();
|
||||
const sentry_uuid_t attachmentId = sentry_scope_attach_bytes(
|
||||
scope, previewBytes.constData(), static_cast<size_t>(previewBytes.size()), "crash-log.txt");
|
||||
const bool attachOk = sentry_uuid_is_nil(&attachmentId) == 0;
|
||||
if (!attachOk) {
|
||||
qCWarning(CrashReportLog) << "Could not attach crash log to crash report";
|
||||
}
|
||||
bool noteAttachOk = true;
|
||||
if (!userNote.isEmpty()) {
|
||||
const QByteArray noteBytes = userNote.toUtf8();
|
||||
const sentry_uuid_t noteId = sentry_scope_attach_bytes(scope, noteBytes.constData(),
|
||||
static_cast<size_t>(noteBytes.size()), "user-note.txt");
|
||||
noteAttachOk = sentry_uuid_is_nil(¬eId) == 0;
|
||||
if (!noteAttachOk) {
|
||||
qCWarning(CrashReportLog) << "Could not attach user note to crash report";
|
||||
}
|
||||
}
|
||||
|
||||
// Release is attached automatically from the init options. Deliberately no
|
||||
// sentry_set_user call: the report must carry no PII. The dialog collects
|
||||
// no contact info, so the message event + attachments are the whole payload
|
||||
// (sentry_value_new_feedback/sentry_capture_feedback exist but stay unused).
|
||||
sentry_value_t event = sentry_value_new_message_event(SENTRY_LEVEL_ERROR, "crash-report", "Crash report");
|
||||
const sentry_uuid_t eventId = sentry_scope_capture_event(scope, event, nullptr);
|
||||
sentry_scope_free(scope);
|
||||
if (sentry_uuid_is_nil(&eventId)) {
|
||||
qCWarning(CrashReportLog) << "Failed to capture crash report event";
|
||||
return false;
|
||||
}
|
||||
char eventIdStr[37] = {};
|
||||
sentry_uuid_as_string(&eventId, eventIdStr);
|
||||
// Capture only queues the envelope to the background transport: a network failure stays
|
||||
// invisible here, so true means "queued", not "delivered". The Save fallback is offered only
|
||||
// when the local capture itself fails.
|
||||
qCInfo(CrashReportLog) << "Crash report queued, event id:" << eventIdStr;
|
||||
// A failed attachment still captures the event above, but reports failure
|
||||
// so callers offer the local-save fallback instead of silent success.
|
||||
return attachOk && noteAttachOk;
|
||||
#else
|
||||
Q_UNUSED(redactedPreview);
|
||||
Q_UNUSED(userNote);
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool CrashReporter::appendAttachmentToEnvelope(const QString &envelopePath,
|
||||
const QString &filename,
|
||||
const QByteArray &payload)
|
||||
{
|
||||
// QIODevice::ReadWrite creates a missing file; appending to a file that was never
|
||||
// written by the SDK would fabricate an envelope, so require it to exist.
|
||||
if (!QFile::exists(envelopePath)) {
|
||||
return false;
|
||||
}
|
||||
QFile file(envelopePath);
|
||||
if (!file.open(QIODevice::ReadWrite)) {
|
||||
return false;
|
||||
}
|
||||
// Item boundaries are single newlines: make sure the header starts on a fresh line.
|
||||
if (file.size() > 0) {
|
||||
if (!file.seek(file.size() - 1)) {
|
||||
return false;
|
||||
}
|
||||
char lastByte = 0;
|
||||
if (file.read(&lastByte, 1) != 1) {
|
||||
return false;
|
||||
}
|
||||
if (lastByte != '\n' && file.write("\n", 1) != 1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Item shape mirrors sentry__envelope_add_attachment in the SDK. `length` is what the
|
||||
// SDK's envelope re-parser uses to frame the payload (a missing length ends the item at
|
||||
// the next newline, which would truncate a multi-line log and make the rest of the file
|
||||
// fail to parse); `attachment_length` is the logical attachment size the SDK also emits.
|
||||
// The filename is a compile-time ASCII constant from the callers, so no JSON escaping is
|
||||
// needed.
|
||||
const QByteArray header = QByteArrayLiteral("{\"type\":\"attachment\",\"length\":") +
|
||||
QByteArray::number(payload.size()) + QByteArrayLiteral(",\"attachment_length\":") +
|
||||
QByteArray::number(payload.size()) + QByteArrayLiteral(",\"filename\":\"") +
|
||||
filename.toUtf8() + QByteArrayLiteral("\",\"content_type\":\"text/plain\"}") + '\n';
|
||||
if (file.write(header) != header.size()) {
|
||||
return false;
|
||||
}
|
||||
return file.write(payload) == payload.size();
|
||||
}
|
||||
|
||||
QStringList CrashReporter::pendingEnvelopePaths(const QString &databasePath)
|
||||
{
|
||||
static const auto isUuid = [](const QString &id) { return id.size() == 36 && id.count(QLatin1Char('-')) == 4; };
|
||||
static const auto isEnvelopeName = [](const QString &name) {
|
||||
if (name == QStringLiteral("__sentry-crash.envelope")) {
|
||||
return false;
|
||||
}
|
||||
const QString stem = name.left(name.size() - 9); // strip ".envelope"
|
||||
if (isUuid(stem)) {
|
||||
return true;
|
||||
}
|
||||
// Retry cache filename: "<ts>-<count>-<uuid>".
|
||||
const qsizetype firstSep = stem.indexOf(QLatin1Char('-'));
|
||||
const qsizetype secondSep = firstSep > 0 ? stem.indexOf(QLatin1Char('-'), firstSep + 1) : -1;
|
||||
return firstSep > 0 && secondSep > 0 && isUuid(stem.mid(secondSep + 1)) && stem.left(firstSep).toLongLong() > 0;
|
||||
};
|
||||
QStringList result;
|
||||
const QDir dbDir(databasePath);
|
||||
if (!dbDir.exists()) {
|
||||
return result;
|
||||
}
|
||||
// Consent-held envelopes (Ask mode: the SDK cached them at crash time instead
|
||||
// of sending, see sentry__submit_envelope). Bare "<uuid>.envelope" with
|
||||
// cache_keep only, "<ts>-<count>-<uuid>.envelope" with http_retry.
|
||||
const QDir cacheDir(databasePath + QStringLiteral("/cache"));
|
||||
const QStringList cacheNames = cacheDir.entryList({QStringLiteral("*.envelope")}, QDir::Files, QDir::Name);
|
||||
for (const QString &name : cacheNames) {
|
||||
if (isEnvelopeName(name)) {
|
||||
result.append(cacheDir.filePath(name));
|
||||
}
|
||||
}
|
||||
// In-flight envelopes dumped to the crashed run's own directory (AlwaysSend
|
||||
// mode: consent was already given, so the transport dump wrote them there and
|
||||
// sentry_init's process_old_runs replays them at startup — attach before init).
|
||||
const QStringList runDirs =
|
||||
dbDir.entryList({QStringLiteral("*.run")}, QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name);
|
||||
for (const QString &runDir : runDirs) {
|
||||
const QDir dir(dbDir.filePath(runDir));
|
||||
const QStringList names = dir.entryList({QStringLiteral("*.envelope")}, QDir::Files, QDir::Name);
|
||||
for (const QString &name : names) {
|
||||
if (isEnvelopeName(name)) {
|
||||
result.append(dir.filePath(name));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
int CrashReporter::attachToPendingCrashEnvelopes(const QString &redactedLogText, const QString &userNoteText)
|
||||
{
|
||||
return attachToPendingCrashEnvelopes(sentryDatabasePath, redactedLogText, userNoteText);
|
||||
}
|
||||
|
||||
int CrashReporter::attachToPendingCrashEnvelopes(const QString &databasePath,
|
||||
const QString &redactedLogText,
|
||||
const QString &userNoteText)
|
||||
{
|
||||
if (databasePath.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
const QByteArray logBytes = redactedLogText.toUtf8();
|
||||
const QByteArray noteBytes = userNoteText.toUtf8();
|
||||
int attached = 0;
|
||||
const QStringList envelopes = pendingEnvelopePaths(databasePath);
|
||||
for (const QString &path : envelopes) {
|
||||
bool ok = appendAttachmentToEnvelope(path, QStringLiteral("crash-log.txt"), logBytes);
|
||||
if (ok && !noteBytes.isEmpty()) {
|
||||
ok = appendAttachmentToEnvelope(path, QStringLiteral("user-note.txt"), noteBytes);
|
||||
}
|
||||
if (ok) {
|
||||
++attached;
|
||||
} else {
|
||||
qCWarning(CrashReportLog) << "Failed to attach crash log to pending envelope" << path;
|
||||
}
|
||||
}
|
||||
if (attached > 0) {
|
||||
qCInfo(CrashReportLog) << "Attached crash log and user note to" << attached << "pending crash envelope(s)";
|
||||
}
|
||||
return attached;
|
||||
}
|
||||
|
||||
QString CrashReporter::sanitizeLogLine(const QString &line)
|
||||
{
|
||||
static const auto colorEscapeCodePattern = QRegularExpression(QStringLiteral("\033\\[[0-9;]*m"));
|
||||
|
||||
QString sanitized = line;
|
||||
sanitized.replace(colorEscapeCodePattern, "");
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
QString CrashReporter::redactCockatriceUrls(const QString &line)
|
||||
{
|
||||
static auto urlPattern = QRegularExpression(QStringLiteral("cockatrice://\\S+"));
|
||||
|
||||
// Single pass over the original line: rescanning the replaced text would match a redacted
|
||||
// secret-free URL again and loop forever.
|
||||
QString redacted;
|
||||
redacted.reserve(line.size());
|
||||
qsizetype pos = 0;
|
||||
QRegularExpressionMatchIterator it = urlPattern.globalMatch(line);
|
||||
while (it.hasNext()) {
|
||||
QRegularExpressionMatch match = it.next();
|
||||
redacted.append(line.mid(pos, match.capturedStart() - pos));
|
||||
// The pattern only matches cockatrice:// URLs, so the check is folded into the parse:
|
||||
// scheme://host, secrets dropped.
|
||||
const QUrl parsed(match.captured(0));
|
||||
redacted.append(parsed.scheme() + QStringLiteral("://") + parsed.host());
|
||||
pos = match.capturedEnd();
|
||||
}
|
||||
redacted.append(line.mid(pos));
|
||||
return redacted;
|
||||
}
|
||||
|
|
@ -1,177 +0,0 @@
|
|||
/**
|
||||
* @file crash_reporter.h
|
||||
* @ingroup Core
|
||||
*/
|
||||
|
||||
#ifndef CRASH_REPORTER_H
|
||||
#define CRASH_REPORTER_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QLoggingCategory>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <libcockatrice/settings/personal_settings.h>
|
||||
#include <memory>
|
||||
|
||||
#ifndef COCKATRICE_WITH_CRASH_REPORTING
|
||||
#define COCKATRICE_WITH_CRASH_REPORTING 0
|
||||
#endif
|
||||
|
||||
#ifndef COCKATRICE_SENTRY_DSN
|
||||
#define COCKATRICE_SENTRY_DSN ""
|
||||
#endif
|
||||
|
||||
#ifndef COCKATRICE_SENTRY_RELEASE
|
||||
#define COCKATRICE_SENTRY_RELEASE ""
|
||||
#endif
|
||||
|
||||
class QLockFile;
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(CrashReportLog);
|
||||
|
||||
/**
|
||||
* @brief Sentry-backed crash-report facade, owned by main().
|
||||
*
|
||||
* An empty DSN (or Never consent) skips the SDK entirely, so the mock dialog
|
||||
* fallback behaves exactly as before. All SDK calls are guarded so OFF builds
|
||||
* compile to no-ops.
|
||||
*
|
||||
* @par Consent gating
|
||||
* init() arms require-user-consent and before_send scrubbing. Stale persisted
|
||||
* consent from a previous AlwaysSend run is cleared before init when the mode is
|
||||
* not AlwaysSend, and cached crash envelopes are held for the dialog instead of
|
||||
* being dropped. giveConsent()/revokeConsent() gate uploads.
|
||||
*
|
||||
* @par Envelope and log helpers
|
||||
* attachToPendingCrashEnvelopes() folds the redacted log and the optional user
|
||||
* note into the pending native crash event(s) before consent releases them, so
|
||||
* dump, stack, log and note arrive as ONE Sentry event. sendReport() is the
|
||||
* fallback for unclean exits that produced no native crash event, capturing a
|
||||
* redacted message event with the log preview attached. The log redaction
|
||||
* helpers are static and SDK-independent.
|
||||
*
|
||||
* @par Session tracking
|
||||
* beginSession()/endSession() are SDK-independent: a QLockFile per process in
|
||||
* the database dir detects unclean exits cross-platform, replacing the legacy
|
||||
* Windows-only .dmp scan. The held lock makes the reporter non-copyable and
|
||||
* non-movable.
|
||||
*/
|
||||
class CrashReporter
|
||||
{
|
||||
public:
|
||||
CrashReporter();
|
||||
CrashReporter(const CrashReporter &) = delete;
|
||||
CrashReporter &operator=(const CrashReporter &) = delete;
|
||||
CrashReporter(CrashReporter &&) = delete;
|
||||
CrashReporter &operator=(CrashReporter &&) = delete;
|
||||
~CrashReporter();
|
||||
|
||||
void init(const QString &dsn,
|
||||
const QString &release,
|
||||
const QString &databasePath,
|
||||
PersonalSettings::CrashReportingMode consentMode);
|
||||
[[nodiscard]] bool isReady() const;
|
||||
void shutdown();
|
||||
void giveConsent();
|
||||
void revokeConsent();
|
||||
bool sendReport(const QString &redactedPreview, const QString &userNote = QString());
|
||||
|
||||
/**
|
||||
* @brief Append the redacted log (and optional user note) to every pending
|
||||
* native crash envelope in the local database, so the crashed event uploads
|
||||
* with dump, stack, log and note together instead of the log arriving as a
|
||||
* separate event that has to be correlated by hand. Both storage locations
|
||||
* are covered: consent-held envelopes in <database>/cache (Ask mode) and
|
||||
* in-flight envelopes dumped into <database>/<run>.run (AlwaysSend mode).
|
||||
*
|
||||
* For the AlwaysSend variant run this BEFORE init() (static overload): the
|
||||
* startup replay of *.run envelopes happens inside sentry_init. SDK-independent
|
||||
* plain-file surgery on the Sentry envelope format, so it stays unit-testable
|
||||
* in OFF builds.
|
||||
*
|
||||
* @return the number of envelopes that received the attachments.
|
||||
*/
|
||||
int attachToPendingCrashEnvelopes(const QString &redactedLogText, const QString &userNoteText);
|
||||
static int attachToPendingCrashEnvelopes(const QString &databasePath,
|
||||
const QString &redactedLogText,
|
||||
const QString &userNoteText);
|
||||
|
||||
/**
|
||||
* @brief Append one attachment item to a cached Sentry envelope file.
|
||||
*
|
||||
* An envelope is a newline-separated stream of item-header-line + payload
|
||||
* pairs. The retry transport replays cached envelopes byte-for-byte
|
||||
* (sentry-native 0.17.3 reads them raw and serializes raw envelopes back
|
||||
* verbatim), so appending well-formed items associates them with the event
|
||||
* already in the file; the first line (event id header) is never touched.
|
||||
*
|
||||
* @param filename ASCII-only item filename, embedded verbatim in the header.
|
||||
*/
|
||||
static bool
|
||||
appendAttachmentToEnvelope(const QString &envelopePath, const QString &filename, const QByteArray &payload);
|
||||
|
||||
/**
|
||||
* @brief Pending crash envelopes in <database>: the consent-held files in
|
||||
* <database>/cache and the in-flight files in <database>/<run>.run.
|
||||
*
|
||||
* Matches the cache filename conventions sentry-native accepts (see
|
||||
* sentry__parse_cache_filename): bare "<uuid>.envelope" and retry
|
||||
* "<ts>-<count>-<uuid>.envelope"; run dirs accept plain "<uuid>.envelope"
|
||||
* minus the SDK's own "__sentry-crash.envelope" marker.
|
||||
*/
|
||||
static QStringList pendingEnvelopePaths(const QString &databasePath);
|
||||
|
||||
/**
|
||||
* @brief SDK-independent gating decision, unit-testable in OFF builds.
|
||||
*/
|
||||
static bool isReportingEnabled(const QString &dsn, PersonalSettings::CrashReportingMode consentMode);
|
||||
|
||||
/**
|
||||
* @brief Whether a DSN has the shape the build and the SDK require:
|
||||
* <scheme>://<key>@<host>/<project-id>.
|
||||
*
|
||||
* Deliberately the same unanchored pattern cmake/CrashReporting.cmake matches (CMake's MATCHES
|
||||
* searches rather than anchoring), so a DSN this build accepted is never rejected here. The SDK
|
||||
* refuses to even build a request for a DSN it considers invalid, so an unchecked typo disables
|
||||
* uploading with no error anywhere.
|
||||
*/
|
||||
static bool isDsnWellFormed(const QString &dsn);
|
||||
|
||||
/**
|
||||
* @brief Human-readable destination of a DSN, e.g. "o0.ingest.sentry.io (project 1234567)".
|
||||
*
|
||||
* Safe to log: the public key is never part of the result. Empty for a malformed DSN.
|
||||
*/
|
||||
static QString describeDsn(const QString &dsn);
|
||||
|
||||
/**
|
||||
* @brief Describe the captured-for DSN of a pending envelope when it points at a different
|
||||
* destination than the DSN this session would upload it with; empty when they agree.
|
||||
*
|
||||
* sentry-native posts every envelope with the DSN held in the transport state of the session
|
||||
* doing the sending (envelopes restored from disk are raw and their own "dsn" header is never
|
||||
* read), so a cached crash envelope captured for one project can silently be uploaded to
|
||||
* another - the exact way a tester's report lands in the wrong place. Compares destinations
|
||||
* (host and project id), not raw strings, so a different key for the same project stays quiet.
|
||||
*/
|
||||
static QString describeOriginMismatch(const QString &envelopePath, const QString ¤tDsn);
|
||||
|
||||
/**
|
||||
* @brief Claim this process's session lock, reporting a previous unclean exit.
|
||||
* @return true if a leftover session lock from a dead process (or an unreadable
|
||||
* garbage file) was found and cleaned; live concurrent instances are ignored.
|
||||
*/
|
||||
bool beginSession(const QString &dir);
|
||||
void endSession();
|
||||
|
||||
static QString sanitizeLogLine(const QString &line);
|
||||
static QString redactCockatriceUrls(const QString &line);
|
||||
|
||||
private:
|
||||
bool ready = false;
|
||||
QString sentryDatabasePath;
|
||||
QString sessionLockPath;
|
||||
std::unique_ptr<QLockFile> sessionLock;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
@ -1,10 +1,7 @@
|
|||
#include "lag_monitor.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QCoreApplication>
|
||||
#include <QDateTime>
|
||||
#include <QEvent>
|
||||
#include <QMessageLogger>
|
||||
#include <QTimer>
|
||||
|
||||
LagMonitor::LagMonitor(QObject *parent) : QObject(parent)
|
||||
|
|
|
|||
|
|
@ -6,15 +6,15 @@
|
|||
#ifndef LAG_MONITOR_H
|
||||
#define LAG_MONITOR_H
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QElapsedTimer>
|
||||
#include <QList>
|
||||
#include <QLoggingCategory>
|
||||
#include <QObject>
|
||||
#include <QtGlobal>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(LagMonitorLog, "lag_monitor");
|
||||
|
||||
class QEvent;
|
||||
class QTimer;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -5,11 +5,7 @@
|
|||
|
||||
#include "latency_graph_widget.h"
|
||||
|
||||
#include <QColor>
|
||||
#include <QPainter>
|
||||
#include <QRectF>
|
||||
#include <QtGlobal>
|
||||
#include <qnumeric.h>
|
||||
|
||||
LatencyGraphWidget::LatencyGraphWidget(QWidget *parent) : QWidget(parent)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@
|
|||
|
||||
#include <QList>
|
||||
#include <QWidget>
|
||||
#include <QtGlobal>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
/**
|
||||
* @brief Bar graph of recent network round-trip samples.
|
||||
|
|
|
|||
|
|
@ -6,16 +6,11 @@
|
|||
#include "latency_status_widget.h"
|
||||
|
||||
#include "latency_graph_widget.h"
|
||||
#include "latency_tracker.h"
|
||||
|
||||
#include <QEvent>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QObject>
|
||||
#include <QPoint>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/utility/qt_utils.h>
|
||||
#include <qnamespace.h>
|
||||
|
||||
LatencyStatusWidget::LatencyStatusWidget(QWidget *parent) : QWidget(parent)
|
||||
{
|
||||
|
|
@ -25,7 +20,9 @@ LatencyStatusWidget::LatencyStatusWidget(QWidget *parent) : QWidget(parent)
|
|||
latencyGraph = new LatencyGraphWidget(this);
|
||||
latencyGraph->setFixedSize(90, 14);
|
||||
|
||||
auto *layout = QtUtils::makeLayout<QHBoxLayout>(this, 4);
|
||||
auto *layout = new QHBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(4);
|
||||
layout->addWidget(latencyGraph);
|
||||
layout->addWidget(pingLabel);
|
||||
|
||||
|
|
|
|||
|
|
@ -7,10 +7,8 @@
|
|||
#define LATENCY_STATUS_WIDGET_H
|
||||
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QWidget>
|
||||
#include <libcockatrice/network/client/abstract/latency_tracker.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
class QLabel;
|
||||
class LatencyGraphWidget;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
#include "remote_connection_controller.h"
|
||||
|
||||
#include "../../../interface/pixel_map_generator.h"
|
||||
#include "../../settings/cache_settings.h"
|
||||
#include "../interface/widgets/dialogs/dlg_connect.h"
|
||||
#include "../interface/widgets/dialogs/dlg_forgot_password_challenge.h"
|
||||
|
|
@ -8,26 +7,14 @@
|
|||
#include "../interface/widgets/dialogs/dlg_forgot_password_reset.h"
|
||||
#include "../interface/widgets/dialogs/dlg_register.h"
|
||||
#include "../interface/widgets/utility/get_text_with_max.h"
|
||||
#include "libcockatrice/protocol/pb/event_connection_closed.pb.h"
|
||||
#include "libcockatrice/protocol/pb/event_server_shutdown.pb.h"
|
||||
|
||||
#include <QChar>
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
#include <QLineEdit>
|
||||
#include <QList>
|
||||
#include <QMessageBox>
|
||||
#include <QPixmap>
|
||||
#include <QStringList>
|
||||
#include <QStringLiteral>
|
||||
#include <QThread>
|
||||
#include <libcockatrice/network/client/remote/remote_client.h>
|
||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||
#include <libcockatrice/settings/servers_settings.h>
|
||||
#include <libcockatrice/utility/qt_utils.h>
|
||||
#include <qnamespace.h>
|
||||
|
||||
class QWidget;
|
||||
|
||||
ConnectionController::ConnectionController(QWidget *dialogParent, QObject *parent)
|
||||
: QObject(parent), dialogParent(dialogParent)
|
||||
|
|
@ -96,9 +83,6 @@ void ConnectionController::wireClientSignals()
|
|||
|
||||
connect(remoteClient, &RemoteClient::sigPromptForForgotPasswordChallenge, this,
|
||||
&ConnectionController::onPromptForgotPasswordChallenge);
|
||||
|
||||
connect(remoteClient, &RemoteClient::sigPasswordVerifierReady, this,
|
||||
&ConnectionController::onPasswordVerifierReady);
|
||||
}
|
||||
|
||||
void ConnectionController::connectToServer()
|
||||
|
|
@ -107,32 +91,16 @@ void ConnectionController::connectToServer()
|
|||
connect(dlgConnect, &DlgConnect::sigStartForgotPasswordRequest, this, &ConnectionController::forgotPasswordRequest);
|
||||
|
||||
if (dlgConnect->exec()) {
|
||||
pendingSaveName = dlgConnect->getSaveName();
|
||||
pendingSavePassword = dlgConnect->getSavePassword();
|
||||
remoteClient->setStoredVerifier(dlgConnect->getStoredVerifier());
|
||||
remoteClient->connectToServer(dlgConnect->getHost(), static_cast<unsigned int>(dlgConnect->getPort()),
|
||||
dlgConnect->getPlayerName(), dlgConnect->getPassword());
|
||||
}
|
||||
}
|
||||
|
||||
void ConnectionController::onPasswordVerifierReady(const QString &verifier)
|
||||
{
|
||||
if (pendingSavePassword) {
|
||||
SettingsCache::instance().servers().setServerPassword(pendingSaveName, verifier);
|
||||
}
|
||||
}
|
||||
|
||||
void ConnectionController::connectToServerDirect(const QString &host,
|
||||
unsigned int port,
|
||||
const QString &playerName,
|
||||
const QString &password,
|
||||
const QString &storedVerifier,
|
||||
const QString &saveName,
|
||||
bool savePassword)
|
||||
const QString &password)
|
||||
{
|
||||
pendingSaveName = saveName;
|
||||
pendingSavePassword = savePassword;
|
||||
remoteClient->setStoredVerifier(storedVerifier);
|
||||
remoteClient->connectToServer(host, port, playerName, password);
|
||||
}
|
||||
|
||||
|
|
@ -212,7 +180,7 @@ void ConnectionController::onServerShutdownEvent(const Event_ServerShutdown &eve
|
|||
"games will be lost.\nReason for shutdown: %1",
|
||||
"", event.minutes())
|
||||
.arg(QString::fromStdString(event.reason())));
|
||||
serverShutdownMessageBox.setIconPixmap(themePixmap(QStringLiteral("cockatrice")).scaled(64, 64));
|
||||
serverShutdownMessageBox.setIconPixmap(QPixmap("theme:cockatrice").scaled(64, 64));
|
||||
serverShutdownMessageBox.setText(tr("Scheduled server shutdown"));
|
||||
serverShutdownMessageBox.setWindowModality(Qt::ApplicationModal);
|
||||
serverShutdownMessageBox.setVisible(true);
|
||||
|
|
@ -244,42 +212,21 @@ void ConnectionController::onLoginError(int r,
|
|||
{
|
||||
switch (static_cast<Response::ResponseCode>(r)) {
|
||||
case Response::RespClientUpdateRequired: {
|
||||
QString formatted = "Missing Features: ";
|
||||
for (int i = 0; i < missingFeatures.size(); ++i) {
|
||||
formatted.append(QString("\n %1").arg(QChar(0x2022)) + " " + missingFeatures.value(i));
|
||||
}
|
||||
|
||||
QMessageBox msgBox(dialogParent);
|
||||
msgBox.setIcon(QMessageBox::Critical);
|
||||
msgBox.setWindowTitle(tr("Failed Login"));
|
||||
|
||||
if (missingFeatures.isEmpty()) {
|
||||
// Without a feature list this is either a login the client stopped itself or a
|
||||
// strict server asking for a password; the reason string already says which and
|
||||
// what to do. No client update can fix any of those causes, so show it instead of
|
||||
// the update advice.
|
||||
msgBox.setText(reasonStr.isEmpty() ? tr("The login did not complete.") : reasonStr);
|
||||
} else {
|
||||
QString formatted = tr("Missing Features:");
|
||||
for (const QString &feature : missingFeatures) {
|
||||
formatted.append(QString("\n %1 ").arg(QChar(0x2022)) + feature);
|
||||
}
|
||||
|
||||
msgBox.setText(tr("Your client seems to be missing features this server requires for connection.") +
|
||||
"\n\n" + tr("To update your client, go to 'Help -> Check for Client Updates'."));
|
||||
msgBox.setDetailedText(formatted);
|
||||
}
|
||||
|
||||
msgBox.setText(tr("Your client seems to be missing features this server requires for connection.") +
|
||||
"\n\n" + tr("To update your client, go to 'Help -> Check for Client Updates'."));
|
||||
msgBox.setDetailedText(formatted);
|
||||
msgBox.exec();
|
||||
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: {
|
||||
QMessageBox::critical(dialogParent, tr("Error"),
|
||||
tr("Incorrect username or password. "
|
||||
|
|
@ -311,8 +258,9 @@ void ConnectionController::onLoginError(int r,
|
|||
}
|
||||
|
||||
case Response::RespRegistrationRequired: {
|
||||
if (QtUtils::confirm(dialogParent, tr("Error"),
|
||||
tr("This server requires user registration. Do you want to register now?"))) {
|
||||
if (QMessageBox::question(dialogParent, tr("Error"),
|
||||
tr("This server requires user registration. Do you want to register now?"),
|
||||
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
|
||||
registerToServer();
|
||||
}
|
||||
return; // don't re-prompt connect
|
||||
|
|
@ -405,15 +353,12 @@ void ConnectionController::onRegisterError(int r, QString reasonStr, quint32 end
|
|||
break;
|
||||
}
|
||||
case Response::RespTooManyRequests: {
|
||||
QString deniedStr =
|
||||
endTime ? tr("Too many registration attempts. Try again after %1.")
|
||||
.arg(QDateTime::fromSecsSinceEpoch(endTime).toString())
|
||||
: tr("It appears you are attempting to register a new account on this server yet you "
|
||||
"already have an account registered with the email provided. This server "
|
||||
"restricts the number of accounts a user can register per address. Please "
|
||||
"contact the server operator for further assistance or to obtain your "
|
||||
"credential information.");
|
||||
QMessageBox::critical(dialogParent, tr("Registration denied"), deniedStr);
|
||||
QMessageBox::critical(dialogParent, tr("Registration denied"),
|
||||
tr("It appears you are attempting to register a new account on this server yet you "
|
||||
"already have an account registered with the email provided. This server "
|
||||
"restricts the number of accounts a user can register per address. Please "
|
||||
"contact the server operator for further assistance or to obtain your "
|
||||
"credential information."));
|
||||
break;
|
||||
}
|
||||
case Response::RespPasswordTooShort: {
|
||||
|
|
@ -527,38 +472,14 @@ void ConnectionController::onForgotPasswordSuccess()
|
|||
SettingsCache::instance().servers().setFPPlayerName("");
|
||||
}
|
||||
|
||||
void ConnectionController::onForgotPasswordError(int responseCode)
|
||||
void ConnectionController::onForgotPasswordError()
|
||||
{
|
||||
// Rate limiting is retryable, so it must not clear the prefill.
|
||||
bool keepPrefill = false;
|
||||
QString message;
|
||||
switch (static_cast<Response::ResponseCode>(responseCode)) {
|
||||
case Response::RespTooManyRequests:
|
||||
// Retryable: keep the host/port/user prefill so the user can try again. The window
|
||||
// is server-configured (an hour by default), so do not promise a shorter wait.
|
||||
keepPrefill = true;
|
||||
message = tr("Too many password reset attempts from your address. Please wait before "
|
||||
"requesting a new reset.");
|
||||
break;
|
||||
case Response::RespFunctionNotAllowed:
|
||||
// Also returned for an unknown user, a banned user, and an expired token or
|
||||
// challenge, so the message must not claim a single cause.
|
||||
message = tr("The server did not accept the reset request. Check the user name, or contact "
|
||||
"the server operator to reset your password.");
|
||||
break;
|
||||
default:
|
||||
message = tr("Failed to reset user account password, please contact the server operator to reset your "
|
||||
"password.");
|
||||
break;
|
||||
}
|
||||
|
||||
QMessageBox::warning(dialogParent, tr("Reset Password"), message);
|
||||
|
||||
if (!keepPrefill) {
|
||||
SettingsCache::instance().servers().setFPHostName("");
|
||||
SettingsCache::instance().servers().setFPPort("");
|
||||
SettingsCache::instance().servers().setFPPlayerName("");
|
||||
}
|
||||
QMessageBox::warning(
|
||||
dialogParent, tr("Reset Password"),
|
||||
tr("Failed to reset user account password, please contact the server operator to reset your password."));
|
||||
SettingsCache::instance().servers().setFPHostName("");
|
||||
SettingsCache::instance().servers().setFPPort("");
|
||||
SettingsCache::instance().servers().setFPPlayerName("");
|
||||
}
|
||||
|
||||
void ConnectionController::onPromptForgotPasswordReset()
|
||||
|
|
|
|||
|
|
@ -2,22 +2,17 @@
|
|||
#define COCKATRICE_REMOTE_CONNECTION_CONTROLLER_H
|
||||
|
||||
#include "abstract_client.h"
|
||||
#include "latency_tracker.h"
|
||||
|
||||
#include <QMessageBox>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QtGlobal>
|
||||
#include <qtmetamacros.h>
|
||||
#include <QThread>
|
||||
#include <libcockatrice/protocol/pb/event_connection_closed.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_server_shutdown.pb.h>
|
||||
|
||||
class RemoteClient;
|
||||
class ServerInfo_User;
|
||||
class DlgConnect;
|
||||
class Event_ConnectionClosed;
|
||||
class Event_ServerShutdown;
|
||||
class QThread;
|
||||
class QWidget;
|
||||
template <typename T> class QList;
|
||||
|
||||
/**
|
||||
* Owns the RemoteClient and its worker thread.
|
||||
|
|
@ -40,13 +35,8 @@ public:
|
|||
void registerToServer();
|
||||
void forgotPasswordRequest();
|
||||
void connectToServer();
|
||||
void connectToServerDirect(const QString &host,
|
||||
unsigned int port,
|
||||
const QString &playerName,
|
||||
const QString &password,
|
||||
const QString &storedVerifier = QString(),
|
||||
const QString &saveName = QString(),
|
||||
bool savePassword = false);
|
||||
void
|
||||
connectToServerDirect(const QString &host, unsigned int port, const QString &playerName, const QString &password);
|
||||
void disconnectFromServer();
|
||||
|
||||
void refreshWindowTitle()
|
||||
|
|
@ -87,13 +77,10 @@ private slots:
|
|||
|
||||
// Forgot-password flow
|
||||
void onForgotPasswordSuccess();
|
||||
void onForgotPasswordError(int responseCode);
|
||||
void onForgotPasswordError();
|
||||
void onPromptForgotPasswordReset();
|
||||
void onPromptForgotPasswordChallenge();
|
||||
|
||||
// Persists the derived scrypt verifier after a successful challenge-response login
|
||||
void onPasswordVerifierReady(const QString &verifier);
|
||||
|
||||
private:
|
||||
void wireClientSignals();
|
||||
void updateWindowTitle();
|
||||
|
|
@ -110,10 +97,6 @@ private:
|
|||
|
||||
// Kept as a member so the forgot-password signal can be wired to it
|
||||
DlgConnect *dlgConnect{nullptr};
|
||||
|
||||
// Captured from the connect dialog when a connection is initiated
|
||||
QString pendingSaveName;
|
||||
bool pendingSavePassword{false};
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_REMOTE_CONNECTION_CONTROLLER_H
|
||||
|
|
|
|||
|
|
@ -1,31 +1,19 @@
|
|||
#include "deck_stats_interface.h"
|
||||
|
||||
#include "libcockatrice/card/card_info.h"
|
||||
#include "libcockatrice/card/database/card_database_querier.h"
|
||||
#include "libcockatrice/deck_list/tree/inner_deck_list_node.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QDesktopServices>
|
||||
#include <QMessageBox>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QRegularExpression>
|
||||
#include <QRegularExpressionMatch>
|
||||
#include <QSharedPointer>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <functional>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
|
||||
#include <libcockatrice/network/network_manager.h>
|
||||
#include <version_string.h>
|
||||
|
||||
DeckStatsInterface::DeckStatsInterface(QObject *parent) : QObject(parent)
|
||||
{
|
||||
manager = NetworkManagerFactory::create(this);
|
||||
manager = new QNetworkAccessManager(this);
|
||||
connect(manager, &QNetworkAccessManager::finished, this, &DeckStatsInterface::queryFinished);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,8 +7,7 @@
|
|||
#ifndef DECKSTATS_INTERFACE_H
|
||||
#define DECKSTATS_INTERFACE_H
|
||||
|
||||
#include <QObject>
|
||||
#include <qtmetamacros.h>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
|
||||
class QByteArray;
|
||||
class QNetworkAccessManager;
|
||||
|
|
|
|||
|
|
@ -1,37 +1,19 @@
|
|||
#include "tapped_out_interface.h"
|
||||
|
||||
#include "libcockatrice/card/card_info.h"
|
||||
#include "libcockatrice/card/database/card_database_querier.h"
|
||||
#include "libcockatrice/deck_list/tree/inner_deck_list_node.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QDebug>
|
||||
#include <QDesktopServices>
|
||||
#include <QList>
|
||||
#include <QMessageBox>
|
||||
#include <QMessageLogger>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QRegularExpression>
|
||||
#include <QRegularExpressionMatch>
|
||||
#include <QRegularExpressionMatchIterator>
|
||||
#include <QSharedPointer>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <QVariant>
|
||||
#include <functional>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
|
||||
#include <libcockatrice/network/network_manager.h>
|
||||
#include <version_string.h>
|
||||
|
||||
TappedOutInterface::TappedOutInterface(QObject *parent) : QObject(parent)
|
||||
{
|
||||
manager = NetworkManagerFactory::create(this);
|
||||
manager = new QNetworkAccessManager(this);
|
||||
connect(manager, &QNetworkAccessManager::finished, this, &TappedOutInterface::queryFinished);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@
|
|||
|
||||
#include <QLoggingCategory>
|
||||
#include <QObject>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(TappedOutInterfaceLog, "tapped_out_interface");
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
#include "deck_link_to_api_transformer.h"
|
||||
|
||||
#include <QRegularExpression>
|
||||
#include <QRegularExpressionMatch>
|
||||
|
||||
namespace DeckLinkToApiTransformer
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,37 +1,21 @@
|
|||
#include "spoiler_background_updater.h"
|
||||
|
||||
#include "../../../../interface/window_main.h"
|
||||
#include "../../../../main.h"
|
||||
#include "../../../settings/cache_settings.h"
|
||||
|
||||
#include <QChar>
|
||||
#include <QCryptographicHash>
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QFutureWatcher>
|
||||
#include <QIODevice>
|
||||
#include <QList>
|
||||
#include <QLocale>
|
||||
#include <QMessageLogger>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QSharedPointer>
|
||||
#include <QSystemTrayIcon>
|
||||
#include <QTimeZone>
|
||||
#include <QUrl>
|
||||
#include <QtConcurrentRun>
|
||||
#include <QtGlobal>
|
||||
#include <functional>
|
||||
#include <QtConcurrent>
|
||||
#include <libcockatrice/card/database/card_database.h>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/network/network_manager.h>
|
||||
#include <libcockatrice/settings/download_settings.h>
|
||||
#include <libcockatrice/settings/paths_settings.h>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
#include <version_string.h>
|
||||
|
||||
#define SPOILERS_STATUS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/SpoilerSeasonEnabled"
|
||||
|
|
@ -57,7 +41,7 @@ void SpoilerBackgroundUpdater::startSpoilerDownloadProcess(QString url, bool sav
|
|||
|
||||
void SpoilerBackgroundUpdater::downloadFromURL(QUrl url, bool saveResults)
|
||||
{
|
||||
auto *nam = NetworkManagerFactory::create(this);
|
||||
auto *nam = new QNetworkAccessManager(this);
|
||||
auto request = QNetworkRequest(url);
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
||||
QNetworkReply *reply = nam->get(request);
|
||||
|
|
@ -179,15 +163,7 @@ bool SpoilerBackgroundUpdater::saveDownloadedFile(QByteArray data)
|
|||
|
||||
// Data written, so reload the card database
|
||||
qCInfo(SpoilerBackgroundUpdaterLog) << "Spoiler Service Data Written";
|
||||
// Spoilers can introduce sets the config has never seen; the loader never
|
||||
// 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(); }));
|
||||
const auto reloadOk = QtConcurrent::run([] { CardDatabaseManager::getInstance()->loadCardDatabases(); });
|
||||
|
||||
// If the user has notifications enabled, let them know
|
||||
// when the database was last updated
|
||||
|
|
|
|||
|
|
@ -10,11 +10,7 @@
|
|||
#include <QByteArray>
|
||||
#include <QLoggingCategory>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
class QProcess;
|
||||
class QUrl;
|
||||
#include <QProcess>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(SpoilerBackgroundUpdaterLog, "spoiler_background_updater");
|
||||
|
||||
|
|
|
|||
|
|
@ -6,12 +6,9 @@
|
|||
|
||||
#ifndef CLIENT_UPDATE_CHECKER_H
|
||||
#define CLIENT_UPDATE_CHECKER_H
|
||||
#include "release_channel.h"
|
||||
|
||||
#include <QMetaObject>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
class Release;
|
||||
|
||||
/**
|
||||
* We use a singleton instance of UpdateChannel, which can cause interference and feedback loops when multiple objects
|
||||
|
|
|
|||
|
|
@ -2,28 +2,14 @@
|
|||
|
||||
#include "version_string.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QDebug>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
#include <QJsonValue>
|
||||
#include <QMap>
|
||||
#include <QMessageLogger>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QMessageBox>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QRegularExpression>
|
||||
#include <QRegularExpressionMatch>
|
||||
#include <QUrl>
|
||||
#include <QVariant>
|
||||
#include <QVariantMap>
|
||||
#include <QSysInfo>
|
||||
#include <QtGlobal>
|
||||
#include <libcockatrice/network/network_manager.h>
|
||||
#include <optional>
|
||||
#include <qnamespace.h>
|
||||
#include <qprocessordetection.h>
|
||||
|
||||
#if defined(Q_OS_MACOS)
|
||||
#include <sys/sysctl.h>
|
||||
|
|
@ -40,30 +26,7 @@
|
|||
|
||||
#define GIT_SHORT_HASH_LEN 7
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
#if defined(Q_OS_MACOS) || defined(Q_OS_WIN)
|
||||
// QSysInfo::productVersion() is not guaranteed to start with a number, e.g. it returns
|
||||
// "Server 2022" on Windows Server. Use the first numeric token as the major host version.
|
||||
std::optional<int> getProductVersionMajor()
|
||||
{
|
||||
const QString versionString = QSysInfo::productVersion();
|
||||
bool ok = false;
|
||||
static const QRegularExpression firstNumber(R"((\d+))");
|
||||
const auto match = firstNumber.match(versionString);
|
||||
const int version = match.hasMatch() ? match.captured(1).toInt(&ok) : 0;
|
||||
if (!ok) {
|
||||
qCWarning(ReleaseChannelLog) << "Unable to determine OS version from" << versionString;
|
||||
return std::nullopt;
|
||||
}
|
||||
return version;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
ReleaseChannel::ReleaseChannel() : netMan(NetworkManagerFactory::create(this)), response(nullptr), lastRelease(nullptr)
|
||||
ReleaseChannel::ReleaseChannel() : netMan(new QNetworkAccessManager(this)), response(nullptr), lastRelease(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -80,93 +43,55 @@ void ReleaseChannel::checkForUpdates()
|
|||
connect(response, &QNetworkReply::finished, this, &ReleaseChannel::releaseListFinished);
|
||||
}
|
||||
|
||||
// Find assets compatible with host platform (Linux is not supported by in-client updater)
|
||||
std::optional<int> ReleaseChannel::getTargetVersionForCurrentOS(const QString &fileName)
|
||||
// Different release channel checking functions for different operating systems
|
||||
bool ReleaseChannel::downloadMatchesCurrentOS(const QString &fileName)
|
||||
{
|
||||
#if defined(Q_OS_WIN) && Q_PROCESSOR_WORDSIZE == 4
|
||||
// The published Windows assets are 64-bit only
|
||||
Q_UNUSED(fileName);
|
||||
return std::nullopt;
|
||||
#endif
|
||||
|
||||
const QRegularExpression *regex = nullptr;
|
||||
|
||||
// Only platforms shipping updater assets (macOS, Windows) need the host version
|
||||
static const std::optional<int> systemVersion =
|
||||
#if defined(Q_OS_MACOS) || defined(Q_OS_WIN)
|
||||
getProductVersionMajor();
|
||||
#else
|
||||
std::nullopt;
|
||||
#endif
|
||||
|
||||
if (!systemVersion) {
|
||||
return std::nullopt;
|
||||
#if defined(Q_OS_MACOS)
|
||||
static QRegularExpression version_regex("macOS(\\d+)");
|
||||
auto match = version_regex.match(fileName);
|
||||
if (!match.hasMatch()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#if defined(Q_OS_MACOS)
|
||||
static const bool isIntel = [] {
|
||||
auto getSystemVersion = [] {
|
||||
// QSysInfo does not go through translation layers
|
||||
// We need to use sysctl to reliably detect the underlying architecture
|
||||
char arch[255];
|
||||
size_t len = sizeof(arch);
|
||||
if (sysctlbyname("machdep.cpu.brand_string", arch, &len, nullptr, 0) == 0) {
|
||||
return QString::fromUtf8(arch).contains("Intel");
|
||||
// Intel mac is only supported on macOS 13 versions
|
||||
if (QString::fromUtf8(arch).contains("Intel")) {
|
||||
return 13;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}();
|
||||
|
||||
// Apple (ARM) --> Cockatrice-3.0.0-macOS15.dmg
|
||||
// Apple (x86) --> Cockatrice-3.0.0-macOS15_Intel.dmg
|
||||
static const QRegularExpression macIntelRegex(R"(-macOS(\d+)_Intel\.[^.]+$)",
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
static const QRegularExpression macRegex(R"(-macOS(\d+)\.[^.]+$)", QRegularExpression::CaseInsensitiveOption);
|
||||
regex = isIntel ? &macIntelRegex : &macRegex;
|
||||
return QSysInfo::productVersion().split(".")[0].toInt();
|
||||
};
|
||||
|
||||
// older(smaller) releases are compatible with a newer or the same system version
|
||||
int sys_maj = getSystemVersion();
|
||||
int rel_maj = match.captured(1).toInt();
|
||||
return rel_maj == sys_maj;
|
||||
|
||||
#elif defined(Q_OS_WIN)
|
||||
// Windows (x86) --> Cockatrice-3.0.0-Windows10.exe
|
||||
static const QRegularExpression winRegex(R"(-(?:Win|Windows)(\d+)\.[^.]+$)",
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
regex = &winRegex;
|
||||
|
||||
#else // if the OS doesn't fit one of the above #defines, then it will never match
|
||||
#if Q_PROCESSOR_WORDSIZE == 4
|
||||
return fileName.contains("32bit");
|
||||
#elif Q_PROCESSOR_WORDSIZE == 8
|
||||
const QString &version = QSysInfo::productVersion();
|
||||
if (version.startsWith("7") || version.startsWith("8")) {
|
||||
return fileName.contains("Win7");
|
||||
} else {
|
||||
return fileName.contains("Win10");
|
||||
}
|
||||
#else
|
||||
Q_UNUSED(fileName);
|
||||
return std::nullopt;
|
||||
return false;
|
||||
#endif
|
||||
|
||||
// Any asset targeting an OS version up to the current host version works
|
||||
auto match = regex->match(fileName);
|
||||
if (!match.hasMatch()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const int targetVersion = match.captured(1).toInt();
|
||||
if (targetVersion > *systemVersion) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return targetVersion;
|
||||
}
|
||||
|
||||
// Pick newest targeted version (highest number) amongst all compatible ones
|
||||
QString ReleaseChannel::findBestDownloadUrl(const QVariantList &assets)
|
||||
{
|
||||
QString bestUrl;
|
||||
int bestVersion = -1;
|
||||
for (const auto &rawAsset : assets) {
|
||||
QVariantMap asset = rawAsset.toMap();
|
||||
QString name = asset["name"].toString();
|
||||
QString url = asset["browser_download_url"].toString();
|
||||
auto version = getTargetVersionForCurrentOS(name);
|
||||
if (!version) {
|
||||
continue;
|
||||
}
|
||||
if (*version > bestVersion) {
|
||||
bestVersion = *version;
|
||||
bestUrl = url;
|
||||
}
|
||||
}
|
||||
if (!bestUrl.isEmpty()) {
|
||||
qCInfo(ReleaseChannelLog) << "Best compatible asset=" << bestUrl;
|
||||
}
|
||||
return bestUrl;
|
||||
#else // If the OS doesn't fit one of the above #defines, then it will never match
|
||||
Q_UNUSED(fileName);
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
QString StableReleaseChannel::getManualDownloadUrl() const
|
||||
|
|
@ -213,9 +138,16 @@ void StableReleaseChannel::releaseListFinished()
|
|||
lastRelease->setPublishDate(resultMap["published_at"].toDate());
|
||||
|
||||
if (resultMap.contains("assets")) {
|
||||
auto url = findBestDownloadUrl(resultMap["assets"].toList());
|
||||
if (!url.isEmpty()) {
|
||||
lastRelease->setDownloadUrl(url);
|
||||
auto rawAssets = resultMap["assets"].toList();
|
||||
for (const auto &rawAsset : rawAssets) {
|
||||
QVariantMap asset = rawAsset.toMap();
|
||||
QString name = asset["name"].toString();
|
||||
QString url = asset["browser_download_url"].toString();
|
||||
|
||||
if (downloadMatchesCurrentOS(name)) {
|
||||
lastRelease->setDownloadUrl(url);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -357,10 +289,21 @@ void BetaReleaseChannel::fileListFinished()
|
|||
bool needToUpdate = (QString::compare(shortHash, myHash, Qt::CaseInsensitive) != 0);
|
||||
bool compatibleVersion = false;
|
||||
|
||||
QString downloadUrl = findBestDownloadUrl(resultList);
|
||||
if (!downloadUrl.isEmpty()) {
|
||||
compatibleVersion = true;
|
||||
lastRelease->setDownloadUrl(downloadUrl);
|
||||
QStringList resultUrlList{};
|
||||
for (QVariant file : resultList) {
|
||||
QVariantMap map = file.toMap();
|
||||
resultUrlList << map["browser_download_url"].toString();
|
||||
}
|
||||
|
||||
resultUrlList.sort();
|
||||
// iterate in reverse so the first item is the latest os version
|
||||
for (auto url = resultUrlList.rbegin(); url < resultUrlList.rend(); ++url) {
|
||||
if (downloadMatchesCurrentOS(*url)) {
|
||||
compatibleVersion = true;
|
||||
lastRelease->setDownloadUrl(*url);
|
||||
qCInfo(ReleaseChannelLog) << "Found compatible version url=" << *url;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
emit finishedCheck(needToUpdate, compatibleVersion, lastRelease);
|
||||
|
|
|
|||
|
|
@ -11,9 +11,7 @@
|
|||
#include <QLoggingCategory>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QVariantList>
|
||||
#include <optional>
|
||||
#include <qtmetamacros.h>
|
||||
#include <QVariantMap>
|
||||
#include <utility>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(ReleaseChannelLog, "release_channel");
|
||||
|
|
@ -98,8 +96,7 @@ protected:
|
|||
Release *lastRelease;
|
||||
|
||||
protected:
|
||||
static std::optional<int> getTargetVersionForCurrentOS(const QString &fileName);
|
||||
static QString findBestDownloadUrl(const QVariantList &assets);
|
||||
static bool downloadMatchesCurrentOS(const QString &fileName);
|
||||
[[nodiscard]] virtual QString getReleaseChannelUrl() const = 0;
|
||||
|
||||
public:
|
||||
|
|
|
|||
|
|
@ -1,19 +1,10 @@
|
|||
#include "update_downloader.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QChar>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QIODevice>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkRequest>
|
||||
#include <QUrl>
|
||||
#include <QVariant>
|
||||
#include <libcockatrice/network/network_manager.h>
|
||||
|
||||
UpdateDownloader::UpdateDownloader(QObject *parent) : QObject(parent), response(nullptr)
|
||||
{
|
||||
netMan = NetworkManagerFactory::create(this);
|
||||
netMan = new QNetworkAccessManager(this);
|
||||
}
|
||||
|
||||
void UpdateDownloader::beginDownload(QUrl downloadUrl)
|
||||
|
|
|
|||
|
|
@ -7,14 +7,8 @@
|
|||
#ifndef COCKATRICE_UPDATEDOWNLOADER_H
|
||||
#define COCKATRICE_UPDATEDOWNLOADER_H
|
||||
|
||||
#include <QNetworkReply>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QtGlobal>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
class QNetworkAccessManager;
|
||||
#include <QtNetwork>
|
||||
|
||||
class UpdateDownloader : public QObject
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,20 +2,15 @@
|
|||
|
||||
#include "../network/update/client/release_channel.h"
|
||||
#include "card_counter_settings.h"
|
||||
#include "libcockatrice/interfaces/interface_card_database_path_provider.h"
|
||||
#include "shortcuts_settings.h"
|
||||
#include "version_string.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QGlobalStatic>
|
||||
#include <QMessageLogger>
|
||||
#include <QSettings>
|
||||
#include <QSharedPointer>
|
||||
#include <QStandardPaths>
|
||||
#include <QVariant>
|
||||
#include <QtGlobal>
|
||||
#include <libcockatrice/settings/appearance_settings.h>
|
||||
#include <libcockatrice/settings/cache_storage_settings.h>
|
||||
#include <libcockatrice/settings/card_database_settings.h>
|
||||
|
|
@ -115,16 +110,6 @@ SettingsCache::SettingsCache()
|
|||
|
||||
settings = new QSettings(settingsPath + "global.ini", QSettings::IniFormat, this);
|
||||
|
||||
// Both migrations below overwrite per-domain files in place, so snapshot the whole settings
|
||||
// folder first: global.ini is preserved as global.ini.old, but pre-existing per-domain files
|
||||
// (and a re-migration after a downgrade) would otherwise have no way back.
|
||||
if (SettingsMigration::isMigrationPending(settingsPath)) {
|
||||
const QString backupPath = SettingsMigration::backupSettingsDirectory(settingsPath);
|
||||
if (!backupPath.isEmpty()) {
|
||||
qCInfo(SettingsCacheLog) << "[SettingsCache] Settings backed up before migration:" << backupPath;
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate from legacy NativeFormat settings to per-class INI files (runs at most once)
|
||||
if (!isPortableBuild) {
|
||||
SettingsMigration::migrateLegacySettings(settingsPath);
|
||||
|
|
@ -133,10 +118,6 @@ SettingsCache::SettingsCache()
|
|||
// Migrate settings from global.ini to per-class INI files (runs at most once)
|
||||
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);
|
||||
cardDatabaseSettings = new CardDatabaseSettings(settingsPath, this);
|
||||
serversSettings = new ServersSettings(settingsPath, this);
|
||||
|
|
|
|||
|
|
@ -7,18 +7,17 @@
|
|||
#ifndef SETTINGSCACHE_H
|
||||
#define SETTINGSCACHE_H
|
||||
|
||||
#include <QList>
|
||||
#include <QLoggingCategory>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <libcockatrice/interfaces/interface_card_database_path_provider.h>
|
||||
#include <libcockatrice/interfaces/interface_network_settings_provider.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <libcockatrice/utility/macros.h>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(SettingsCacheLog, "settings_cache");
|
||||
|
||||
class ReleaseChannel;
|
||||
|
||||
#define DEFAULT_LANG_NAME "English"
|
||||
#define CLIENT_INFO_NOT_SET "notset"
|
||||
|
||||
#define DEFAULT_FONT_SIZE 12
|
||||
|
|
@ -59,33 +58,33 @@ signals:
|
|||
void themeChanged();
|
||||
|
||||
private:
|
||||
QSettings *settings = nullptr;
|
||||
ShortcutsSettings *shortcutsSettings = nullptr;
|
||||
CardDatabaseSettings *cardDatabaseSettings = nullptr;
|
||||
ServersSettings *serversSettings = nullptr;
|
||||
MessageSettings *messageSettings = nullptr;
|
||||
GameFiltersSettings *gameFiltersSettings = nullptr;
|
||||
LayoutsSettings *layoutsSettings = nullptr;
|
||||
DownloadSettings *downloadSettings = nullptr;
|
||||
RecentsSettings *recentsSettings = nullptr;
|
||||
CardOverrideSettings *cardOverrideSettings = nullptr;
|
||||
DebugSettings *debugSettings = nullptr;
|
||||
CardCounterSettings *cardCounterSettings = nullptr;
|
||||
TabsSettings *tabsSettings = nullptr;
|
||||
SoundSettings *soundSettings = nullptr;
|
||||
GameSettings *gameSettings = nullptr;
|
||||
ChatSettings *chatSettings = nullptr;
|
||||
CacheStorageSettings *cacheStorageSettings = nullptr;
|
||||
UpdatesSettings *updatesSettings = nullptr;
|
||||
PersonalSettings *personalSettings = nullptr;
|
||||
CardsDisplaySettings *cardsDisplaySettings = nullptr;
|
||||
InterfaceSettings *interfaceSettings = nullptr;
|
||||
DeckEditorSettings *deckEditorSettings = nullptr;
|
||||
PathsSettings *pathsSettings = nullptr;
|
||||
VisualDeckStorageSettings *visualDeckStorageSettings = nullptr;
|
||||
AppearanceSettings *appearanceSettings = nullptr;
|
||||
NetworkSettings *networkSettings = nullptr;
|
||||
CommanderBracketSettings *commanderBracketSettings = nullptr;
|
||||
QSettings *settings;
|
||||
ShortcutsSettings *shortcutsSettings;
|
||||
CardDatabaseSettings *cardDatabaseSettings;
|
||||
ServersSettings *serversSettings;
|
||||
MessageSettings *messageSettings;
|
||||
GameFiltersSettings *gameFiltersSettings;
|
||||
LayoutsSettings *layoutsSettings;
|
||||
DownloadSettings *downloadSettings;
|
||||
RecentsSettings *recentsSettings;
|
||||
CardOverrideSettings *cardOverrideSettings;
|
||||
DebugSettings *debugSettings;
|
||||
CardCounterSettings *cardCounterSettings;
|
||||
TabsSettings *tabsSettings;
|
||||
SoundSettings *soundSettings;
|
||||
GameSettings *gameSettings;
|
||||
ChatSettings *chatSettings;
|
||||
CacheStorageSettings *cacheStorageSettings;
|
||||
UpdatesSettings *updatesSettings;
|
||||
PersonalSettings *personalSettings;
|
||||
CardsDisplaySettings *cardsDisplaySettings;
|
||||
InterfaceSettings *interfaceSettings;
|
||||
DeckEditorSettings *deckEditorSettings;
|
||||
PathsSettings *pathsSettings;
|
||||
VisualDeckStorageSettings *visualDeckStorageSettings;
|
||||
AppearanceSettings *appearanceSettings;
|
||||
NetworkSettings *networkSettings;
|
||||
CommanderBracketSettings *commanderBracketSettings;
|
||||
|
||||
QString themeName;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,8 @@
|
|||
#include "card_counter_settings.h"
|
||||
|
||||
#include "libcockatrice/settings/settings_manager.h"
|
||||
|
||||
#include <QChar>
|
||||
#include <QColor>
|
||||
#include <QSettings>
|
||||
#include <QVariant>
|
||||
#include <QtMath>
|
||||
#include <iterator>
|
||||
|
||||
class QObject;
|
||||
|
||||
CardCounterSettings::CardCounterSettings(const QString &settingsPath, QObject *parent)
|
||||
: SettingsManager(settingsPath + "card_counters.ini", "cards", "counters", parent)
|
||||
|
|
|
|||
|
|
@ -7,12 +7,10 @@
|
|||
#ifndef CARD_COUNTER_SETTINGS_H
|
||||
#define CARD_COUNTER_SETTINGS_H
|
||||
|
||||
#include <QString>
|
||||
#include <libcockatrice/settings/settings_manager.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
class QSettings;
|
||||
class QColor;
|
||||
class QObject;
|
||||
|
||||
class CardCounterSettings : public SettingsManager
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3,24 +3,7 @@
|
|||
#include "cache_settings.h"
|
||||
#include "shortcuts_settings.h"
|
||||
|
||||
#include <QAbstractItemModel>
|
||||
#include <QAbstractItemView>
|
||||
#include <QFont>
|
||||
#include <QHash>
|
||||
#include <QHeaderView>
|
||||
#include <QList>
|
||||
#include <QModelIndex>
|
||||
#include <QRegularExpression>
|
||||
#include <QSharedPointer>
|
||||
#include <QStandardItem>
|
||||
#include <QStandardItemModel>
|
||||
#include <QStringList>
|
||||
#include <QVariant>
|
||||
#include <algorithm>
|
||||
#include <qnamespace.h>
|
||||
|
||||
class QObject;
|
||||
class QWidget;
|
||||
|
||||
ShortcutFilterProxyModel::ShortcutFilterProxyModel(QObject *parent) : QSortFilterProxyModel(parent)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,13 +8,8 @@
|
|||
#define SHORTCUT_TREEVIEW_H
|
||||
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QString>
|
||||
#include <QStandardItemModel>
|
||||
#include <QTreeView>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
class QObject;
|
||||
class QStandardItemModel;
|
||||
class QWidget;
|
||||
|
||||
/**
|
||||
* Custom implementation of QSortFilterProxyModel that appends the source and parent strings together when filtering
|
||||
|
|
|
|||
|
|
@ -2,13 +2,9 @@
|
|||
|
||||
#include <QDebug>
|
||||
#include <QFile>
|
||||
#include <QMap>
|
||||
#include <QMessageBox>
|
||||
#include <QMessageLogger>
|
||||
#include <QSettings>
|
||||
#include <QSharedPointer>
|
||||
#include <QStringList>
|
||||
#include <QVariant>
|
||||
#include <utility>
|
||||
|
||||
ShortcutKey::ShortcutKey(const QString &_name, QList<QKeySequence> _sequence, ShortcutGroup::Groups _group)
|
||||
: QList<QKeySequence>(_sequence), name(_name), group(_group)
|
||||
|
|
|
|||
|
|
@ -8,18 +8,9 @@
|
|||
#define SHORTCUTSSETTINGS_H
|
||||
|
||||
#include <QApplication>
|
||||
#include <QByteArray>
|
||||
#include <QChar>
|
||||
#include <QHash>
|
||||
#include <QKeySequence>
|
||||
#include <QList>
|
||||
#include <QLoggingCategory>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QtGlobal>
|
||||
#include <qtmetamacros.h>
|
||||
#include <utility>
|
||||
#include <QSettings>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(ShortcutsSettingsLog, "shortcuts_settings");
|
||||
|
||||
|
|
|
|||
|
|
@ -4,15 +4,8 @@
|
|||
|
||||
#include <QApplication>
|
||||
#include <QAudioOutput>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QList>
|
||||
#include <QMediaPlayer>
|
||||
#include <QMessageLogger>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
#include <QtGlobal>
|
||||
#include <libcockatrice/settings/sound_settings.h>
|
||||
|
||||
#define DEFAULT_THEME_NAME "Default"
|
||||
|
|
@ -101,7 +94,7 @@ QStringMap &SoundEngine::getAvailableThemes()
|
|||
QDir dir;
|
||||
availableThemes.clear();
|
||||
|
||||
// Load themes from user profile dir
|
||||
// load themes from user profile dir
|
||||
|
||||
dir.setPath(SettingsCache::instance().getDataPath() + "/sounds");
|
||||
|
||||
|
|
@ -111,7 +104,7 @@ QStringMap &SoundEngine::getAvailableThemes()
|
|||
}
|
||||
}
|
||||
|
||||
// Load themes from Cockatrice system dir
|
||||
// load themes from cockatrice system dir
|
||||
dir.setPath(qApp->applicationDirPath() +
|
||||
#ifdef Q_OS_MAC
|
||||
"/../Resources/sounds"
|
||||
|
|
|
|||
|
|
@ -7,17 +7,17 @@
|
|||
#ifndef SOUNDENGINE_H
|
||||
#define SOUNDENGINE_H
|
||||
|
||||
#include <QAudioOutput>
|
||||
#include <QLoggingCategory>
|
||||
#include <QMap>
|
||||
#include <QMediaPlayer>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
class QAudioOutput;
|
||||
class QMediaPlayer;
|
||||
|
||||
inline Q_LOGGING_CATEGORY(SoundEngineLog, "sound_engine");
|
||||
|
||||
class QBuffer;
|
||||
|
||||
typedef QMap<QString, QString> QStringMap;
|
||||
|
||||
class SoundEngine : public QObject
|
||||
|
|
|
|||
|
|
@ -2,22 +2,22 @@
|
|||
#define COCKATRICE_SETTINGS_CARD_PREFERENCE_PROVIDER_H
|
||||
#include "../../client/settings/cache_settings.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <libcockatrice/interfaces/interface_card_preference_provider.h>
|
||||
#include <libcockatrice/settings/card_override_settings.h>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
|
||||
class SettingsCardPreferenceProvider : public QObject, public ICardPreferenceProvider
|
||||
class SettingsCardPreferenceProvider : public ICardPreferenceProvider
|
||||
{
|
||||
public:
|
||||
explicit SettingsCardPreferenceProvider(QObject *parent = nullptr) : QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
[[nodiscard]] QString getCardPreferenceOverride(const QString &cardName) const override
|
||||
{
|
||||
return SettingsCache::instance().cardOverrides().getCardPreferenceOverride(cardName);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool getIncludeRebalancedCards() const override
|
||||
{
|
||||
return SettingsCache::instance().cardsDisplay().getIncludeRebalancedCards();
|
||||
}
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_SETTINGS_CARD_PREFERENCE_PROVIDER_H
|
||||
|
|
|
|||
|
|
@ -1,27 +1,9 @@
|
|||
#include "deck_filter_string.h"
|
||||
|
||||
#include "libcockatrice/card/card_localization.h"
|
||||
#include "libcockatrice/card/database/card_database_querier.h"
|
||||
#include "libcockatrice/deck_list/deck_list.h"
|
||||
#include "libcockatrice/deck_list/tree/deck_list_card_node.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QDebug>
|
||||
#include <QFileInfo>
|
||||
#include <QList>
|
||||
#include <QMessageLogger>
|
||||
#include <QSharedPointer>
|
||||
#include <algorithm>
|
||||
#include <any>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/deck_list/loaded_deck.h>
|
||||
#include <libcockatrice/filters/filter_string.h>
|
||||
#include <libcockatrice/utility/peglib.h>
|
||||
#include <mutex>
|
||||
#include <qnamespace.h>
|
||||
#include <stddef.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
static peg::parser search(R"(
|
||||
Start <- QueryPartList
|
||||
|
|
@ -61,12 +43,6 @@ NumericValue <- [0-9]+
|
|||
|
||||
static std::once_flag init;
|
||||
|
||||
// The peglib parser is a single permanent object, so the rule actions below cannot see
|
||||
// per-instance state. The card language that the nested [[card name]] search matches
|
||||
// against is passed through this thread-local context, which is live only while a
|
||||
// DeckFilterString is being parsed, and copied into the nested FilterString closures.
|
||||
thread_local CardSearchLanguage deckSearchLanguageContext;
|
||||
|
||||
static void setupParserRules()
|
||||
{
|
||||
// plumbing
|
||||
|
|
@ -140,7 +116,7 @@ static void setupParserRules()
|
|||
|
||||
// actual functionality
|
||||
search["DeckContentQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||
auto cardFilter = FilterString(std::any_cast<QString>(sv[0]), deckSearchLanguageContext);
|
||||
auto cardFilter = FilterString(std::any_cast<QString>(sv[0]));
|
||||
auto numberMatcher = sv.size() > 1 ? std::any_cast<NumberMatcher>(sv[1]) : [](int count) { return count > 0; };
|
||||
|
||||
return [=](const DeckSearchData &data) -> bool {
|
||||
|
|
@ -210,7 +186,7 @@ DeckFilterString::DeckFilterString()
|
|||
_error = "Not initialized";
|
||||
}
|
||||
|
||||
DeckFilterString::DeckFilterString(const QString &expr, const CardSearchLanguage &searchLanguage)
|
||||
DeckFilterString::DeckFilterString(const QString &expr)
|
||||
{
|
||||
QByteArray ba = expr.simplified().toUtf8();
|
||||
|
||||
|
|
@ -223,8 +199,6 @@ DeckFilterString::DeckFilterString(const QString &expr, const CardSearchLanguage
|
|||
return;
|
||||
}
|
||||
|
||||
deckSearchLanguageContext = searchLanguage;
|
||||
|
||||
search.set_logger([&](size_t /*ln*/, size_t col, const std::string &msg) {
|
||||
_error = QString("Error at position %1: %2").arg(col).arg(QString::fromStdString(msg));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,14 +7,11 @@
|
|||
#ifndef DECK_FILTER_STRING_H
|
||||
#define DECK_FILTER_STRING_H
|
||||
|
||||
#include "../interface/deck_loader/loaded_deck.h"
|
||||
|
||||
#include <QLoggingCategory>
|
||||
#include <QString>
|
||||
#include <functional>
|
||||
#include <libcockatrice/card/card_localization.h>
|
||||
#include <libcockatrice/deck_list/loaded_deck.h>
|
||||
|
||||
struct CardSearchLanguage;
|
||||
struct LoadedDeck;
|
||||
|
||||
inline Q_LOGGING_CATEGORY(DeckFilterStringLog, "deck_filter_string");
|
||||
|
||||
|
|
@ -38,7 +35,7 @@ class DeckFilterString
|
|||
{
|
||||
public:
|
||||
DeckFilterString();
|
||||
explicit DeckFilterString(const QString &expr, const CardSearchLanguage &searchLanguage = {});
|
||||
explicit DeckFilterString(const QString &expr);
|
||||
bool check(const DeckSearchData &data) const
|
||||
{
|
||||
return filter(data);
|
||||
|
|
|
|||
|
|
@ -1,20 +1,11 @@
|
|||
#include "filter_builder.h"
|
||||
|
||||
#include "../interface/pixel_map_generator.h"
|
||||
#include "../interface/widgets/utility/custom_line_edit.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QGridLayout>
|
||||
#include <QIcon>
|
||||
#include <QPushButton>
|
||||
#include <QSizePolicy>
|
||||
#include <QString>
|
||||
#include <QStringLiteral>
|
||||
#include <QVariant>
|
||||
#include <QtGlobal>
|
||||
#include <libcockatrice/filters/filter_card.h>
|
||||
#include <libcockatrice/utility/qt_utils.h>
|
||||
#include <stddef.h>
|
||||
|
||||
FilterBuilder::FilterBuilder(QWidget *parent) : QWidget(parent)
|
||||
{
|
||||
|
|
@ -30,7 +21,7 @@ FilterBuilder::FilterBuilder(QWidget *parent) : QWidget(parent)
|
|||
typeCombo->addItem(CardFilter::typeName(static_cast<CardFilter::Type>(i)), QVariant(i));
|
||||
}
|
||||
|
||||
QPushButton *ok = new QPushButton(themePixmap(QStringLiteral("icons/increment")), QString());
|
||||
QPushButton *ok = new QPushButton(QPixmap("theme:icons/increment"), QString());
|
||||
ok->setObjectName("ok");
|
||||
ok->setMaximumSize(20, 20);
|
||||
|
||||
|
|
@ -40,8 +31,9 @@ FilterBuilder::FilterBuilder(QWidget *parent) : QWidget(parent)
|
|||
edit->setClearButtonEnabled(true);
|
||||
edit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
|
||||
QGridLayout *layout = QtUtils::makeLayout<QGridLayout>();
|
||||
QGridLayout *layout = new QGridLayout;
|
||||
layout->setObjectName("layout");
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
layout->addWidget(typeCombo, 0, 0, 1, 2);
|
||||
layout->addWidget(filterCombo, 0, 2, 1, 2);
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@
|
|||
#define FILTERBUILDER_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
class QCheckBox;
|
||||
class QComboBox;
|
||||
class LineEditUnfocusable;
|
||||
class CardFilter;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,8 @@
|
|||
#include "filter_tree_model.h"
|
||||
|
||||
#include <QFlags>
|
||||
#include <QFont>
|
||||
#include <QString>
|
||||
#include <QVariant>
|
||||
#include <functional>
|
||||
#include <libcockatrice/filters/filter_card.h>
|
||||
#include <libcockatrice/filters/filter_tree.h>
|
||||
#include <stddef.h>
|
||||
|
||||
class QObject;
|
||||
|
||||
FilterTreeModel::FilterTreeModel(QObject *parent) : QAbstractItemModel(parent)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,15 +8,11 @@
|
|||
#define FILTERTREEMODEL_H
|
||||
|
||||
#include <QAbstractItemModel>
|
||||
#include <QList>
|
||||
#include <QModelIndex>
|
||||
#include <libcockatrice/filters/filter_card.h>
|
||||
#include <qnamespace.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
class FilterTree;
|
||||
class CardFilter;
|
||||
class FilterTreeNode;
|
||||
class QObject;
|
||||
|
||||
class FilterTreeModel : public QAbstractItemModel
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,21 +1,8 @@
|
|||
#include "syntax_help.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QFile>
|
||||
#include <QFlags>
|
||||
#include <QIODevice>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageLogger>
|
||||
#include <QObject>
|
||||
#include <QRegularExpression>
|
||||
#include <QSharedPointer>
|
||||
#include <QSize>
|
||||
#include <QString>
|
||||
#include <QTextBrowser>
|
||||
#include <QTextDocument>
|
||||
#include <QTextStream>
|
||||
#include <QUrl>
|
||||
#include <qnamespace.h>
|
||||
|
||||
/**
|
||||
* Creates the card search syntax help window
|
||||
|
|
|
|||
|
|
@ -8,10 +8,9 @@
|
|||
#ifndef SEARCH_SYNTAX_HELP_H
|
||||
#define SEARCH_SYNTAX_HELP_H
|
||||
|
||||
#include <QLineEdit>
|
||||
#include <QLoggingCategory>
|
||||
|
||||
class QLineEdit;
|
||||
class QTextBrowser;
|
||||
#include <QTextBrowser>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(SyntaxHelpLog, "syntax_help");
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +1,14 @@
|
|||
#include "abstract_game.h"
|
||||
|
||||
#include "game_event_handler.h"
|
||||
#include "game_meta_info.h"
|
||||
#include "game_state.h"
|
||||
#include "libcockatrice/protocol/pb/game_replay.pb.h"
|
||||
#include "player/player_info.h"
|
||||
#include "../interface/widgets/tabs/tab_game.h"
|
||||
#include "player/player_logic.h"
|
||||
#include "player/player_manager.h"
|
||||
#include "zones/card_zone_logic.h"
|
||||
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
|
||||
class AbstractClient;
|
||||
|
||||
AbstractGame::AbstractGame(QObject *_parent) : QObject(_parent)
|
||||
{
|
||||
gameMetaInfo = new GameMetaInfo(this);
|
||||
gameEventHandler = new GameEventHandler(this);
|
||||
|
||||
activeCard = nullptr;
|
||||
}
|
||||
|
||||
bool AbstractGame::isHost() const
|
||||
|
|
@ -46,7 +37,12 @@ void AbstractGame::loadReplay(const GameReplay *replay)
|
|||
gameMetaInfo->setSpectatorsOmniscient(true);
|
||||
}
|
||||
|
||||
CardState *AbstractGame::getCard(int playerId, const QString &zoneName, int cardId) const
|
||||
void AbstractGame::setActiveCard(CardItem *card)
|
||||
{
|
||||
activeCard = card;
|
||||
}
|
||||
|
||||
CardItem *AbstractGame::getCard(int playerId, const QString &zoneName, int cardId) const
|
||||
{
|
||||
PlayerLogic *player = playerManager->getPlayer(playerId);
|
||||
if (!player) {
|
||||
|
|
@ -7,18 +7,15 @@
|
|||
#ifndef COCKATRICE_ABSTRACT_GAME_H
|
||||
#define COCKATRICE_ABSTRACT_GAME_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <qtmetamacros.h>
|
||||
#include "game_event_handler.h"
|
||||
#include "game_meta_info.h"
|
||||
#include "game_state.h"
|
||||
#include "player/player_manager.h"
|
||||
|
||||
class AbstractClient;
|
||||
class CardState;
|
||||
class GameEventHandler;
|
||||
class GameMetaInfo;
|
||||
class GameReplay;
|
||||
class GameState;
|
||||
class PlayerManager;
|
||||
class CardState;
|
||||
#include <QObject>
|
||||
#include <libcockatrice/protocol/pb/game_replay.pb.h>
|
||||
|
||||
class CardItem;
|
||||
class AbstractGame : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
|
@ -30,6 +27,7 @@ public:
|
|||
GameState *gameState;
|
||||
GameEventHandler *gameEventHandler;
|
||||
PlayerManager *playerManager;
|
||||
CardItem *activeCard;
|
||||
|
||||
GameMetaInfo *getGameMetaInfo()
|
||||
{
|
||||
|
|
@ -57,7 +55,13 @@ public:
|
|||
|
||||
void loadReplay(const GameReplay *replay);
|
||||
|
||||
CardState *getCard(int playerId, const QString &zoneName, int cardId) const;
|
||||
CardItem *getCard(int playerId, const QString &zoneName, int cardId) const;
|
||||
|
||||
void setActiveCard(CardItem *card);
|
||||
CardItem *getActiveCard() const
|
||||
{
|
||||
return activeCard;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_ABSTRACT_GAME_H
|
||||
|
|
@ -1,9 +1,6 @@
|
|||
#include "arrow_registry.h"
|
||||
|
||||
#include "arrow_item.h"
|
||||
|
||||
#include <QSharedPointer>
|
||||
#include <libcockatrice/game/board/arrow_data.h>
|
||||
#include "../game_graphics/board/arrow_item.h"
|
||||
|
||||
void ArrowRegistry::insert(QSharedPointer<ArrowData> data, ArrowItem *arrow)
|
||||
{
|
||||
|
|
@ -1,14 +1,13 @@
|
|||
#ifndef COCKATRICE_ARROW_REGISTRY_H
|
||||
#define COCKATRICE_ARROW_REGISTRY_H
|
||||
|
||||
#include <QList>
|
||||
#include "board/arrow_data.h"
|
||||
|
||||
#include <QMap>
|
||||
#include <QSet>
|
||||
#include <libcockatrice/game/board/arrow_data.h>
|
||||
#include <QSharedPointer>
|
||||
|
||||
class ArrowItem;
|
||||
struct ArrowData;
|
||||
template <class T> class QSharedPointer;
|
||||
|
||||
struct ArrowKey
|
||||
{
|
||||
|
|
@ -1,8 +1,5 @@
|
|||
#include "arrow_data.h"
|
||||
|
||||
#include "libcockatrice/protocol/pb/serverinfo_arrow.pb.h"
|
||||
#include "libcockatrice/utility/color.h"
|
||||
|
||||
ArrowData ArrowData::fromProto(const ServerInfo_Arrow &arrow, int creatorId, bool isLocalCreator)
|
||||
{
|
||||
ArrowData data;
|
||||
|
|
@ -3,8 +3,8 @@
|
|||
|
||||
#include <QColor>
|
||||
#include <QString>
|
||||
|
||||
class ServerInfo_Arrow;
|
||||
#include <libcockatrice/protocol/pb/serverinfo_arrow.pb.h>
|
||||
#include <libcockatrice/utility/color.h>
|
||||
|
||||
struct ArrowData
|
||||
{
|
||||
|
|
@ -1,35 +1,31 @@
|
|||
#include "card_list.h"
|
||||
|
||||
#include "card_state.h"
|
||||
#include "../../game_graphics/board/card_item.h"
|
||||
|
||||
#include <QChar>
|
||||
#include <QDebug>
|
||||
#include <QMessageLogger>
|
||||
#include <QSharedPointer>
|
||||
#include <algorithm>
|
||||
#include <compare>
|
||||
#include <libcockatrice/card/card_info.h>
|
||||
|
||||
CardList::CardList(bool _contentsKnown) : QList<CardState *>(), contentsKnown(_contentsKnown)
|
||||
CardList::CardList(bool _contentsKnown) : QList<CardItem *>(), contentsKnown(_contentsKnown)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Finds the CardState with the given id in the list.
|
||||
* @brief Finds the CardItem with the given id in the list.
|
||||
* If contentsKnown is false, then this just returns the first element of the list.
|
||||
*
|
||||
* @param cardId The id of the card to find.
|
||||
*
|
||||
* @returns A pointer to the CardState, or a nullptr if not found.
|
||||
* @returns A pointer to the CardItem, or a nullptr if not found.
|
||||
*/
|
||||
CardState *CardList::findCard(const int cardId) const
|
||||
CardItem *CardList::findCard(const int cardId) const
|
||||
{
|
||||
if (!contentsKnown && !empty()) {
|
||||
return at(0);
|
||||
} else {
|
||||
for (auto *cardState : *this) {
|
||||
if (cardState->getId() == cardId) {
|
||||
return cardState;
|
||||
for (auto *cardItem : *this) {
|
||||
if (cardItem->getId() == cardId) {
|
||||
return cardItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -37,7 +33,7 @@ CardState *CardList::findCard(const int cardId) const
|
|||
}
|
||||
|
||||
/**
|
||||
* @brief sorts the list by using string comparison on properties extracted from the CardState
|
||||
* @brief sorts the list by using string comparison on properties extracted from the CardItem
|
||||
* The cards are compared using each property in order.
|
||||
* If two cards have the same value for a property, then the next property in the list is used.
|
||||
*
|
||||
|
|
@ -50,7 +46,7 @@ void CardList::sortBy(const QList<SortOption> &option)
|
|||
return;
|
||||
}
|
||||
|
||||
auto comparator = [&option](CardState *a, CardState *b) {
|
||||
auto comparator = [&option](CardItem *a, CardItem *b) {
|
||||
for (auto prop : option) {
|
||||
auto extractor = getExtractorFor(prop);
|
||||
QString t1 = extractor(a);
|
||||
|
|
@ -115,26 +111,26 @@ static QString getColorSortString(const CardInfo &c, bool appendAtEnd)
|
|||
}
|
||||
|
||||
/**
|
||||
* @brief returns the function that extracts the given property from the CardState.
|
||||
* @brief returns the function that extracts the given property from the CardItem.
|
||||
*/
|
||||
std::function<QString(CardState *)> CardList::getExtractorFor(SortOption option)
|
||||
std::function<QString(CardItem *)> CardList::getExtractorFor(SortOption option)
|
||||
{
|
||||
switch (option) {
|
||||
case NoSort:
|
||||
return [](CardState *) { return ""; };
|
||||
return [](CardItem *) { return ""; };
|
||||
case SortByMainType:
|
||||
return [](CardState *c) { return c->getCardInfo().getMainCardType(); };
|
||||
return [](CardItem *c) { return c->getCardInfo().getMainCardType(); };
|
||||
case SortByManaValue:
|
||||
// getCmc returns the int as a string. We pad with 0's so that string comp also works on it
|
||||
return [](CardState *c) { return c->getCard() ? c->getCardInfo().getCmc().rightJustified(4, '0') : ""; };
|
||||
return [](CardItem *c) { return c->getCard() ? c->getCardInfo().getCmc().rightJustified(4, '0') : ""; };
|
||||
case SortByColorGrouping:
|
||||
return [](CardState *c) { return c->getCard() ? getColorSortString(c->getCardInfo(), false) : ""; };
|
||||
return [](CardItem *c) { return c->getCard() ? getColorSortString(c->getCardInfo(), false) : ""; };
|
||||
case SortByName:
|
||||
return [](CardState *c) { return c->getName(); };
|
||||
return [](CardItem *c) { return c->getName(); };
|
||||
case SortByType:
|
||||
return [](CardState *c) { return c->getCardInfo().getCardType(); };
|
||||
return [](CardItem *c) { return c->getCardInfo().getCardType(); };
|
||||
case SortByManaCost:
|
||||
return [](CardState *c) {
|
||||
return [](CardItem *c) {
|
||||
if (!c->getCard()) {
|
||||
return QString();
|
||||
}
|
||||
|
|
@ -146,18 +142,18 @@ std::function<QString(CardState *)> CardList::getExtractorFor(SortOption option)
|
|||
return QString("%1%2").arg(info.getCmc(), 4, QChar('0')).arg(info.getManaCost());
|
||||
};
|
||||
case SortByColors:
|
||||
return [](CardState *c) { return c->getCard() ? getColorSortString(c->getCardInfo(), true) : ""; };
|
||||
return [](CardItem *c) { return c->getCard() ? getColorSortString(c->getCardInfo(), true) : ""; };
|
||||
case SortByPt:
|
||||
// do the same padding trick as above
|
||||
return
|
||||
[](CardState *c) { return c->getCard() ? c->getCardInfo().getPowTough().rightJustified(10, '0') : ""; };
|
||||
[](CardItem *c) { return c->getCard() ? c->getCardInfo().getPowTough().rightJustified(10, '0') : ""; };
|
||||
case SortBySet:
|
||||
return [](CardState *c) { return c->getCardInfo().getSetsNames(); };
|
||||
return [](CardItem *c) { return c->getCardInfo().getSetsNames(); };
|
||||
case SortByPrinting:
|
||||
return [](CardState *c) { return c->getProviderId(); };
|
||||
return [](CardItem *c) { return c->getProviderId(); };
|
||||
}
|
||||
|
||||
// this line should never be reached
|
||||
qCWarning(CardListLog) << "cardlist.cpp: Could not find extractor for SortOption" << option;
|
||||
return [](CardState *) { return ""; };
|
||||
return [](CardItem *) { return ""; };
|
||||
}
|
||||
|
|
@ -9,14 +9,12 @@
|
|||
|
||||
#include <QList>
|
||||
#include <QLoggingCategory>
|
||||
#include <QString>
|
||||
#include <functional>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(CardListLog, "card_list");
|
||||
|
||||
class CardState;
|
||||
class CardItem;
|
||||
|
||||
class CardList : public QList<CardState *>
|
||||
class CardList : public QList<CardItem *>
|
||||
{
|
||||
protected:
|
||||
bool contentsKnown;
|
||||
|
|
@ -43,7 +41,7 @@ public:
|
|||
SortByPrinting
|
||||
};
|
||||
explicit CardList(bool _contentsKnown);
|
||||
CardState *findCard(const int cardId) const;
|
||||
CardItem *findCard(const int cardId) const;
|
||||
bool getContentsKnown() const
|
||||
{
|
||||
return contentsKnown;
|
||||
|
|
@ -51,7 +49,7 @@ public:
|
|||
|
||||
void sortBy(const QList<SortOption> &options);
|
||||
|
||||
static std::function<QString(CardState *)> getExtractorFor(SortOption option);
|
||||
static std::function<QString(CardItem *)> getExtractorFor(SortOption option);
|
||||
};
|
||||
|
||||
#endif
|
||||
111
cockatrice/src/game/board/card_state.cpp
Normal file
111
cockatrice/src/game/board/card_state.cpp
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
#include "card_state.h"
|
||||
|
||||
void CardState::resetState(bool keepAnnotations)
|
||||
{
|
||||
attacking = false;
|
||||
counters.clear();
|
||||
pt.clear();
|
||||
if (!keepAnnotations) {
|
||||
annotation.clear();
|
||||
}
|
||||
attachedTo = nullptr;
|
||||
}
|
||||
|
||||
void CardState::setZone(CardZoneLogic *_zone)
|
||||
{
|
||||
if (zone == _zone) {
|
||||
return;
|
||||
}
|
||||
|
||||
zone = _zone;
|
||||
emit zoneChanged(this, zone);
|
||||
emit stateChanged();
|
||||
}
|
||||
|
||||
void CardState::setAttacking(bool _attacking)
|
||||
{
|
||||
if (attacking == _attacking) {
|
||||
return;
|
||||
}
|
||||
attacking = _attacking;
|
||||
emit attackingChanged(_attacking);
|
||||
emit stateChanged();
|
||||
}
|
||||
|
||||
void CardState::insertCounter(int id, int value)
|
||||
{
|
||||
counters.insert(id, value);
|
||||
|
||||
emit countersChanged(counters);
|
||||
emit stateChanged();
|
||||
}
|
||||
|
||||
void CardState::setCounter(int id, int value)
|
||||
{
|
||||
if (value) {
|
||||
counters[id] = value;
|
||||
} else {
|
||||
counters.remove(id);
|
||||
}
|
||||
|
||||
emit countersChanged(counters);
|
||||
emit stateChanged();
|
||||
}
|
||||
|
||||
void CardState::clearCounters()
|
||||
{
|
||||
counters.clear();
|
||||
emit countersChanged(counters);
|
||||
emit stateChanged();
|
||||
}
|
||||
|
||||
void CardState::setAnnotation(const QString &_annotation)
|
||||
{
|
||||
if (annotation == _annotation) {
|
||||
return;
|
||||
}
|
||||
annotation = _annotation;
|
||||
emit annotationChanged(annotation);
|
||||
emit stateChanged();
|
||||
}
|
||||
|
||||
void CardState::setPT(const QString &_pt)
|
||||
{
|
||||
if (pt == _pt) {
|
||||
return;
|
||||
}
|
||||
pt = _pt;
|
||||
emit ptChanged(pt);
|
||||
emit stateChanged();
|
||||
}
|
||||
|
||||
void CardState::setDoesntUntap(bool _doesntUntap)
|
||||
{
|
||||
if (doesntUntap == _doesntUntap) {
|
||||
return;
|
||||
}
|
||||
doesntUntap = _doesntUntap;
|
||||
emit doesntUntapChanged(_doesntUntap);
|
||||
emit stateChanged();
|
||||
}
|
||||
|
||||
void CardState::setDestroyOnZoneChange(bool _destroyOnZoneChange)
|
||||
{
|
||||
if (destroyOnZoneChange == _destroyOnZoneChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
destroyOnZoneChange = _destroyOnZoneChange;
|
||||
emit destroyOnZoneChangeChanged(_destroyOnZoneChange);
|
||||
emit stateChanged();
|
||||
}
|
||||
|
||||
void CardState::setAttachedTo(CardItem *_attachedTo)
|
||||
{
|
||||
if (attachedTo == _attachedTo) {
|
||||
return;
|
||||
}
|
||||
attachedTo = _attachedTo;
|
||||
emit attachedToChanged(_attachedTo);
|
||||
emit stateChanged();
|
||||
}
|
||||
103
cockatrice/src/game/board/card_state.h
Normal file
103
cockatrice/src/game/board/card_state.h
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
#ifndef COCKATRICE_CARD_STATE_H
|
||||
#define COCKATRICE_CARD_STATE_H
|
||||
|
||||
#include <QMap>
|
||||
#include <QObject>
|
||||
|
||||
class CardZoneLogic;
|
||||
class CardItem;
|
||||
class CardState : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
bool attacking = false;
|
||||
QMap<int, int> counters;
|
||||
QString annotation;
|
||||
QString pt;
|
||||
bool doesntUntap = false;
|
||||
bool destroyOnZoneChange = false;
|
||||
|
||||
CardItem *attachedTo = nullptr;
|
||||
CardZoneLogic *zone = nullptr;
|
||||
|
||||
signals:
|
||||
void stateChanged();
|
||||
|
||||
void attackingChanged(bool newValue);
|
||||
void countersChanged(const QMap<int, int> &newCounters);
|
||||
void annotationChanged(const QString &newAnnotation);
|
||||
void ptChanged(const QString &newPt);
|
||||
void doesntUntapChanged(bool newValue);
|
||||
void destroyOnZoneChangeChanged(bool newValue);
|
||||
void attachedToChanged(CardItem *newAttachedTo);
|
||||
void zoneChanged(CardState *changedCard, CardZoneLogic *newZone);
|
||||
|
||||
public:
|
||||
explicit CardState(QObject *parent, CardZoneLogic *_zone) : QObject(parent), zone(_zone)
|
||||
{
|
||||
}
|
||||
|
||||
void resetState(bool keepAnnotations);
|
||||
|
||||
CardZoneLogic *getZone() const
|
||||
{
|
||||
return zone;
|
||||
}
|
||||
|
||||
void setZone(CardZoneLogic *_zone);
|
||||
|
||||
bool getAttacking() const
|
||||
{
|
||||
return attacking;
|
||||
}
|
||||
void setAttacking(bool _attacking);
|
||||
|
||||
const QMap<int, int> &getCounters() const
|
||||
{
|
||||
return counters;
|
||||
}
|
||||
|
||||
void insertCounter(int id, int value);
|
||||
|
||||
void setCounter(int id, int value);
|
||||
|
||||
void clearCounters();
|
||||
|
||||
QString getAnnotation() const
|
||||
{
|
||||
return annotation;
|
||||
}
|
||||
|
||||
void setAnnotation(const QString &_annotation);
|
||||
|
||||
QString getPT() const
|
||||
{
|
||||
return pt;
|
||||
}
|
||||
|
||||
void setPT(const QString &_pt);
|
||||
|
||||
bool getDoesntUntap() const
|
||||
{
|
||||
return doesntUntap;
|
||||
}
|
||||
|
||||
void setDoesntUntap(bool _doesntUntap);
|
||||
|
||||
bool getDestroyOnZoneChange() const
|
||||
{
|
||||
return destroyOnZoneChange;
|
||||
}
|
||||
|
||||
void setDestroyOnZoneChange(bool _destroyOnZoneChange);
|
||||
|
||||
CardItem *getAttachedTo() const
|
||||
{
|
||||
return attachedTo;
|
||||
}
|
||||
|
||||
void setAttachedTo(CardItem *_attachedTo);
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_CARD_STATE_H
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
#include "counter_state.h"
|
||||
|
||||
#include "libcockatrice/protocol/pb/serverinfo_counter.pb.h"
|
||||
|
||||
#include <libcockatrice/utility/color.h>
|
||||
|
||||
CounterState::CounterState(int id, const QString &name, const QColor &color, int radius, int value, QObject *parent)
|
||||
|
|
@ -15,12 +13,12 @@ CounterState *CounterState::fromProto(const ServerInfo_Counter &counter, QObject
|
|||
convertColorToQColor(counter.counter_color()), counter.radius(), counter.count(), parent);
|
||||
}
|
||||
|
||||
void CounterState::setValue(int newValue, bool skipDamageAnimation)
|
||||
void CounterState::setValue(int newValue)
|
||||
{
|
||||
if (newValue == value) {
|
||||
return;
|
||||
}
|
||||
int old = value;
|
||||
value = newValue;
|
||||
emit valueChanged(old, newValue, skipDamageAnimation);
|
||||
emit valueChanged(old, newValue);
|
||||
}
|
||||
|
|
@ -4,9 +4,7 @@
|
|||
#include <QColor>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
class ServerInfo_Counter;
|
||||
#include <libcockatrice/protocol/pb/serverinfo_counter.pb.h>
|
||||
|
||||
class CounterState : public QObject
|
||||
{
|
||||
|
|
@ -37,23 +35,10 @@ public:
|
|||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set the counter value.
|
||||
* @param newValue The new value.
|
||||
* @param skipDamageAnimation When true, valueChanged is emitted with skipDamageAnimation=true, letting views
|
||||
* suppress damage-related feedback (e.g. battlefield shimmer, life counter flash) for values set during replay
|
||||
* rewinds.
|
||||
*/
|
||||
void setValue(int newValue, bool skipDamageAnimation = false);
|
||||
void setValue(int newValue);
|
||||
|
||||
signals:
|
||||
/**
|
||||
* @brief Emitted whenever the value changes.
|
||||
* @param oldValue The previous value.
|
||||
* @param newValue The new value.
|
||||
* @param skipDamageAnimation True when the change should not trigger damage/life-change feedback in views.
|
||||
*/
|
||||
void valueChanged(int oldValue, int newValue, bool skipDamageAnimation);
|
||||
void valueChanged(int oldValue, int newValue);
|
||||
|
||||
private:
|
||||
int id;
|
||||
|
|
@ -1,31 +1,20 @@
|
|||
#include "game.h"
|
||||
|
||||
#include "game_meta_info.h"
|
||||
#include "game_state.h"
|
||||
#include "player/player_manager.h"
|
||||
#include "../interface/widgets/tabs/tab_game.h"
|
||||
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
#include <libcockatrice/protocol/pb/event_game_joined.pb.h>
|
||||
|
||||
class AbstractClient;
|
||||
class QObject;
|
||||
|
||||
Game::Game(QObject *_parent,
|
||||
bool isLocalGame,
|
||||
QList<AbstractClient *> &_clients,
|
||||
const Event_GameJoined &event,
|
||||
const QMap<int, QString> &_roomGameTypes,
|
||||
IInterfaceSettingsProvider *interfaceSettings,
|
||||
ICardPreferenceProvider *cardPreferences)
|
||||
const QMap<int, QString> &_roomGameTypes)
|
||||
: AbstractGame(_parent)
|
||||
{
|
||||
gameMetaInfo->setFromProto(event.game_info());
|
||||
gameMetaInfo->setRoomGameTypes(_roomGameTypes);
|
||||
gameState = new GameState(this, 0, event.host_id(), isLocalGame, _clients, false, event.resuming(), -1, false);
|
||||
connect(gameMetaInfo, &GameMetaInfo::startedChanged, gameState, &GameState::onStartedChanged);
|
||||
playerManager = new PlayerManager(this, event.player_id(), event.judge(), event.spectator(), interfaceSettings,
|
||||
cardPreferences);
|
||||
playerManager = new PlayerManager(this, event.player_id(), event.judge(), event.spectator());
|
||||
gameMetaInfo->setStarted(false);
|
||||
}
|
||||
26
cockatrice/src/game/game.h
Normal file
26
cockatrice/src/game/game.h
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* @file game.h
|
||||
* @ingroup GameLogic
|
||||
*/
|
||||
//! \todo Document this file.
|
||||
|
||||
#ifndef COCKATRICE_GAME_H
|
||||
#define COCKATRICE_GAME_H
|
||||
|
||||
#include "abstract_game.h"
|
||||
|
||||
#include <QObject>
|
||||
|
||||
class Game : public AbstractGame
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
Game(QObject *parent,
|
||||
bool isLocalGame,
|
||||
QList<AbstractClient *> &_clients,
|
||||
const Event_GameJoined &event,
|
||||
const QMap<int, QString> &_roomGameTypes);
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_GAME_H
|
||||
|
|
@ -1,46 +1,18 @@
|
|||
#include "game_event_handler.h"
|
||||
|
||||
#include "../game_graphics/log/message_log_widget.h"
|
||||
#include "../interface/widgets/tabs/tab_game.h"
|
||||
#include "abstract_game.h"
|
||||
#include "game_meta_info.h"
|
||||
#include "game_state.h"
|
||||
#include "libcockatrice/protocol/pb/commands.pb.h"
|
||||
#include "libcockatrice/protocol/pb/game_commands.pb.h"
|
||||
#include "libcockatrice/protocol/pb/game_event.pb.h"
|
||||
#include "libcockatrice/protocol/pb/game_event_context.pb.h"
|
||||
#include "libcockatrice/protocol/pb/response.pb.h"
|
||||
#include "libcockatrice/protocol/pb/serverinfo_game.pb.h"
|
||||
#include "libcockatrice/protocol/pb/serverinfo_player.pb.h"
|
||||
#include "libcockatrice/protocol/pb/serverinfo_playerproperties.pb.h"
|
||||
#include "libcockatrice/protocol/pb/serverinfo_user.pb.h"
|
||||
#include "player/player_event_handler.h"
|
||||
#include "player/player_info.h"
|
||||
#include "player/player_logic.h"
|
||||
#include "player/player_manager.h"
|
||||
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QMapIterator>
|
||||
#include <QMessageLogger>
|
||||
#include <QSharedPointer>
|
||||
#include <google/protobuf/descriptor.h>
|
||||
#include <google/protobuf/message.h>
|
||||
#include <google/protobuf/stubs/port.h>
|
||||
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||
#include <libcockatrice/protocol/get_pb_extension.h>
|
||||
#include <libcockatrice/protocol/pb/command_concede.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_select.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_delete_arrow.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_draw_cards.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_game_say.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_leave_game.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_next_turn.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_ready_start.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_reverse_turn.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_active_phase.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_card_attr.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_playmat.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_sideboard_lock.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_sideboard_plan.pb.h>
|
||||
#include <libcockatrice/protocol/pb/context_connection_state_changed.pb.h>
|
||||
#include <libcockatrice/protocol/pb/context_deck_select.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_game_closed.pb.h>
|
||||
|
|
@ -56,9 +28,6 @@
|
|||
#include <libcockatrice/protocol/pb/event_set_active_player.pb.h>
|
||||
#include <libcockatrice/protocol/pb/game_event_container.pb.h>
|
||||
#include <libcockatrice/protocol/pending_command.h>
|
||||
#include <libcockatrice/utility/zone_names.h>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
GameEventHandler::GameEventHandler(AbstractGame *_game) : QObject(_game), game(_game)
|
||||
{
|
||||
|
|
@ -232,89 +201,6 @@ void GameEventHandler::handleActivePhaseChanged(int phase)
|
|||
sendGameCommand(cmd);
|
||||
}
|
||||
|
||||
void GameEventHandler::handleUntapAll()
|
||||
{
|
||||
Command_SetCardAttr cmd;
|
||||
cmd.set_zone(ZoneNames::TABLE);
|
||||
cmd.set_attribute(AttrTapped);
|
||||
cmd.set_attr_value("0");
|
||||
|
||||
sendGameCommand(cmd);
|
||||
}
|
||||
|
||||
void GameEventHandler::handleDrawCard()
|
||||
{
|
||||
Command_DrawCards cmd;
|
||||
cmd.set_number(1);
|
||||
|
||||
sendGameCommand(cmd);
|
||||
}
|
||||
|
||||
PendingCommand *GameEventHandler::prepareDeckSelect(const QString &deckList)
|
||||
{
|
||||
Command_DeckSelect cmd;
|
||||
cmd.set_deck(deckList.toStdString());
|
||||
|
||||
return prepareGameCommand(cmd);
|
||||
}
|
||||
|
||||
PendingCommand *GameEventHandler::prepareDeckSelectId(int deckId)
|
||||
{
|
||||
Command_DeckSelect cmd;
|
||||
cmd.set_deck_id(deckId);
|
||||
|
||||
return prepareGameCommand(cmd);
|
||||
}
|
||||
|
||||
PendingCommand *GameEventHandler::prepareSetPlaymat(const PlaymatInfo &playmat)
|
||||
{
|
||||
Command_SetPlaymat cmd;
|
||||
auto *pp = cmd.mutable_playmat_params();
|
||||
pp->set_card_name(playmat.card.name.toStdString());
|
||||
pp->set_card_provider_id(playmat.card.providerId.toStdString());
|
||||
pp->set_margin_pct_l(playmat.params.marginPctL);
|
||||
pp->set_margin_pct_r(playmat.params.marginPctR);
|
||||
pp->set_vertical_offset(playmat.params.verticalOffset);
|
||||
pp->set_zoom(playmat.params.zoom);
|
||||
|
||||
return prepareGameCommand(cmd);
|
||||
}
|
||||
|
||||
void GameEventHandler::sendReadyStart(bool ready, int playerId)
|
||||
{
|
||||
Command_ReadyStart cmd;
|
||||
cmd.set_ready(ready);
|
||||
|
||||
sendGameCommand(cmd, playerId);
|
||||
}
|
||||
|
||||
void GameEventHandler::sendForceStart(int playerId)
|
||||
{
|
||||
Command_ReadyStart cmd;
|
||||
cmd.set_force_start(true);
|
||||
cmd.set_ready(true);
|
||||
|
||||
sendGameCommand(cmd, playerId);
|
||||
}
|
||||
|
||||
void GameEventHandler::sendSetSideboardLock(bool locked, int playerId)
|
||||
{
|
||||
Command_SetSideboardLock cmd;
|
||||
cmd.set_locked(locked);
|
||||
|
||||
sendGameCommand(cmd, playerId);
|
||||
}
|
||||
|
||||
void GameEventHandler::sendSetSideboardPlan(const QList<MoveCard_ToZone> &plan, int playerId)
|
||||
{
|
||||
Command_SetSideboardPlan cmd;
|
||||
for (const auto &zone : plan) {
|
||||
cmd.add_move_list()->CopyFrom(zone);
|
||||
}
|
||||
|
||||
sendGameCommand(cmd, playerId);
|
||||
}
|
||||
|
||||
void GameEventHandler::handleGameLeft()
|
||||
{
|
||||
sendGameCommand(Command_LeaveGame());
|
||||
|
|
@ -369,6 +255,8 @@ void GameEventHandler::eventSpectatorLeave(const Event_Leave &event,
|
|||
emit spectatorLeft(eventPlayerId);
|
||||
|
||||
game->getPlayerManager()->removeSpectator(eventPlayerId);
|
||||
|
||||
emitUserEvent();
|
||||
}
|
||||
|
||||
void GameEventHandler::eventGameStateChanged(const Event_GameStateChanged &event,
|
||||
|
|
@ -542,22 +430,21 @@ void GameEventHandler::eventJoin(const Event_Join &event, int /*eventPlayerId*/,
|
|||
QString playerName = QString::fromStdString(playerInfo.user_info().name());
|
||||
emit addPlayerToAutoCompleteList(playerName);
|
||||
|
||||
PlayerManager *playerManager = game->getPlayerManager();
|
||||
if (playerManager->getPlayers().contains(playerId) || playerManager->getSpectators().contains(playerId)) {
|
||||
if (game->getPlayerManager()->getPlayers().contains(playerId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (playerInfo.spectator()) {
|
||||
playerManager->addSpectator(playerId, playerInfo);
|
||||
game->getPlayerManager()->addSpectator(playerId, playerInfo);
|
||||
emit logJoinSpectator(playerName);
|
||||
emit spectatorJoined(playerInfo);
|
||||
} else {
|
||||
PlayerLogic *newPlayer = game->getPlayerManager()->addPlayer(playerId, playerInfo.user_info());
|
||||
emit logJoinPlayer(newPlayer);
|
||||
emit playerJoined(playerInfo);
|
||||
|
||||
emitUserEvent();
|
||||
}
|
||||
|
||||
emitUserEvent();
|
||||
}
|
||||
|
||||
QString GameEventHandler::getLeaveReason(Event_Leave::LeaveReason reason)
|
||||
|
|
@ -19,30 +19,23 @@
|
|||
|
||||
#include <QLoggingCategory>
|
||||
#include <QObject>
|
||||
#include <QPair>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
#include <libcockatrice/protocol/pb/event_leave.pb.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
// IWYU pragma: keep
|
||||
// GameEventContext, ServerInfo_Player and ServerInfo_User are passed by value
|
||||
// to signals and slots, so the moc-generated code needs the complete types.
|
||||
#include <libcockatrice/protocol/pb/game_event_context.pb.h>
|
||||
#include <libcockatrice/protocol/pb/move_card_to_zone.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_player.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
|
||||
#include <libcockatrice/utility/playmat_params.h>
|
||||
|
||||
class AbstractClient;
|
||||
class AbstractGame;
|
||||
class CommandContainer;
|
||||
class GameCommand;
|
||||
class GameEventContainer;
|
||||
class GameEventContext;
|
||||
class PendingCommand;
|
||||
class PlayerLogic;
|
||||
class Response;
|
||||
|
||||
class Event_GameStateChanged;
|
||||
class Event_PlayerPropertiesChanged;
|
||||
class Event_Join;
|
||||
class Event_Leave;
|
||||
class Event_GameHostChanged;
|
||||
class Event_GameClosed;
|
||||
class Event_SetActivePlayer;
|
||||
|
|
@ -51,15 +44,6 @@ class Event_GameSay;
|
|||
class Event_Kicked;
|
||||
class Event_ReverseTurn;
|
||||
class Event_Ping;
|
||||
class ServerInfo_PlayerProperties;
|
||||
namespace google
|
||||
{
|
||||
namespace protobuf
|
||||
{
|
||||
class Message;
|
||||
} // namespace protobuf
|
||||
} // namespace google
|
||||
template <typename T> class QList;
|
||||
|
||||
inline Q_LOGGING_CATEGORY(GameEventHandlerLog, "game_event_handler");
|
||||
|
||||
|
|
@ -128,12 +112,6 @@ public:
|
|||
*/
|
||||
void handleActivePhaseChanged(int phase);
|
||||
|
||||
/** @brief Request untapping every permanent on the table. */
|
||||
void handleUntapAll();
|
||||
|
||||
/** @brief Request drawing a single card. */
|
||||
void handleDrawCard();
|
||||
|
||||
/** @brief Leave the current game session. */
|
||||
void handleGameLeft();
|
||||
|
||||
|
|
@ -152,70 +130,6 @@ public:
|
|||
void handleArrowDeletion(int creatorId, int arrowId);
|
||||
void handleArrowDeletionFinished(const Response &response, int creatorId, int arrowId);
|
||||
|
||||
/**
|
||||
* @brief Prepare a deck selection from its native deck list text.
|
||||
*
|
||||
* The returned command is not sent yet, so the caller can connect to
|
||||
* PendingCommand::finished before handing it to sendGameCommand().
|
||||
*
|
||||
* @param deckList Serialized deck list.
|
||||
* @return The pending command awaiting the deck download response.
|
||||
*/
|
||||
PendingCommand *prepareDeckSelect(const QString &deckList);
|
||||
|
||||
/**
|
||||
* @brief Prepare a deck selection for a deck stored on the server.
|
||||
*
|
||||
* The returned command is not sent yet, so the caller can connect to
|
||||
* PendingCommand::finished before handing it to sendGameCommand().
|
||||
*
|
||||
* @param deckId Server-side deck identifier.
|
||||
* @return The pending command awaiting the deck download response.
|
||||
*/
|
||||
PendingCommand *prepareDeckSelectId(int deckId);
|
||||
|
||||
/**
|
||||
* @brief Build the playmat command for a player without sending it.
|
||||
*
|
||||
* The returned command is not sent yet, so the caller can connect to
|
||||
* PendingCommand::finished before handing it to sendGameCommand().
|
||||
*
|
||||
* @param playmat Resolved playmat card and positioning parameters.
|
||||
* @return The pending command awaiting the playmat response.
|
||||
*/
|
||||
PendingCommand *prepareSetPlaymat(const PlaymatInfo &playmat);
|
||||
|
||||
/**
|
||||
* @brief Set a player's ready state.
|
||||
*
|
||||
* @param ready Whether the player is ready.
|
||||
* @param playerId Player the state applies to.
|
||||
*/
|
||||
void sendReadyStart(bool ready, int playerId);
|
||||
|
||||
/**
|
||||
* @brief Force-start the game, kicking every player who is not ready.
|
||||
*
|
||||
* @param playerId Player issuing the force start.
|
||||
*/
|
||||
void sendForceStart(int playerId);
|
||||
|
||||
/**
|
||||
* @brief Lock or unlock a player's sideboard.
|
||||
*
|
||||
* @param locked Whether the sideboard is locked.
|
||||
* @param playerId Player the lock applies to.
|
||||
*/
|
||||
void sendSetSideboardLock(bool locked, int playerId);
|
||||
|
||||
/**
|
||||
* @brief Send a player's sideboard plan.
|
||||
*
|
||||
* @param plan Cards to move between zones when leaving sideboarding.
|
||||
* @param playerId Player the plan belongs to.
|
||||
*/
|
||||
void sendSetSideboardPlan(const QList<MoveCard_ToZone> &plan, int playerId);
|
||||
|
||||
/** @} */
|
||||
|
||||
/** @name Incoming event processing
|
||||
|
|
@ -9,16 +9,13 @@
|
|||
|
||||
#include <QMap>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_game.pb.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
// Translation layer class to expose protobuf safely and hook it up to Qt Signals.
|
||||
// This class de-couples the domain object (i.e. the GameMetaInfo) from the network object.
|
||||
// If the network object changes, only this class needs to be adjusted.
|
||||
|
||||
class AbstractGame;
|
||||
|
||||
class GameMetaInfo : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
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