Compare commits

..

11 commits

Author SHA1 Message Date
Lukas Brübach
8f6d18df10
[Client] Drop dead player event handler extension points
The second PlayerLogic constructor taking a custom PlayerEventHandler was UB
(converting this to PlayerLogic* in the base initializer, and connecting a
freshly-built handler against a not-yet-constructed object), and its abstract
extension surface — virtual initializeZones, virtual getPlayerEventHandler,
virtual processGameEvent, protected player member — has no subclass consumer in
the stack. Revert to the single primary constructor.

Also:
- Drop the unused GameMetaInfo::setIsTournament setter. Tournament status is
  authoritative from Event_GameJoined's ServerInfo_Game; the redundant
  is_tournament field on Event_GameStateChanged is removed in [Protocol].
- Gate the parent-game routing on has_parent_game_id() instead of comparing the
  proto default, so a future drop of the default keeps working.
2026-09-06 16:15:47 +02:00
Lukas Brübach
2d5734de21
[Client] Add tournament event plumbing and player extension points
Wires the client game layer for tournament state without touching
existing behavior:
- GameEventHandler dispatches Event_TournamentState (2027) to a new
  tournamentStateChanged signal
- GameMetaInfo exposes isTournament and parentGameId over the new
  ServerInfo_Game field
- PlayerEventHandler::processGameEvent is virtual and player is
  protected; PlayerLogic gains a protected constructor accepting a
  custom handler, so mode-specific subclasses can intercept events
2026-09-06 16:15:47 +02:00
Lukas Brübach
de03428e51
[Server] Stop mirroring is_tournament on the game-state event
The client learns tournament status from Event_GameJoined's ServerInfo_Game;
mirroring it on every Event_GameStateChanged was dead wire data. The field was
removed from the proto in [Protocol]; drop the now-invalidated setter.
2026-09-06 16:15:46 +02:00
Lukas Brübach
80703adbbb
[Server] Replay tournament state to late joiners
The bracket, phase and standings live in Event_TournamentState, which only
flows on mutation. Without a copy a player or spectator joining after round
one would sit on an empty bracket until the next advance, so the current
state is now enqueued as part of the join snapshot.

Extracts Event_TournamentState building into buildStateEvent() so both the
broadcast path and the join path share one source of truth.
2026-09-06 16:15:46 +02:00
Lukas Brübach
99a409de09
[Server] Adapt tournament engine to the shared settings message
- read games per match from Command_CreateGame.tournament_settings after the
  proto rework replaced the standalone games_per_match field
- use qWarning to match the surrounding file
2026-09-06 16:15:46 +02:00
Lukas Brübach
18fac5c5b9
[Server] Harden Swiss tournament engine against drops and stalls
- link match sub-games to the hub through QPointer so a finished match can
  never dereference a torn-down parent game
- a player who leaves the hub is dropped: no longer paired, current
  undecided match awarded to the opponent, absent from the bracket
- refuse to spawn a match game when either participant is disconnected, and
  set disconnectRemovesPlayer on match games so a mid-match disconnect ends
  it instead of leaving a half-present participant
- match winner is a strict majority (gamesPerMatch/2+1); an exhausted
  series with no majority is recorded as a draw so the round always advances
- match games are started without force-start: a missing deck no longer
  kicks the player; the game stays open for deck selection
- buyes are handed to every leftover player, worst-ranked first, at most one
  per player over the tournament
- tournament hubs cannot start on mere 'everyone ready': host force-start is
  required and fewer than two players never starts
- sub-game creator copies the real player user info instead of fabricating
  IsAdmin
2026-09-06 16:15:46 +02:00
Lukas Brübach
5cb90c29a6
[Server] Implement Swiss tournament engine for lobby games
Adds the server-side tournament mode driven through the strategy and
factory interfaces from #7132:
- Server_Tournament runs greedy Swiss pairing with rematch avoidance,
  byes, ceil(log2(n)) rounds, and best-of-N series via gamesPerMatch;
  match sub-games are spawned through the parent game's
  Server_MatchGameFactory implementation with players auto-joined and
  their submitted decks installed
- Tournament lifecycle strategy gates the hub game start until all
  decks are submitted; match result strategy reports sub-game outcomes
  back to the parent tournament
- Server_Game gains tournament state (isTournament flag, settings,
  parent link), protocol fields in getInfo/game state events, a
  full-lobby bypass for tournament hubs, and disconnectRemovesPlayer
  plumbing for upcoming draft modes
- Match-game creation is deferred to the owning thread's event loop so
  room registration never nests lock orders; shared state is guarded by
  the tournament mutex
2026-09-06 16:15:46 +02:00
Lukas Brübach
7d1ad2191a
[Server] Add extension points for tournament game commands
Adds the server-side seams the tournament engine will plug into:
- Dispatch ReportMatchResult / AdvanceTournament /
  TournamentSettingsSelect in processGameCommand; base implementations
  return RespContextError so regular games reject them safely
- Server_AbstractPlayer::setDeck lets a backend install a deck into a
  player slot directly
- Server_Room::getUserInterfaceByName resolves a room user by name for
  automated game setup


Took 24 minutes
2026-09-06 16:15:46 +02:00
Lukas Brübach
5cea39a1fc
[Protocol] Drop redundant is_tournament from the game-state event
The client already learns a game is a tournament from Event_GameJoined's
ServerInfo_Game.is_tournament, so mirroring it on every Event_GameStateChanged
is dead wire data — nothing on the client reads it. Removed.
2026-09-06 16:15:45 +02:00
Lukas Brübach
dca6fdf17f
[Protocol] Share tournament messages and report draws explicitly
- TournamentPlayer/TournamentPairing/TournamentSettings move into
  serverinfo_tournament.proto, imported by both the tournament event and
  the tournament game commands, so Command_CreateGame reaches them without
  crossing the command/event boundary
- Command_CreateGame carries the full TournamentSettings instead of a
  duplicate games_per_match field, keeping game creation and mid-game
  changes on one wire representation
- draw results are explicit: Command_ReportMatchResult and
  TournamentPairing gain is_draw, so a report stays distinguishable from
  an unset -1 winner
- pairing match-wins are uint32 like the other game counts
2026-09-06 16:15:45 +02:00
Lukas Brübach
e82b311c8b
[Protocol] Add tournament state event and tournament game commands
Adds the protocol layer for the tournament game mode:
- Event_TournamentState (2027) carrying phase, round counters, player
  standings, pairings, and settings (games_per_match)
- Command_ReportMatchResult / Command_AdvanceTournament /
  Command_TournamentSettingsSelect game command extensions (1037-1039)
- is_tournament flag on Command_CreateGame and ServerInfo_Game;
  is_tournament and parent_game_id on Event_GameStateChanged


Took 13 minutes
2026-09-06 16:15:45 +02:00
1480 changed files with 57007 additions and 192201 deletions

View file

@ -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 \

View file

@ -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 \

View file

@ -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 \

View file

@ -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 \

View file

@ -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++ \

View file

@ -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 \

View file

@ -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 \

View file

@ -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

View file

@ -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 $?

View file

@ -1,3 +1,4 @@
.git/
build/
.github/
.tx/

View file

@ -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 \

View file

@ -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"

View file

@ -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"

View file

@ -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/"

View file

@ -1,7 +1,7 @@
name: Update Translations
permissions:
contents: write
contents: read
pull-requests: write
on:

View file

@ -1,7 +1,7 @@
name: Update Translation Source
permissions:
contents: write
contents: read
pull-requests: write
on:

View file

@ -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 }}

View file

@ -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

View file

@ -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
View file

@ -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.

View file

@ -15,10 +15,9 @@
---
<br><pre><p align='center'>
<b>To get started with Cockatrice &#8674; [view our webpage](https://cockatrice.github.io/)</b>
<b>To get support, or suggest changes to the app &#8674; [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 &#8674; [read our code documentation](https://cockatrice.github.io/docs/)</b>
<b>To help with development &#8674; learn [how to contribute](#contribute)</b>
<b>To get started with Cockatrice &#8674; [view our webpage](https://cockatrice.github.io/)</b><br>
<b>To get support, or suggest changes to the app &#8674; [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 &#8674; 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:
- [![Good first issue](https://img.shields.io/github/issues/cockatrice/cockatrice/Good%20First%20Issue)](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 [![CI Docs](https://github.com/Cockatrice/Cockatrice/actions/workflows/documentation-build.yml/badge.svg?event=push)](https://github.com/Cockatrice/Cockatrice/actions/workflows/documentation-build.yml?query=event%3Arelease+event%3Aworkflow_dispatch)
### Documentation [![CI Docs](https://github.com/Cockatrice/Cockatrice/actions/workflows/documentation-build.yml/badge.svg?event=push)](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 [![Transifex Project](https://img.shields.io/badge/translate-on%20transifex-brightgreen)](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

View file

@ -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()

View file

@ -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()

View file

@ -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

View file

@ -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()

View file

@ -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
}

View file

@ -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}")

View file

@ -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>

View file

@ -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>

View file

@ -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)

View file

@ -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

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

View file

@ -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

View file

@ -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

View file

@ -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 &currentDsn)
{
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(&noteId) == 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;
}

View file

@ -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 &currentDsn);
/**
* @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

View file

@ -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)

View file

@ -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;
/**

View file

@ -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)
{

View file

@ -8,8 +8,6 @@
#include <QList>
#include <QWidget>
#include <QtGlobal>
#include <qtmetamacros.h>
/**
* @brief Bar graph of recent network round-trip samples.

View file

@ -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);

View file

@ -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;

View file

@ -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()

View file

@ -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

View file

@ -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);
}

View file

@ -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;

View file

@ -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);
}

View file

@ -9,7 +9,6 @@
#include <QLoggingCategory>
#include <QObject>
#include <qtmetamacros.h>
inline Q_LOGGING_CATEGORY(TappedOutInterfaceLog, "tapped_out_interface");

View file

@ -1,7 +1,6 @@
#include "deck_link_to_api_transformer.h"
#include <QRegularExpression>
#include <QRegularExpressionMatch>
namespace DeckLinkToApiTransformer
{

View file

@ -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

View file

@ -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");

View file

@ -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

View file

@ -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);

View file

@ -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:

View file

@ -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)

View file

@ -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
{

View file

@ -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);

View file

@ -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;

View file

@ -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)

View file

@ -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
{

View file

@ -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)
{

View file

@ -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

View file

@ -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)

View file

@ -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");

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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));
});

View file

@ -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);

View file

@ -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);

View file

@ -8,8 +8,8 @@
#define FILTERBUILDER_H
#include <QWidget>
#include <qtmetamacros.h>
class QCheckBox;
class QComboBox;
class LineEditUnfocusable;
class CardFilter;

View file

@ -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)
{

View file

@ -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
{

View file

@ -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

View file

@ -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");

View file

@ -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) {

View file

@ -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

View file

@ -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)
{

View file

@ -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
{

View file

@ -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;

View file

@ -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
{

View file

@ -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 ""; };
}

View file

@ -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

View 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();
}

View 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

View file

@ -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);
}

View file

@ -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;

View file

@ -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);
}

View 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

View file

@ -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>
@ -57,9 +29,6 @@
#include <libcockatrice/protocol/pb/event_tournament_state.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)
{
@ -236,89 +205,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());
@ -373,6 +259,8 @@ void GameEventHandler::eventSpectatorLeave(const Event_Leave &event,
emit spectatorLeft(eventPlayerId);
game->getPlayerManager()->removeSpectator(eventPlayerId);
emitUserEvent();
}
void GameEventHandler::eventGameStateChanged(const Event_GameStateChanged &event,
@ -553,22 +441,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)

View file

@ -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;
@ -52,15 +45,6 @@ class Event_Kicked;
class Event_ReverseTurn;
class Event_TournamentState;
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");
@ -129,12 +113,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();
@ -153,70 +131,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

View file

@ -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