mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-09 03:23:24 -08:00
Compare commits
172 commits
e50b928f8b
...
8c2bfc46ff
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c2bfc46ff | ||
|
|
69f4fb02db | ||
|
|
37b9544ca3 | ||
|
|
3daefe074c | ||
|
|
755fa38d96 | ||
|
|
71fd913058 | ||
|
|
9cb33f8fe4 | ||
|
|
dd15c1a5f9 | ||
|
|
d4f0b75e87 | ||
|
|
6b566af94e | ||
|
|
66405e966b | ||
|
|
dfef59ca4e | ||
|
|
15655767a8 | ||
|
|
c78fd1e06c | ||
|
|
77e9b92c5a | ||
|
|
d854bdfd83 | ||
|
|
3def6b00ef | ||
|
|
44d5138181 | ||
|
|
9f30892dff | ||
|
|
8b0c980e85 | ||
|
|
62e590d300 | ||
|
|
a1a8a59f4a | ||
|
|
94e6281d87 | ||
|
|
3cf8bdb00e | ||
|
|
5134e72eb4 | ||
|
|
b37c8b828f | ||
|
|
719863e43d | ||
|
|
d021f20d14 | ||
|
|
ce543896f5 | ||
|
|
429eb2a1fd | ||
|
|
a74dfeef6e | ||
|
|
5fe7f186d3 | ||
|
|
50a2ae9cc8 | ||
|
|
ef4574d2b9 | ||
|
|
e08402ac94 | ||
|
|
ff393b8990 | ||
|
|
00151380e3 | ||
|
|
344dbd2caa | ||
|
|
825aba953a | ||
|
|
ae43659b91 | ||
|
|
d4002dc52b | ||
|
|
f2c359ba9f | ||
|
|
52d3978f84 | ||
|
|
345b0e8ba9 | ||
|
|
78964779c6 | ||
|
|
7ca23430cd | ||
|
|
65e9f1b370 | ||
|
|
5d0d466930 | ||
|
|
edb5caec8a | ||
|
|
2335e064ac | ||
|
|
47c90cda10 | ||
|
|
4879ddbd0f | ||
|
|
c9853734ad | ||
|
|
6f0c5dca12 | ||
|
|
85fd052fdb | ||
|
|
3ccd44155d | ||
|
|
77f882ca37 | ||
|
|
674a607960 | ||
|
|
73d5a2a0ab | ||
|
|
095b135640 | ||
|
|
563833a88a | ||
|
|
4972ebcb97 | ||
|
|
94b91be5a2 | ||
|
|
93c5d85996 | ||
|
|
52c5bd2ea3 | ||
|
|
d30d275296 | ||
|
|
bbc7e462ff | ||
|
|
720adf5410 | ||
|
|
0369aaffa9 | ||
|
|
ded83aaab5 | ||
|
|
9e50c5ad6e | ||
|
|
06da7f815a | ||
|
|
5cf3358c99 | ||
|
|
d89b502342 | ||
|
|
a83d82b5e8 | ||
|
|
bf3562a85a | ||
|
|
30d3afc7b4 | ||
|
|
92d082e735 | ||
|
|
aa42cfbccc | ||
|
|
d42b9607c5 | ||
|
|
4793c1fc04 | ||
|
|
675e1f7447 | ||
|
|
858b270fa9 | ||
|
|
4d8ab54313 | ||
|
|
e6b4c51585 | ||
|
|
4ab875be48 | ||
|
|
f3fb3331d1 | ||
|
|
93309e01a2 | ||
|
|
b4e24f958a | ||
|
|
a4c4c67674 | ||
|
|
f33cfcb288 | ||
|
|
5fbc636f93 | ||
|
|
5ddfcd7ead | ||
|
|
518c7e6125 | ||
|
|
3b07b10b19 | ||
|
|
66b9338c90 | ||
|
|
77a3875c25 | ||
|
|
28c8474f03 | ||
|
|
11e2bb0d2f | ||
|
|
eac149656b | ||
|
|
e806085681 | ||
|
|
06b9ca368f | ||
|
|
67a212d677 | ||
|
|
4028606bd9 | ||
|
|
6e770eb7b2 | ||
|
|
e73dd2b520 | ||
|
|
ab13a6748f | ||
|
|
1e0fc35d94 | ||
|
|
bbc6003f0a | ||
|
|
ad4320cd7b | ||
|
|
c25a2c5c3b | ||
|
|
b85436461e | ||
|
|
f4b4365464 | ||
|
|
4980f88a7a | ||
|
|
e666a4978d | ||
|
|
c4c7cc0253 | ||
|
|
0fe4fd0d8c | ||
|
|
9980c90cea | ||
|
|
9709fe4a1a | ||
|
|
829152f388 | ||
|
|
c6f682f9cf | ||
|
|
9f59f5c495 | ||
|
|
ad0fd2d4b0 | ||
|
|
6385f3fab3 | ||
|
|
ea6af0048c | ||
|
|
46cdbac830 | ||
|
|
4343544588 | ||
|
|
4750e67b7f | ||
|
|
4974cac59a | ||
|
|
860625be4d | ||
|
|
665f7267e4 | ||
|
|
679762c624 | ||
|
|
a12b97ec50 | ||
|
|
b849a57dab | ||
|
|
4c03e0062b | ||
|
|
fe8a3f486b | ||
|
|
12587e2da9 | ||
|
|
c9650db87e | ||
|
|
890bc0d4a4 | ||
|
|
3f93e93f13 | ||
|
|
aee866b242 | ||
|
|
cc35258cdb | ||
|
|
03bfa2dcc6 | ||
|
|
c049c5c2b7 | ||
|
|
47133bcc6b | ||
|
|
5b895dc553 | ||
|
|
a1aa3d9aa9 | ||
|
|
f565910790 | ||
|
|
74ae39f0d4 | ||
|
|
33df0916a8 | ||
|
|
44155064b4 | ||
|
|
1756501ded | ||
|
|
a19e0e459a | ||
|
|
c5b24e81da | ||
|
|
2d4aa4414a | ||
|
|
72f0a8975f | ||
|
|
b231f55df4 | ||
|
|
07a699851d | ||
|
|
a331da10e8 | ||
|
|
8e391f6fa0 | ||
|
|
09846d26f3 | ||
|
|
a139084ad6 | ||
|
|
155f7b0143 | ||
|
|
ce51e7147d | ||
|
|
c99113ab0e | ||
|
|
05dcde69db | ||
|
|
17d2e8d86e | ||
|
|
be38e8d429 | ||
|
|
6c660d6b21 | ||
|
|
711386574d | ||
|
|
b11b096828 | ||
|
|
bbe438b09a |
643 changed files with 157395 additions and 52334 deletions
|
|
@ -3,11 +3,13 @@ FROM debian:12
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||||
build-essential \
|
build-essential \
|
||||||
|
ca-certificates \
|
||||||
ccache \
|
ccache \
|
||||||
cmake \
|
cmake \
|
||||||
file \
|
file \
|
||||||
g++ \
|
g++ \
|
||||||
git \
|
git \
|
||||||
|
libcurl4-openssl-dev \
|
||||||
libgl-dev \
|
libgl-dev \
|
||||||
liblzma-dev \
|
liblzma-dev \
|
||||||
libmariadb-dev-compat \
|
libmariadb-dev-compat \
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ RUN apt-get update && \
|
||||||
file \
|
file \
|
||||||
g++ \
|
g++ \
|
||||||
git \
|
git \
|
||||||
|
libcurl4-openssl-dev \
|
||||||
libgl-dev \
|
libgl-dev \
|
||||||
liblzma-dev \
|
liblzma-dev \
|
||||||
libmariadb-dev-compat \
|
libmariadb-dev-compat \
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
FROM fedora:43
|
FROM fedora:43
|
||||||
|
|
||||||
RUN dnf install -y \
|
RUN dnf install -y \
|
||||||
|
ca-certificates \
|
||||||
ccache \
|
ccache \
|
||||||
cmake \
|
cmake \
|
||||||
gcc-c++ \
|
gcc-c++ \
|
||||||
git \
|
git \
|
||||||
|
libcurl-devel \
|
||||||
mariadb-devel \
|
mariadb-devel \
|
||||||
ninja-build \
|
ninja-build \
|
||||||
openssl-devel \
|
openssl-devel \
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
FROM fedora:44
|
FROM fedora:44
|
||||||
|
|
||||||
RUN dnf install -y \
|
RUN dnf install -y \
|
||||||
|
ca-certificates \
|
||||||
ccache \
|
ccache \
|
||||||
cmake \
|
cmake \
|
||||||
gcc-c++ \
|
gcc-c++ \
|
||||||
git \
|
git \
|
||||||
|
libcurl-devel \
|
||||||
mariadb-devel \
|
mariadb-devel \
|
||||||
ninja-build \
|
ninja-build \
|
||||||
openssl-devel \
|
openssl-devel \
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,13 @@ FROM ubuntu:24.04
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||||
build-essential \
|
build-essential \
|
||||||
|
ca-certificates \
|
||||||
ccache \
|
ccache \
|
||||||
cmake \
|
cmake \
|
||||||
file \
|
file \
|
||||||
g++ \
|
g++ \
|
||||||
git \
|
git \
|
||||||
|
libcurl4-openssl-dev \
|
||||||
libgl-dev \
|
libgl-dev \
|
||||||
liblzma-dev \
|
liblzma-dev \
|
||||||
libmariadb-dev-compat \
|
libmariadb-dev-compat \
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ RUN apt-get update && \
|
||||||
g++ \
|
g++ \
|
||||||
gcovr \
|
gcovr \
|
||||||
git \
|
git \
|
||||||
|
libcurl4-openssl-dev \
|
||||||
libgl-dev \
|
libgl-dev \
|
||||||
liblzma-dev \
|
liblzma-dev \
|
||||||
libmariadb-dev-compat \
|
libmariadb-dev-compat \
|
||||||
|
|
|
||||||
232
.ci/compile.sh
232
.ci/compile.sh
|
|
@ -200,6 +200,20 @@ if [[ $USE_VCPKG ]]; then
|
||||||
flags+=("-DUSE_VCPKG=1")
|
flags+=("-DUSE_VCPKG=1")
|
||||||
flags+=("-DVCPKG_INSTALL_OPTIONS=--x-abi-tools-use-exact-versions")
|
flags+=("-DVCPKG_INSTALL_OPTIONS=--x-abi-tools-use-exact-versions")
|
||||||
fi
|
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
|
# Add cmake --build flags
|
||||||
buildflags=(--config "$BUILDTYPE")
|
buildflags=(--config "$BUILDTYPE")
|
||||||
|
|
@ -297,16 +311,24 @@ if [[ $RUNNER_OS == macOS ]]; then
|
||||||
|
|
||||||
if [[ $MAKE_PACKAGE ]]; then
|
if [[ $MAKE_PACKAGE ]]; then
|
||||||
# Workaround https://github.com/actions/runner-images/issues/7522
|
# Workaround https://github.com/actions/runner-images/issues/7522
|
||||||
# have hdiutil repeat the command 10 times in hope of success
|
# have hdiutil repeat the command 10 times in hope of success.
|
||||||
|
# The wrapper has to exit non-zero once it gives up: CPack reads that status to
|
||||||
|
# decide whether the disk image was created, so a zero exit turns a failed hdiutil
|
||||||
|
# into a much later, misleading "Problem copying the package" error that hides
|
||||||
|
# hdiutil's own message.
|
||||||
hdiutil_script="/tmp/hdiutil.sh"
|
hdiutil_script="/tmp/hdiutil.sh"
|
||||||
# shellcheck disable=SC2016
|
# shellcheck disable=SC2016
|
||||||
echo '#!/bin/bash
|
echo '#!/bin/bash
|
||||||
|
attempts=10
|
||||||
i=0
|
i=0
|
||||||
while ! hdiutil "$@"; do
|
while true; do
|
||||||
if (( ++i >= 10 )); then
|
hdiutil "$@" && exit 0
|
||||||
echo "Error: hdiutil failed $i times!" >&2
|
rc=$?
|
||||||
break
|
if (( ++i >= attempts )); then
|
||||||
|
echo "::error::hdiutil $1 failed $i times (last status $rc), giving up" >&2
|
||||||
|
exit "$rc"
|
||||||
fi
|
fi
|
||||||
|
echo "hdiutil $1 attempt $i/$attempts failed (status $rc), retrying" >&2
|
||||||
sleep 1
|
sleep 1
|
||||||
done' >"$hdiutil_script"
|
done' >"$hdiutil_script"
|
||||||
chmod +x "$hdiutil_script"
|
chmod +x "$hdiutil_script"
|
||||||
|
|
@ -445,6 +467,206 @@ if [[ $MAKE_COVERAGE ]]; then
|
||||||
echo "::endgroup::"
|
echo "::endgroup::"
|
||||||
fi
|
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"
|
||||||
|
echo "::endgroup::"
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ $MAKE_INSTALL ]]; then
|
if [[ $MAKE_INSTALL ]]; then
|
||||||
echo "::group::Install"
|
echo "::group::Install"
|
||||||
cmake --build . --target install --config "$BUILDTYPE"
|
cmake --build . --target install --config "$BUILDTYPE"
|
||||||
|
|
|
||||||
|
|
@ -175,6 +175,12 @@ function RUN ()
|
||||||
args+=(--mount "type=bind,source=$GITHUB_OUTPUT,target=/gh_output")
|
args+=(--mount "type=bind,source=$GITHUB_OUTPUT,target=/gh_output")
|
||||||
args+=(--env "GITHUB_OUTPUT=/gh_output")
|
args+=(--env "GITHUB_OUTPUT=/gh_output")
|
||||||
fi
|
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
|
# shellcheck disable=2086
|
||||||
docker run "${args[@]}" $RUN_ARGS "$IMAGE_NAME" bash "$BUILD_SCRIPT" $RUN_OPTS "$@"
|
docker run "${args[@]}" $RUN_ARGS "$IMAGE_NAME" bash "$BUILD_SCRIPT" $RUN_OPTS "$@"
|
||||||
return $?
|
return $?
|
||||||
|
|
|
||||||
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
|
|
@ -60,8 +60,10 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y --no-install-recommends \
|
sudo apt-get install -y --no-install-recommends \
|
||||||
|
ca-certificates \
|
||||||
cmake \
|
cmake \
|
||||||
g++ \
|
g++ \
|
||||||
|
libcurl4-openssl-dev \
|
||||||
libgl-dev \
|
libgl-dev \
|
||||||
liblzma-dev \
|
liblzma-dev \
|
||||||
libmariadb-dev-compat \
|
libmariadb-dev-compat \
|
||||||
|
|
|
||||||
267
.github/workflows/desktop-build.yml
vendored
267
.github/workflows/desktop-build.yml
vendored
|
|
@ -46,6 +46,11 @@ jobs:
|
||||||
outputs:
|
outputs:
|
||||||
tag: ${{ steps.configure.outputs.tag }}
|
tag: ${{ steps.configure.outputs.tag }}
|
||||||
sha: ${{ steps.configure.outputs.sha }}
|
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:
|
steps:
|
||||||
- name: "Configure"
|
- name: "Configure"
|
||||||
|
|
@ -56,6 +61,9 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
if [[ "$GITHUB_REF_TYPE" == 'tag' ]]; then # release
|
if [[ "$GITHUB_REF_TYPE" == 'tag' ]]; then # release
|
||||||
echo "tag=$GITHUB_REF_NAME" >> "$GITHUB_OUTPUT"
|
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
|
fi
|
||||||
echo "sha=$RESOLVED_SHA" >> "$GITHUB_OUTPUT"
|
echo "sha=$RESOLVED_SHA" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
|
@ -93,6 +101,36 @@ jobs:
|
||||||
--title "$release_name" \
|
--title "$release_name" \
|
||||||
--notes-file "$body_path"
|
--notes-file "$body_path"
|
||||||
|
|
||||||
|
vcpkg-baseline:
|
||||||
|
name: vcpkg baseline
|
||||||
|
runs-on: ubuntu-slim
|
||||||
|
steps:
|
||||||
|
- name: "Checkout"
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
|
# vcpkg-tool reads versions/baseline.json out of the commit named by
|
||||||
|
# `builtin-baseline`, through the `vcpkg` submodule's git history. A submodule
|
||||||
|
# pointer that lags the manifest - e.g. a stale `vcpkg/` worktree captured by
|
||||||
|
# `git add` in a PR - makes every vcpkg leg fail in configure with "failed to
|
||||||
|
# `git show` versions/baseline.json". That reads like a baseline or network
|
||||||
|
# problem, when the pointer is what has to move. Check the two agree here in
|
||||||
|
# seconds instead of on five build legs.
|
||||||
|
- name: "Check the submodule pointer against the manifest baseline"
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
pointer="$(git rev-parse HEAD:vcpkg 2>/dev/null || true)"
|
||||||
|
baseline="$(sed -nE 's/.*"builtin-baseline"[[:space:]]*:[[:space:]]*"([0-9a-f]{40})".*/\1/p' vcpkg.json)"
|
||||||
|
if [[ -z "$pointer" || -z "$baseline" ]]; then
|
||||||
|
echo "::error file=vcpkg.json::could not read the vcpkg submodule pointer or the builtin-baseline"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ "$pointer" != "$baseline" ]]; then
|
||||||
|
echo "::error file=vcpkg.json::vcpkg submodule is at $pointer but builtin-baseline is $baseline"
|
||||||
|
echo "Move them together: git -C vcpkg checkout $baseline && git add vcpkg"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "vcpkg submodule and builtin-baseline agree on $baseline"
|
||||||
|
|
||||||
build-linux:
|
build-linux:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
|
@ -225,7 +263,7 @@ jobs:
|
||||||
|
|
||||||
- name: "Upload coverage artifact"
|
- name: "Upload coverage artifact"
|
||||||
if: matrix.coverage == 'yes'
|
if: matrix.coverage == 'yes'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: coverage-report
|
name: coverage-report
|
||||||
path: coverage/
|
path: coverage/
|
||||||
|
|
@ -239,6 +277,12 @@ jobs:
|
||||||
SUFFIX: '-${{ matrix.distro }}${{ matrix.version }}'
|
SUFFIX: '-${{ matrix.distro }}${{ matrix.version }}'
|
||||||
package: '${{ matrix.package }}'
|
package: '${{ matrix.package }}'
|
||||||
server_only: '${{ matrix.server_only }}'
|
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: |
|
run: |
|
||||||
source .ci/docker.sh
|
source .ci/docker.sh
|
||||||
args=()
|
args=()
|
||||||
|
|
@ -247,9 +291,58 @@ jobs:
|
||||||
args+=(--ccache "$CCACHE_SIZE")
|
args+=(--ccache "$CCACHE_SIZE")
|
||||||
args+=(--cmake-generator "$CMAKE_GENERATOR")
|
args+=(--cmake-generator "$CMAKE_GENERATOR")
|
||||||
args+=(--suffix "$SUFFIX")
|
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[@]}"
|
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
|
# Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342
|
||||||
- name: "Delete remote compiler cache (ccache)"
|
- name: "Delete remote compiler cache (ccache)"
|
||||||
if: github.ref == 'refs/heads/master' && steps.restore_ccache.outputs.cache-hit
|
if: github.ref == 'refs/heads/master' && steps.restore_ccache.outputs.cache-hit
|
||||||
|
|
@ -403,7 +496,7 @@ jobs:
|
||||||
vcpkg_upload: 1 # only one leg per vcpkg_scope hands its archives over
|
vcpkg_upload: 1 # only one leg per vcpkg_scope hands its archives over
|
||||||
|
|
||||||
name: ${{ matrix.os }} ${{ matrix.target }}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }}
|
name: ${{ matrix.os }} ${{ matrix.target }}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }}
|
||||||
needs: configure
|
needs: [configure, vcpkg-baseline]
|
||||||
runs-on: ${{ matrix.runner }}
|
runs-on: ${{ matrix.runner }}
|
||||||
timeout-minutes: 100
|
timeout-minutes: 100
|
||||||
env:
|
env:
|
||||||
|
|
@ -521,11 +614,17 @@ jobs:
|
||||||
# job. A scope nobody has written to yet therefore leaves the directory missing
|
# 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
|
# 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.
|
# 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"
|
- name: "Create vcpkg cache directory"
|
||||||
|
if: always()
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
CACHE_DIR: '${{ steps.vcpkg-cache.outputs.path }}'
|
CACHE_DIR: '${{ steps.vcpkg-cache.outputs.path }}'
|
||||||
run: mkdir -p "$CACHE_DIR"
|
run: mkdir -p "${CACHE_DIR:-$GITHUB_WORKSPACE/.vcpkg-cache}"
|
||||||
|
|
||||||
# Uses environment variables, see compile.sh for more details
|
# Uses environment variables, see compile.sh for more details
|
||||||
- name: "Build Cockatrice"
|
- name: "Build Cockatrice"
|
||||||
|
|
@ -535,6 +634,11 @@ jobs:
|
||||||
BUILDTYPE: '${{ matrix.type }}'
|
BUILDTYPE: '${{ matrix.type }}'
|
||||||
CCACHE_EVICTION_AGE: ${{ matrix.ccache_eviction_age }}
|
CCACHE_EVICTION_AGE: ${{ matrix.ccache_eviction_age }}
|
||||||
CMAKE_GENERATOR: ${{ matrix.cmake_generator }}
|
CMAKE_GENERATOR: ${{ matrix.cmake_generator }}
|
||||||
|
# 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 || '' }}
|
||||||
DEVELOPER_DIR: '/Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer'
|
DEVELOPER_DIR: '/Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer'
|
||||||
GITHUB_TOKEN: ${{ github.token }} # needed for vcpkg dependency graph updates, see VCPKG_FEATURE_FLAGS
|
GITHUB_TOKEN: ${{ github.token }} # needed for vcpkg dependency graph updates, see VCPKG_FEATURE_FLAGS
|
||||||
MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }}
|
MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }}
|
||||||
|
|
@ -558,8 +662,125 @@ jobs:
|
||||||
VCPKG_DISABLE_METRICS: 1
|
VCPKG_DISABLE_METRICS: 1
|
||||||
VCPKG_FEATURE_FLAGS: dependencygraph
|
VCPKG_FEATURE_FLAGS: dependencygraph
|
||||||
VCPKG_TARGET_MACOS_VERSION: ${{ matrix.vcpkg_deployment_target }}
|
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
|
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"
|
# 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
|
# 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.
|
# the actual compile error is otherwise nowhere in the workflow output.
|
||||||
|
|
@ -703,19 +924,6 @@ jobs:
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
path: ${{ steps.build.outputs.path }}
|
path: ${{ steps.build.outputs.path }}
|
||||||
|
|
||||||
# Ninja is single-config: binaries and their PDBs land directly in the
|
|
||||||
# target directories instead of a Release subdirectory.
|
|
||||||
- name: "[Windows] Upload PDBs (Program Databases)"
|
|
||||||
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/*.pdb
|
|
||||||
build/oracle/*.pdb
|
|
||||||
build/servatrice/*.pdb
|
|
||||||
|
|
||||||
- name: "Upload to release"
|
- name: "Upload to release"
|
||||||
if: needs.configure.outputs.tag != null && matrix.make_package == '1'
|
if: needs.configure.outputs.tag != null && matrix.make_package == '1'
|
||||||
id: upload_release
|
id: upload_release
|
||||||
|
|
@ -727,6 +935,33 @@ jobs:
|
||||||
tag_name: ${{ needs.configure.outputs.tag }}
|
tag_name: ${{ needs.configure.outputs.tag }}
|
||||||
run: gh release upload "$tag_name" "$asset_path#$asset_name"
|
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"
|
- name: "Attest binary provenance"
|
||||||
if: steps.upload_release.outcome == 'success'
|
if: steps.upload_release.outcome == 'success'
|
||||||
id: attestation
|
id: attestation
|
||||||
|
|
|
||||||
16
.tx/github_integration_config.yml
Normal file
16
.tx/github_integration_config.yml
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
# 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
|
||||||
|
|
@ -90,7 +90,7 @@ endif()
|
||||||
|
|
||||||
# A project name is needed for CPack
|
# A project name is needed for CPack
|
||||||
# Version can be overriden by git tags, see cmake/getversion.cmake
|
# Version can be overriden by git tags, see cmake/getversion.cmake
|
||||||
project("Cockatrice" VERSION 3.1.2)
|
project("Cockatrice" VERSION 3.2.0)
|
||||||
|
|
||||||
# Set release name if not provided via env/cmake var
|
# Set release name if not provided via env/cmake var
|
||||||
if(NOT DEFINED GIT_TAG_RELEASENAME)
|
if(NOT DEFINED GIT_TAG_RELEASENAME)
|
||||||
|
|
@ -220,6 +220,7 @@ endif()
|
||||||
# when ENABLE_SANITIZERS or ENABLE_COVERAGE is on.
|
# when ENABLE_SANITIZERS or ENABLE_COVERAGE is on.
|
||||||
include(Sanitizers)
|
include(Sanitizers)
|
||||||
include(Coverage)
|
include(Coverage)
|
||||||
|
include(CrashReporting)
|
||||||
|
|
||||||
# GNU systems need to define the Mersenne exponent for the RNG to compile w/o warning
|
# GNU systems need to define the Mersenne exponent for the RNG to compile w/o warning
|
||||||
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
||||||
|
|
@ -249,7 +250,22 @@ message(STATUS "Update Translations: ${UPDATE_TRANSLATIONS}")
|
||||||
|
|
||||||
include(FindQtRuntime)
|
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 once here for every target: these variables are inherited by the
|
||||||
|
# subdirectories, so the per-directory copies were redundant. Only the
|
||||||
|
# variables are set; qt_standard_project_setup would also change the runtime
|
||||||
|
# output directories and the install RPATH.
|
||||||
set(CMAKE_AUTOMOC TRUE)
|
set(CMAKE_AUTOMOC TRUE)
|
||||||
|
set(CMAKE_AUTOUIC TRUE)
|
||||||
|
set(CMAKE_AUTORCC TRUE)
|
||||||
|
|
||||||
# Find other needed libraries
|
# Find other needed libraries
|
||||||
find_package(Protobuf CONFIG)
|
find_package(Protobuf CONFIG)
|
||||||
|
|
@ -327,7 +343,7 @@ if(UNIX)
|
||||||
endif()
|
endif()
|
||||||
elseif(WIN32)
|
elseif(WIN32)
|
||||||
set(CPACK_GENERATOR NSIS ${CPACK_GENERATOR})
|
set(CPACK_GENERATOR NSIS ${CPACK_GENERATOR})
|
||||||
if("${CMAKE_GENERATOR_PLATFORM}" MATCHES "(x64)")
|
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||||
set(TRICE_IS_64_BIT 1)
|
set(TRICE_IS_64_BIT 1)
|
||||||
else()
|
else()
|
||||||
set(TRICE_IS_64_BIT 0)
|
set(TRICE_IS_64_BIT 0)
|
||||||
|
|
@ -336,6 +352,15 @@ elseif(WIN32)
|
||||||
# Configure file with custom definitions for NSIS.
|
# Configure file with custom definitions for NSIS.
|
||||||
configure_file("${COCKATRICE_CMAKE_PATH}/NSIS.definitions.nsh.in" "${PROJECT_BINARY_DIR}/NSIS.definitions.nsh")
|
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
|
# 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
|
# 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.
|
# installer is missing one of them, or carries a runtime it does not know about.
|
||||||
|
|
@ -410,6 +435,9 @@ if(WITH_ORACLE OR WITH_CLIENT)
|
||||||
add_subdirectory(${CMAKE_SOURCE_DIR}/libcockatrice_models ${CMAKE_BINARY_DIR}/libcockatrice_models)
|
add_subdirectory(${CMAKE_SOURCE_DIR}/libcockatrice_models ${CMAKE_BINARY_DIR}/libcockatrice_models)
|
||||||
add_subdirectory(${CMAKE_SOURCE_DIR}/libcockatrice_filters ${CMAKE_BINARY_DIR}/libcockatrice_filters)
|
add_subdirectory(${CMAKE_SOURCE_DIR}/libcockatrice_filters ${CMAKE_BINARY_DIR}/libcockatrice_filters)
|
||||||
endif()
|
endif()
|
||||||
|
if(WITH_CLIENT)
|
||||||
|
add_subdirectory(${CMAKE_SOURCE_DIR}/libcockatrice_game ${CMAKE_BINARY_DIR}/libcockatrice_game)
|
||||||
|
endif()
|
||||||
|
|
||||||
if(WITH_SERVER)
|
if(WITH_SERVER)
|
||||||
add_subdirectory(servatrice)
|
add_subdirectory(servatrice)
|
||||||
|
|
@ -421,6 +449,13 @@ if(WITH_CLIENT)
|
||||||
set(CPACK_INSTALL_CMAKE_PROJECTS "Cockatrice;Cockatrice;ALL;/" ${CPACK_INSTALL_CMAKE_PROJECTS})
|
set(CPACK_INSTALL_CMAKE_PROJECTS "Cockatrice;Cockatrice;ALL;/" ${CPACK_INSTALL_CMAKE_PROJECTS})
|
||||||
endif()
|
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)
|
if(WITH_ORACLE)
|
||||||
add_subdirectory(oracle)
|
add_subdirectory(oracle)
|
||||||
set(CPACK_INSTALL_CMAKE_PROJECTS "Oracle;Oracle;ALL;/" ${CPACK_INSTALL_CMAKE_PROJECTS})
|
set(CPACK_INSTALL_CMAKE_PROJECTS "Oracle;Oracle;ALL;/" ${CPACK_INSTALL_CMAKE_PROJECTS})
|
||||||
|
|
@ -433,5 +468,5 @@ endif()
|
||||||
|
|
||||||
if(Qt6_FOUND AND Qt6_VERSION_MINOR GREATER_EQUAL 3)
|
if(Qt6_FOUND AND Qt6_VERSION_MINOR GREATER_EQUAL 3)
|
||||||
# Qt6.3+ requires project finalization to support translations
|
# Qt6.3+ requires project finalization to support translations
|
||||||
qt6_finalize_project()
|
qt_finalize_project()
|
||||||
endif()
|
endif()
|
||||||
|
|
|
||||||
14
README.md
14
README.md
|
|
@ -15,9 +15,10 @@
|
||||||
---
|
---
|
||||||
|
|
||||||
<br><pre><p align='center'>
|
<br><pre><p align='center'>
|
||||||
<b>To get started with Cockatrice ⇢ [view our webpage](https://cockatrice.github.io/)</b><br>
|
<b>To get started with Cockatrice ⇢ [view our webpage](https://cockatrice.github.io/)</b>
|
||||||
<b>To get support, or suggest changes to the app ⇢ [file an issue](https://github.com/Cockatrice/Cockatrice/issues) ([How?](https://github.com/Cockatrice/Cockatrice/wiki/How-to-Create-a-GitHub-Ticket-Regarding-Cockatrice))</b>
|
<b>To get support, or suggest changes to the app ⇢ [file an issue](https://github.com/Cockatrice/Cockatrice/issues) ([How?](https://github.com/Cockatrice/Cockatrice/wiki/How-to-Create-a-GitHub-Ticket-Regarding-Cockatrice))</b><br>
|
||||||
<b>To help with development ⇢ learn [how to contribute](#contribute-)</b>
|
<b>To learn about the app and its code ⇢ [read our code documentation](https://cockatrice.github.io/docs/)</b>
|
||||||
|
<b>To help with development ⇢ learn [how to contribute](#contribute)</b>
|
||||||
</pre><p><br>
|
</pre><p><br>
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -83,7 +84,7 @@ Join our [Discord community](https://discord.gg/3Z9yzmA) to connect with other p
|
||||||
|
|
||||||
### Code
|
### Code
|
||||||
|
|
||||||
To contribute code to the project, please review our [guidelines](https://github.com/Cockatrice/Cockatrice/blob/master/.github/CONTRIBUTING.md) first.<br>
|
To commit code to the project, please review our [Contribution 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:
|
We maintain two tags for contributors to easier find issues to potentially work on:
|
||||||
- [](https://github.com/Cockatrice/Cockatrice/issues?utf8=%E2%9C%93&q=is%3Aopen%20is%3Aissue%20label%3A%22Good%20first%20issue%22%20)<br>
|
- [](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.
|
Issues tagged in this way provide a simple way to get started. They don't require much experience to be worked on.
|
||||||
|
|
@ -95,7 +96,7 @@ 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).
|
You can also have a look at our `Todo List` in our [Code Documentation](https://cockatrice.github.io/docs) or search the repo for [`\todo` comments](https://github.com/search?q=repo%3ACockatrice%2FCockatrice%20%5Ctodo&type=code).
|
||||||
|
|
||||||
### Documentation [](https://github.com/Cockatrice/Cockatrice/actions/workflows/documentation-build.yml?query=event%3Apush)
|
### Documentation [](https://github.com/Cockatrice/Cockatrice/actions/workflows/documentation-build.yml?query=event%3Arelease+event%3Aworkflow_dispatch)
|
||||||
|
|
||||||
There are various places where useful information for different needs are maintained:
|
There are various places where useful information for different needs are maintained:
|
||||||
- [Official Code Documentation](https://cockatrice.github.io/docs/)
|
- [Official Code Documentation](https://cockatrice.github.io/docs/)
|
||||||
|
|
@ -104,7 +105,8 @@ There are various places where useful information for different needs are mainta
|
||||||
- [Official README](https://github.com/Cockatrice/Cockatrice/blob/master/README.md) `This file`
|
- [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)
|
- [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 you 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 everybody to improve the documentation by suggesting edits based on this guide.
|
||||||
|
|
||||||
### Translation [](https://explore.transifex.com/cockatrice/cockatrice/)
|
### Translation [](https://explore.transifex.com/cockatrice/cockatrice/)
|
||||||
|
|
||||||
|
|
|
||||||
212
cmake/CrashReporting.cmake
Normal file
212
cmake/CrashReporting.cmake
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
# 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()
|
||||||
File diff suppressed because it is too large
Load diff
146
cmake/SendInstallerLog.ps1.in
Normal file
146
cmake/SendInstallerLog.ps1.in
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
# 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
|
||||||
|
}
|
||||||
80
cmake/SplitDebugSymbols.cmake.in
Normal file
80
cmake/SplitDebugSymbols.cmake.in
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
# 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}")
|
||||||
|
|
@ -15,6 +15,7 @@ set(cockatrice_SOURCES
|
||||||
src/client/network/update/client/client_update_checker.cpp
|
src/client/network/update/client/client_update_checker.cpp
|
||||||
src/client/network/update/client/release_channel.cpp
|
src/client/network/update/client/release_channel.cpp
|
||||||
src/client/network/update/card_spoiler/spoiler_background_updater.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_graph_widget.cpp
|
||||||
src/client/latency_status_widget.cpp
|
src/client/latency_status_widget.cpp
|
||||||
src/client/lag_monitor.cpp
|
src/client/lag_monitor.cpp
|
||||||
|
|
@ -24,12 +25,11 @@ set(cockatrice_SOURCES
|
||||||
src/client/settings/shortcut_treeview.cpp
|
src/client/settings/shortcut_treeview.cpp
|
||||||
src/client/settings/shortcuts_settings.cpp
|
src/client/settings/shortcuts_settings.cpp
|
||||||
src/interface/deck_loader/card_node_function.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/deck_loader.cpp
|
||||||
src/interface/deck_loader/loaded_deck.cpp
|
|
||||||
src/interface/widgets/dialogs/dlg_connect.cpp
|
src/interface/widgets/dialogs/dlg_connect.cpp
|
||||||
src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.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_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_default_tags_editor.cpp
|
||||||
src/interface/widgets/dialogs/dlg_edit_avatar.cpp
|
src/interface/widgets/dialogs/dlg_edit_avatar.cpp
|
||||||
src/interface/widgets/dialogs/dlg_edit_password.cpp
|
src/interface/widgets/dialogs/dlg_edit_password.cpp
|
||||||
|
|
@ -60,6 +60,7 @@ set(cockatrice_SOURCES
|
||||||
src/interface/widgets/dialogs/dlg_view_log.cpp
|
src/interface/widgets/dialogs/dlg_view_log.cpp
|
||||||
src/interface/widgets/dialogs/override_printing_warning.cpp
|
src/interface/widgets/dialogs/override_printing_warning.cpp
|
||||||
src/interface/widgets/dialogs/path_safety_warning.cpp
|
src/interface/widgets/dialogs/path_safety_warning.cpp
|
||||||
|
src/interface/widgets/dialogs/settings_view.cpp
|
||||||
src/interface/widgets/dialogs/tip_of_the_day.cpp
|
src/interface/widgets/dialogs/tip_of_the_day.cpp
|
||||||
src/interface/widgets/deck_share/deck_share_utils.cpp
|
src/interface/widgets/deck_share/deck_share_utils.cpp
|
||||||
src/interface/widgets/deck_share/shared_deck_preview_widget.cpp
|
src/interface/widgets/deck_share/shared_deck_preview_widget.cpp
|
||||||
|
|
@ -68,36 +69,27 @@ set(cockatrice_SOURCES
|
||||||
src/filters/filter_builder.cpp
|
src/filters/filter_builder.cpp
|
||||||
src/filters/filter_tree_model.cpp
|
src/filters/filter_tree_model.cpp
|
||||||
src/filters/syntax_help.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_drag_item.cpp
|
||||||
src/game_graphics/board/abstract_card_item.cpp
|
src/game_graphics/board/abstract_card_item.cpp
|
||||||
src/game_graphics/board/abstract_counter.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_item.cpp
|
||||||
|
src/game_graphics/board/arrow_registry.cpp
|
||||||
src/game_graphics/board/arrow_target.cpp
|
src/game_graphics/board/arrow_target.cpp
|
||||||
src/game_graphics/board/card_drag_item.cpp
|
src/game_graphics/board/card_drag_item.cpp
|
||||||
src/game_graphics/board/card_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_graphics/board/counter_general.cpp
|
||||||
src/game/board/counter_state.cpp
|
|
||||||
src/game_graphics/board/translate_counter_name.cpp
|
src/game_graphics/board/translate_counter_name.cpp
|
||||||
src/game_graphics/deckview/deck_view.cpp
|
src/game_graphics/deckview/deck_view.cpp
|
||||||
src/game_graphics/deckview/deck_view_container.cpp
|
src/game_graphics/deckview/deck_view_container.cpp
|
||||||
src/game_graphics/deckview/tabbed_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_create_token.cpp
|
||||||
src/game_graphics/dialogs/dlg_move_top_cards_until.cpp
|
src/game_graphics/dialogs/dlg_move_top_cards_until.cpp
|
||||||
src/game_graphics/dialogs/dlg_roll_dice.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_graphics/game_scene.cpp
|
||||||
src/game/game_state.cpp
|
|
||||||
src/game_graphics/game_view.cpp
|
src/game_graphics/game_view.cpp
|
||||||
src/game_graphics/hand_counter.cpp
|
src/game_graphics/hand_counter.cpp
|
||||||
src/game_graphics/log/message_log_widget.cpp
|
src/game_graphics/log/message_log_widget.cpp
|
||||||
src/game/phase.cpp
|
|
||||||
src/game_graphics/phases_toolbar.cpp
|
src/game_graphics/phases_toolbar.cpp
|
||||||
src/game_graphics/player/menu/card_menu.cpp
|
src/game_graphics/player/menu/card_menu.cpp
|
||||||
src/game_graphics/player/menu/custom_zone_menu.cpp
|
src/game_graphics/player/menu/custom_zone_menu.cpp
|
||||||
|
|
@ -115,23 +107,11 @@ set(cockatrice_SOURCES
|
||||||
src/game_graphics/tally/stats_tally.cpp
|
src/game_graphics/tally/stats_tally.cpp
|
||||||
src/game_graphics/tally/subtype_tally.cpp
|
src/game_graphics/tally/subtype_tally.cpp
|
||||||
src/game_graphics/tally/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_area.cpp
|
||||||
src/game_graphics/player/player_dialogs.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_graphics/player/player_graphics_item.cpp
|
||||||
src/game/player/player_info.cpp
|
|
||||||
src/game_graphics/player/player_list_widget.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_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/card_zone.cpp
|
||||||
src/game_graphics/zones/hand_zone.cpp
|
src/game_graphics/zones/hand_zone.cpp
|
||||||
src/game_graphics/zones/pile_zone.cpp
|
src/game_graphics/zones/pile_zone.cpp
|
||||||
|
|
@ -141,6 +121,8 @@ set(cockatrice_SOURCES
|
||||||
src/game_graphics/zones/view_zone.cpp
|
src/game_graphics/zones/view_zone.cpp
|
||||||
src/game_graphics/zones/view_zone_widget.cpp
|
src/game_graphics/zones/view_zone_widget.cpp
|
||||||
src/game_graphics/board/abstract_graphics_item.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.cpp
|
||||||
src/interface/card_database_update/card_database_update_status_bar.h
|
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.cpp
|
||||||
|
|
@ -172,6 +154,7 @@ set(cockatrice_SOURCES
|
||||||
src/interface/palette_editor/palette_generator.cpp
|
src/interface/palette_editor/palette_generator.cpp
|
||||||
src/interface/palette_editor/quick_setup_panel.cpp
|
src/interface/palette_editor/quick_setup_panel.cpp
|
||||||
src/interface/palette_editor/palette_grid_widget.cpp
|
src/interface/palette_editor/palette_grid_widget.cpp
|
||||||
|
src/interface/palette_editor/font_grid_widget.cpp
|
||||||
src/interface/palette_editor/palette_editor_dialog.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/color_identity_widget.cpp
|
||||||
src/interface/widgets/cards/additional_info/deck_color_identity.cpp
|
src/interface/widgets/cards/additional_info/deck_color_identity.cpp
|
||||||
|
|
@ -189,6 +172,7 @@ set(cockatrice_SOURCES
|
||||||
src/interface/widgets/cards/card_info_picture_widget.cpp
|
src/interface/widgets/cards/card_info_picture_widget.cpp
|
||||||
src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.cpp
|
src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.cpp
|
||||||
src/interface/widgets/cards/card_info_text_widget.cpp
|
src/interface/widgets/cards/card_info_text_widget.cpp
|
||||||
|
src/interface/widgets/cards/card_picture_delegate.cpp
|
||||||
src/interface/widgets/cards/card_size_widget.cpp
|
src/interface/widgets/cards/card_size_widget.cpp
|
||||||
src/interface/widgets/cards/deck_card_zone_display_widget.cpp
|
src/interface/widgets/cards/deck_card_zone_display_widget.cpp
|
||||||
src/interface/widgets/cards/deck_preview_card_picture_widget.cpp
|
src/interface/widgets/cards/deck_preview_card_picture_widget.cpp
|
||||||
|
|
@ -308,6 +292,7 @@ set(cockatrice_SOURCES
|
||||||
src/interface/widgets/utility/sequence_edit.cpp
|
src/interface/widgets/utility/sequence_edit.cpp
|
||||||
src/interface/widgets/utility/visibility_change_listener.cpp
|
src/interface/widgets/utility/visibility_change_listener.cpp
|
||||||
src/interface/widgets/utility/visibility_change_listener.h
|
src/interface/widgets/utility/visibility_change_listener.h
|
||||||
|
src/interface/widgets/visual_database_display/card_database_visual_view.cpp
|
||||||
src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp
|
src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp
|
||||||
src/interface/widgets/visual_database_display/visual_database_display_filter_save_load_widget.cpp
|
src/interface/widgets/visual_database_display/visual_database_display_filter_save_load_widget.cpp
|
||||||
src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp
|
src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp
|
||||||
|
|
@ -340,6 +325,7 @@ 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_tag_filter_widget.cpp
|
||||||
src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp
|
src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp
|
||||||
src/interface/window_main.cpp
|
src/interface/window_main.cpp
|
||||||
|
src/installer_close_server.cpp
|
||||||
src/main.cpp
|
src/main.cpp
|
||||||
src/single_instance_manager.cpp
|
src/single_instance_manager.cpp
|
||||||
src/interface/widgets/tabs/abstract_tab_deck_editor.cpp
|
src/interface/widgets/tabs/abstract_tab_deck_editor.cpp
|
||||||
|
|
@ -445,6 +431,7 @@ set(cockatrice_SOURCES
|
||||||
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.h
|
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.h
|
||||||
src/interface/widgets/utility/compact_push_button.cpp
|
src/interface/widgets/utility/compact_push_button.cpp
|
||||||
src/interface/widgets/utility/compact_push_button.h
|
src/interface/widgets/utility/compact_push_button.h
|
||||||
|
src/installer_close_server.h
|
||||||
src/single_instance_manager.h
|
src/single_instance_manager.h
|
||||||
src/client/url_scheme_event_filter.h
|
src/client/url_scheme_event_filter.h
|
||||||
src/interface/intents/intent_connect_to_server.cpp
|
src/interface/intents/intent_connect_to_server.cpp
|
||||||
|
|
@ -506,7 +493,7 @@ if(APPLE)
|
||||||
set(cockatrice_SOURCES ${cockatrice_SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/resources/appicon.icns)
|
set(cockatrice_SOURCES ${cockatrice_SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/resources/appicon.icns)
|
||||||
endif(APPLE)
|
endif(APPLE)
|
||||||
|
|
||||||
qt6_add_resources(cockatrice_RESOURCES_RCC ${cockatrice_RESOURCES})
|
qt_add_resources(cockatrice_RESOURCES_RCC ${cockatrice_RESOURCES})
|
||||||
|
|
||||||
# Declare path variables
|
# Declare path variables
|
||||||
set(ICONDIR
|
set(ICONDIR
|
||||||
|
|
@ -527,7 +514,7 @@ set(COCKATRICE_MAC_QM_INSTALL_DIR "cockatrice.app/Contents/Resources/translation
|
||||||
set(COCKATRICE_UNIX_QM_INSTALL_DIR "share/cockatrice/translations")
|
set(COCKATRICE_UNIX_QM_INSTALL_DIR "share/cockatrice/translations")
|
||||||
set(COCKATRICE_WIN32_QM_INSTALL_DIR "translations")
|
set(COCKATRICE_WIN32_QM_INSTALL_DIR "translations")
|
||||||
|
|
||||||
qt6_add_executable(
|
qt_add_executable(
|
||||||
cockatrice
|
cockatrice
|
||||||
WIN32
|
WIN32
|
||||||
MACOSX_BUNDLE
|
MACOSX_BUNDLE
|
||||||
|
|
@ -539,7 +526,7 @@ qt6_add_executable(
|
||||||
|
|
||||||
target_precompile_headers(cockatrice PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtwidgets_pch.h")
|
target_precompile_headers(cockatrice PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtwidgets_pch.h")
|
||||||
|
|
||||||
qt6_add_shaders(
|
qt_add_shaders(
|
||||||
cockatrice
|
cockatrice
|
||||||
"onboarding_shaders"
|
"onboarding_shaders"
|
||||||
PREFIX
|
PREFIX
|
||||||
|
|
@ -551,7 +538,7 @@ qt6_add_shaders(
|
||||||
src/interface/widgets/onboarding/shaders/brand_plate.frag
|
src/interface/widgets/onboarding/shaders/brand_plate.frag
|
||||||
)
|
)
|
||||||
|
|
||||||
qt6_add_resources(
|
qt_add_resources(
|
||||||
cockatrice
|
cockatrice
|
||||||
"onboarding_qml"
|
"onboarding_qml"
|
||||||
PREFIX
|
PREFIX
|
||||||
|
|
@ -567,6 +554,7 @@ target_link_libraries(
|
||||||
PUBLIC libcockatrice_card
|
PUBLIC libcockatrice_card
|
||||||
libcockatrice_deck_list
|
libcockatrice_deck_list
|
||||||
libcockatrice_filters
|
libcockatrice_filters
|
||||||
|
libcockatrice_game
|
||||||
libcockatrice_utility
|
libcockatrice_utility
|
||||||
libcockatrice_network
|
libcockatrice_network
|
||||||
libcockatrice_models
|
libcockatrice_models
|
||||||
|
|
@ -575,6 +563,8 @@ target_link_libraries(
|
||||||
${COCKATRICE_QT_MODULES}
|
${COCKATRICE_QT_MODULES}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
cockatrice_target_enable_crash_reporting(cockatrice)
|
||||||
|
|
||||||
if(UNIX)
|
if(UNIX)
|
||||||
if(APPLE)
|
if(APPLE)
|
||||||
set(MACOSX_BUNDLE_INFO_STRING "${PROJECT_NAME}")
|
set(MACOSX_BUNDLE_INFO_STRING "${PROJECT_NAME}")
|
||||||
|
|
@ -781,7 +771,7 @@ endif()
|
||||||
if(Qt6LinguistTools_FOUND)
|
if(Qt6LinguistTools_FOUND)
|
||||||
#Qt6 Translations happen after the executable is built up
|
#Qt6 Translations happen after the executable is built up
|
||||||
if(UPDATE_TRANSLATIONS)
|
if(UPDATE_TRANSLATIONS)
|
||||||
qt6_add_translations(
|
qt_add_translations(
|
||||||
cockatrice
|
cockatrice
|
||||||
TS_FILES
|
TS_FILES
|
||||||
${cockatrice_TS}
|
${cockatrice_TS}
|
||||||
|
|
@ -791,7 +781,7 @@ if(Qt6LinguistTools_FOUND)
|
||||||
cockatrice_QM
|
cockatrice_QM
|
||||||
)
|
)
|
||||||
else()
|
else()
|
||||||
qt6_add_translations(cockatrice TS_FILES ${cockatrice_TS} QM_FILES_OUTPUT_VARIABLE cockatrice_QM)
|
qt_add_translations(cockatrice TS_FILES ${cockatrice_TS} QM_FILES_OUTPUT_VARIABLE cockatrice_QM)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(UNIX)
|
if(UNIX)
|
||||||
|
|
@ -817,4 +807,4 @@ if(Qt6LinguistTools_FOUND)
|
||||||
endforeach()
|
endforeach()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
qt6_finalize_target(cockatrice)
|
qt_finalize_target(cockatrice)
|
||||||
|
|
|
||||||
616
cockatrice/src/client/crash/crash_reporter.cpp
Normal file
616
cockatrice/src/client/crash/crash_reporter.cpp
Normal file
|
|
@ -0,0 +1,616 @@
|
||||||
|
#include "crash_reporter.h"
|
||||||
|
|
||||||
|
#if COCKATRICE_WITH_CRASH_REPORTING
|
||||||
|
#include <sentry.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QLockFile>
|
||||||
|
#include <QLoggingCategory>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QThread>
|
||||||
|
#include <QUrl>
|
||||||
|
#include <cstring>
|
||||||
|
#include <libcockatrice/settings/personal_settings.h>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
Q_LOGGING_CATEGORY(CrashReportLog, "crash_report");
|
||||||
|
|
||||||
|
#if COCKATRICE_WITH_CRASH_REPORTING
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
constexpr char CockatriceUrlScheme[] = "cockatrice://";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Qt-free cockatrice:// secret scrubber for the before_send hook.
|
||||||
|
*
|
||||||
|
* before_send does not run in async-signal context on our backends (breakpad invokes it on the
|
||||||
|
* crashing thread outside the signal handler, and crashpad/native crashes bypass the hook
|
||||||
|
* entirely), but it still avoids QString/QRegularExpression so it stays safe to call from the
|
||||||
|
* crash-handler thread. Truncates each URL at the first terminator, keeping scheme://host like
|
||||||
|
* redactCockatriceUrls does. (std::string allocates, so this is not strictly signal-safe.)
|
||||||
|
*/
|
||||||
|
std::string scrubCockatriceUrls(const char *input)
|
||||||
|
{
|
||||||
|
std::string out;
|
||||||
|
const char *pos = input;
|
||||||
|
const char *scheme = CockatriceUrlScheme;
|
||||||
|
const size_t schemeLen = std::strlen(scheme);
|
||||||
|
while (*pos != '\0') {
|
||||||
|
const char *found = std::strstr(pos, scheme);
|
||||||
|
if (found == nullptr) {
|
||||||
|
out.append(pos);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
out.append(pos, found);
|
||||||
|
const char *hostEnd = found + schemeLen;
|
||||||
|
while (*hostEnd != '\0' && *hostEnd != '/' && *hostEnd != '?' && *hostEnd != '#' && *hostEnd != ' ' &&
|
||||||
|
*hostEnd != '\t' && *hostEnd != '\n' && *hostEnd != '\'' && *hostEnd != '"' && *hostEnd != '<' &&
|
||||||
|
*hostEnd != '>') {
|
||||||
|
++hostEnd;
|
||||||
|
}
|
||||||
|
out.append(found, hostEnd);
|
||||||
|
pos = hostEnd;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
void scrubStringValue(sentry_value_t holder, const char *key)
|
||||||
|
{
|
||||||
|
sentry_value_t current = sentry_value_get_by_key(holder, key);
|
||||||
|
if (sentry_value_get_type(current) != SENTRY_VALUE_TYPE_STRING) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const char *raw = sentry_value_as_string(current);
|
||||||
|
if (raw == nullptr || std::strstr(raw, CockatriceUrlScheme) == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sentry_value_set_by_key(holder, key, sentry_value_new_string(scrubCockatriceUrls(raw).c_str()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Scrub trivially reachable string values inside a breadcrumb data object.
|
||||||
|
*/
|
||||||
|
int scrubDataEntry(const char *key, sentry_value_t value, void *userdata)
|
||||||
|
{
|
||||||
|
if (sentry_value_get_type(value) != SENTRY_VALUE_TYPE_STRING) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const char *raw = sentry_value_as_string(value);
|
||||||
|
if (raw == nullptr || std::strstr(raw, CockatriceUrlScheme) == nullptr) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
auto *holder = static_cast<sentry_value_t *>(userdata);
|
||||||
|
sentry_value_set_by_key(*holder, key, sentry_value_new_string(scrubCockatriceUrls(raw).c_str()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void scrubDataObject(sentry_value_t data)
|
||||||
|
{
|
||||||
|
if (sentry_value_get_type(data) != SENTRY_VALUE_TYPE_OBJECT) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sentry_value_foreach_key_value(data, scrubDataEntry, &data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void scrubCrumbList(sentry_value_t crumbs)
|
||||||
|
{
|
||||||
|
const size_t count = sentry_value_get_length(crumbs);
|
||||||
|
for (size_t i = 0; i < count; ++i) {
|
||||||
|
sentry_value_t crumb = sentry_value_get_by_index(crumbs, i);
|
||||||
|
if (sentry_value_get_type(crumb) != SENTRY_VALUE_TYPE_OBJECT) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
scrubStringValue(crumb, "message");
|
||||||
|
scrubDataObject(sentry_value_get_by_key(crumb, "data"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void scrubBreadcrumbs(sentry_value_t event)
|
||||||
|
{
|
||||||
|
// The SDK merges scope breadcrumbs onto the event as a plain list (sentry__value_merge_breadcrumbs
|
||||||
|
// in sentry_scope.c); the { "values": [...] } envelope shape only exists on the wire. Accept both
|
||||||
|
// so the scrubbing runs on the actual merged form.
|
||||||
|
sentry_value_t breadcrumbs = sentry_value_get_by_key(event, "breadcrumbs");
|
||||||
|
if (sentry_value_get_type(breadcrumbs) == SENTRY_VALUE_TYPE_LIST) {
|
||||||
|
scrubCrumbList(breadcrumbs);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (sentry_value_get_type(breadcrumbs) == SENTRY_VALUE_TYPE_OBJECT) {
|
||||||
|
sentry_value_t values = sentry_value_get_by_key(breadcrumbs, "values");
|
||||||
|
if (sentry_value_get_type(values) == SENTRY_VALUE_TYPE_LIST) {
|
||||||
|
scrubCrumbList(values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief before_send hook: scrub cockatrice:// secrets from the event.
|
||||||
|
*
|
||||||
|
* Defense in depth — payloads we build are pre-redacted. No breadcrumbs are recorded today (the Qt
|
||||||
|
* integration stays off), so this currently covers the top-level message and logentry.formatted,
|
||||||
|
* plus any breadcrumb messages/data that reach the merged event in the future. Native crash
|
||||||
|
* minidumps bypass this hook regardless of backend: breakpad captures them in-process without
|
||||||
|
* invoking before_send, and fast-fail crashes on Windows skip the local handler, so before_send is
|
||||||
|
* never invoked for those.
|
||||||
|
*/
|
||||||
|
sentry_value_t scrubEventBeforeSend(sentry_value_t event, sentry_hint_t *hint, void *userData)
|
||||||
|
{
|
||||||
|
Q_UNUSED(hint);
|
||||||
|
Q_UNUSED(userData);
|
||||||
|
scrubStringValue(event, "message");
|
||||||
|
sentry_value_t logentry = sentry_value_get_by_key(event, "logentry");
|
||||||
|
if (sentry_value_get_type(logentry) == SENTRY_VALUE_TYPE_OBJECT) {
|
||||||
|
scrubStringValue(logentry, "formatted");
|
||||||
|
}
|
||||||
|
scrubBreadcrumbs(event);
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief DSN matcher shared by isDsnWellFormed() and describeDsn().
|
||||||
|
*
|
||||||
|
* Kept unanchored on purpose, mirroring the MATCHES in cmake/CrashReporting.cmake (CMake searches
|
||||||
|
* rather than anchoring), so the client never rejects a DSN the build itself baked in. Compiled once
|
||||||
|
* because QRegularExpression construction is not free and both helpers are called per pending
|
||||||
|
* envelope.
|
||||||
|
*/
|
||||||
|
const QRegularExpression &dsnPattern()
|
||||||
|
{
|
||||||
|
static const QRegularExpression pattern(QStringLiteral("(https?)://([^@]+)@([^/]+)/([0-9]+)"));
|
||||||
|
return pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool CrashReporter::isDsnWellFormed(const QString &dsn)
|
||||||
|
{
|
||||||
|
return dsnPattern().match(dsn).hasMatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString CrashReporter::describeDsn(const QString &dsn)
|
||||||
|
{
|
||||||
|
const QRegularExpressionMatch match = dsnPattern().match(dsn);
|
||||||
|
if (!match.hasMatch()) {
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
// Host and project id only: the key in group 2 must never reach a log line.
|
||||||
|
return QStringLiteral("%1 (project %2)").arg(match.captured(3), match.captured(4));
|
||||||
|
}
|
||||||
|
|
||||||
|
QString CrashReporter::describeOriginMismatch(const QString &envelopePath, const QString ¤tDsn)
|
||||||
|
{
|
||||||
|
const QString destination = describeDsn(currentDsn);
|
||||||
|
if (destination.isEmpty()) {
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
QFile file(envelopePath);
|
||||||
|
if (!file.open(QIODevice::ReadOnly)) {
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
// The envelope header is the first line and is small; the cap only keeps a corrupt file from
|
||||||
|
// being read in full.
|
||||||
|
const QByteArray headerLine = file.readLine(4096).trimmed();
|
||||||
|
if (headerLine.isEmpty() || !headerLine.startsWith('{')) {
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
const QString originDsn = QJsonDocument::fromJson(headerLine).object().value(QStringLiteral("dsn")).toString();
|
||||||
|
const QString origin = describeDsn(originDsn);
|
||||||
|
// Equal destinations are not a mismatch: a different key for the same host and project still
|
||||||
|
// uploads where the crash was captured.
|
||||||
|
if (origin.isEmpty() || origin == destination) {
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
return QStringLiteral("was captured for %1 but this session uploads with %2").arg(origin, destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CrashReporter::isReportingEnabled(const QString &dsn, PersonalSettings::CrashReportingMode consentMode)
|
||||||
|
{
|
||||||
|
return !dsn.isEmpty() && consentMode != PersonalSettings::CrashReportingMode::NeverSend;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CrashReporter::init(const QString &dsn,
|
||||||
|
const QString &release,
|
||||||
|
const QString &databasePath,
|
||||||
|
PersonalSettings::CrashReportingMode consentMode)
|
||||||
|
{
|
||||||
|
shutdown();
|
||||||
|
sentryDatabasePath = databasePath;
|
||||||
|
if (!isReportingEnabled(dsn, consentMode)) {
|
||||||
|
if (!dsn.isEmpty()) {
|
||||||
|
// A deliberate choice rather than a misconfiguration, so it is not a warning. The
|
||||||
|
// no-DSN case is reported by the caller, which is the only place that knows whether
|
||||||
|
// the environment or the build supplied the DSN.
|
||||||
|
qCInfo(CrashReportLog) << "crash reporting disabled by preference (Never send)";
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Only created when the SDK will actually run: no-DSN launches stay side-effect free.
|
||||||
|
#if COCKATRICE_WITH_CRASH_REPORTING
|
||||||
|
// A crash captured by an earlier run is uploaded by THIS session's transport, which posts every
|
||||||
|
// envelope with the DSN it was armed with (sentry-native keeps the DSN in the transport state,
|
||||||
|
// and envelopes restored from disk are raw, so their own "dsn" header is never read). When the
|
||||||
|
// two disagree the report lands in a project nobody chose, which no other log line reveals.
|
||||||
|
const QStringList pendingEnvelopes = pendingEnvelopePaths(sentryDatabasePath);
|
||||||
|
for (const QString &envelopePath : pendingEnvelopes) {
|
||||||
|
const QString mismatch = describeOriginMismatch(envelopePath, dsn);
|
||||||
|
if (!mismatch.isEmpty()) {
|
||||||
|
qCWarning(CrashReportLog) << "pending crash envelope" << envelopePath << mismatch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
qCInfo(CrashReportLog) << "crash reporting armed for" << describeDsn(dsn);
|
||||||
|
QDir().mkpath(sentryDatabasePath);
|
||||||
|
if (consentMode != PersonalSettings::CrashReportingMode::AlwaysSend) {
|
||||||
|
// sentry-native persists consent to <database>/user-consent and reloads it in sentry_init
|
||||||
|
// before old runs are processed, so a stale "given" from an earlier AlwaysSend run (or an
|
||||||
|
// earlier Send in Ask mode) would upload the next crash before the dialog asks. Our
|
||||||
|
// CrashReportingMode is the source of truth, so clear the persisted state first; asking
|
||||||
|
// afterwards re-establishes it via giveConsent()/revokeConsent().
|
||||||
|
QFile::remove(sentryDatabasePath + QStringLiteral("/user-consent"));
|
||||||
|
}
|
||||||
|
sentry_options_t *options = sentry_options_new();
|
||||||
|
sentry_options_set_dsn(options, dsn.toUtf8().constData());
|
||||||
|
sentry_options_set_release(options, release.toUtf8().constData());
|
||||||
|
sentry_options_set_database_path(options, sentryDatabasePath.toUtf8().constData());
|
||||||
|
// Consent-gated uploads: crashes are captured into the local database but
|
||||||
|
// nothing leaves the machine until giveConsent(). cache_keep holds the previous run's
|
||||||
|
// crash envelope in <database>/cache instead of dropping it at init, so the Ask dialog
|
||||||
|
// decides its fate: giveConsent() flushes it via the transport retry, revokeConsent()
|
||||||
|
// deletes it below. This also covers the first crash after switching to AlwaysSend,
|
||||||
|
// which giveConsent() (running after init) would otherwise miss. http_retry is required
|
||||||
|
// for the flush: without it the held envelope is written with a plain filename the retry
|
||||||
|
// scanner skips, and giveConsent()'s sentry_transport_retry() sends nothing.
|
||||||
|
sentry_options_set_require_user_consent(options, 1);
|
||||||
|
sentry_options_set_cache_keep(options, SENTRY_CACHE_KEEP_OFFLINE);
|
||||||
|
sentry_options_set_http_retry(options, 1);
|
||||||
|
sentry_options_set_before_send(options, scrubEventBeforeSend, nullptr);
|
||||||
|
ready = (sentry_init(options) == 0);
|
||||||
|
#else
|
||||||
|
Q_UNUSED(release);
|
||||||
|
// Not a misconfiguration like an empty DSN: this build cannot report whatever the DSN says, so
|
||||||
|
// a tester forcing a crash needs to know the difference.
|
||||||
|
qCInfo(CrashReportLog) << "crash reporting unavailable: built without Sentry support";
|
||||||
|
ready = false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CrashReporter::isReady() const
|
||||||
|
{
|
||||||
|
return ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CrashReporter::shutdown()
|
||||||
|
{
|
||||||
|
#if COCKATRICE_WITH_CRASH_REPORTING
|
||||||
|
if (ready) {
|
||||||
|
sentry_close();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
ready = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
CrashReporter::~CrashReporter() = default;
|
||||||
|
CrashReporter::CrashReporter() = default;
|
||||||
|
|
||||||
|
bool CrashReporter::beginSession(const QString &dir)
|
||||||
|
{
|
||||||
|
// Restartable: releasing first keeps begin->end->begin round-trips clean.
|
||||||
|
endSession();
|
||||||
|
|
||||||
|
QDir().mkpath(dir);
|
||||||
|
const QDir sessionDir(dir);
|
||||||
|
const QString selfName = QStringLiteral("session-%1").arg(QCoreApplication::applicationPid());
|
||||||
|
|
||||||
|
bool unclean = false;
|
||||||
|
const QStringList leftovers = sessionDir.entryList({QStringLiteral("session-*")}, QDir::Files);
|
||||||
|
for (const QString &name : leftovers) {
|
||||||
|
if (name == selfName) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const QString path = sessionDir.filePath(name);
|
||||||
|
QLockFile probe(path);
|
||||||
|
// Long-lived session lock: stale means "owner dead", regardless of file age, so a
|
||||||
|
// crash followed by an immediate relaunch (younger than the 30 s default) is still
|
||||||
|
// detected. tryLock() only deletes the file when it finds it stale, so a live
|
||||||
|
// concurrent instance's lock is never touched here. (The static
|
||||||
|
// QLockFile::removeStaleLockFile(path) form does not exist in Qt5/Qt6, and the
|
||||||
|
// member removeStaleLockFile() force-removes without a staleness check, so neither
|
||||||
|
// is usable for this scan.)
|
||||||
|
probe.setStaleLockTime(0);
|
||||||
|
if (probe.tryLock(0)) {
|
||||||
|
unclean = true;
|
||||||
|
probe.unlock(); // deletes the stale file
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// tryLock failed: either a live concurrent instance owns the file, or it is
|
||||||
|
// unreadable (garbage/empty) and therefore has no live owner. Only the latter is
|
||||||
|
// cleaned up; a live instance is left alone. On Unix QLockFile creates the file
|
||||||
|
// before writing the pid, so a second instance starting at the same moment can
|
||||||
|
// briefly observe an empty (or half-written) file for a live owner: re-read once
|
||||||
|
// after a beat so a live owner's fresh lock is never mistaken for garbage and deleted.
|
||||||
|
qint64 pid = 0;
|
||||||
|
QString hostname, appname;
|
||||||
|
if (!probe.getLockInfo(&pid, &hostname, &appname)) {
|
||||||
|
QThread::msleep(100);
|
||||||
|
if (!probe.getLockInfo(&pid, &hostname, &appname)) {
|
||||||
|
QFile::remove(path);
|
||||||
|
unclean = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionLockPath = sessionDir.filePath(selfName);
|
||||||
|
sessionLock = std::make_unique<QLockFile>(sessionLockPath);
|
||||||
|
sessionLock->setStaleLockTime(0);
|
||||||
|
if (!sessionLock->tryLock(0)) {
|
||||||
|
qCWarning(CrashReportLog) << "Failed to claim session lock at" << sessionLockPath;
|
||||||
|
}
|
||||||
|
return unclean;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CrashReporter::endSession()
|
||||||
|
{
|
||||||
|
if (sessionLock) {
|
||||||
|
// unlock() removes the lock file; nothing left to clean up.
|
||||||
|
sessionLock->unlock();
|
||||||
|
sessionLock.reset();
|
||||||
|
} else if (!sessionLockPath.isEmpty()) {
|
||||||
|
// The lock was never claimed (a live concurrent instance owns its file): only our
|
||||||
|
// own unclaimed file, if tryLock left one behind, is removed.
|
||||||
|
QFile::remove(sessionLockPath);
|
||||||
|
}
|
||||||
|
sessionLockPath.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CrashReporter::giveConsent()
|
||||||
|
{
|
||||||
|
#if COCKATRICE_WITH_CRASH_REPORTING
|
||||||
|
if (ready) {
|
||||||
|
sentry_user_consent_give();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void CrashReporter::revokeConsent()
|
||||||
|
{
|
||||||
|
#if COCKATRICE_WITH_CRASH_REPORTING
|
||||||
|
// Revoke (not reset): an explicit refusal blocks uploads; reset would
|
||||||
|
// return to unknown, which blocks uploads too but loses the decision.
|
||||||
|
if (ready) {
|
||||||
|
sentry_user_consent_revoke();
|
||||||
|
// Drop the held crash envelope: without this it would sit in <database>/cache and
|
||||||
|
// upload on the next give (e.g. after switching to AlwaysSend), after the user
|
||||||
|
// explicitly refused it. The SDK recreates the directory as needed.
|
||||||
|
QDir(sentryDatabasePath + QStringLiteral("/cache")).removeRecursively();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CrashReporter::sendReport(const QString &redactedPreview, const QString &userNote)
|
||||||
|
{
|
||||||
|
#if COCKATRICE_WITH_CRASH_REPORTING
|
||||||
|
if (!ready) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Local scope (not the global one): the log preview rides only on this event. A global
|
||||||
|
// sentry_attach_bytes would linger and ride along on every later event in the session,
|
||||||
|
// including a real native crash later on — and with the crashpad backend byte attachments
|
||||||
|
// are written to disk for exactly that. In-memory bytes also mean no report files land in
|
||||||
|
// the Sentry DB dir, so there is no disk growth and no same-second filename collision.
|
||||||
|
sentry_scope_t *scope = sentry_scope_new();
|
||||||
|
if (scope == nullptr) {
|
||||||
|
qCWarning(CrashReportLog) << "Could not create scope for crash report";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const QByteArray previewBytes = redactedPreview.toUtf8();
|
||||||
|
const sentry_uuid_t attachmentId = sentry_scope_attach_bytes(
|
||||||
|
scope, previewBytes.constData(), static_cast<size_t>(previewBytes.size()), "crash-log.txt");
|
||||||
|
const bool attachOk = sentry_uuid_is_nil(&attachmentId) == 0;
|
||||||
|
if (!attachOk) {
|
||||||
|
qCWarning(CrashReportLog) << "Could not attach crash log to crash report";
|
||||||
|
}
|
||||||
|
bool noteAttachOk = true;
|
||||||
|
if (!userNote.isEmpty()) {
|
||||||
|
const QByteArray noteBytes = userNote.toUtf8();
|
||||||
|
const sentry_uuid_t noteId = sentry_scope_attach_bytes(scope, noteBytes.constData(),
|
||||||
|
static_cast<size_t>(noteBytes.size()), "user-note.txt");
|
||||||
|
noteAttachOk = sentry_uuid_is_nil(¬eId) == 0;
|
||||||
|
if (!noteAttachOk) {
|
||||||
|
qCWarning(CrashReportLog) << "Could not attach user note to crash report";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release is attached automatically from the init options. Deliberately no
|
||||||
|
// sentry_set_user call: the report must carry no PII. The dialog collects
|
||||||
|
// no contact info, so the message event + attachments are the whole payload
|
||||||
|
// (sentry_value_new_feedback/sentry_capture_feedback exist but stay unused).
|
||||||
|
sentry_value_t event = sentry_value_new_message_event(SENTRY_LEVEL_ERROR, "crash-report", "Crash report");
|
||||||
|
const sentry_uuid_t eventId = sentry_scope_capture_event(scope, event, nullptr);
|
||||||
|
sentry_scope_free(scope);
|
||||||
|
if (sentry_uuid_is_nil(&eventId)) {
|
||||||
|
qCWarning(CrashReportLog) << "Failed to capture crash report event";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
char eventIdStr[37] = {};
|
||||||
|
sentry_uuid_as_string(&eventId, eventIdStr);
|
||||||
|
// Capture only queues the envelope to the background transport: a network failure stays
|
||||||
|
// invisible here, so true means "queued", not "delivered". The Save fallback is offered only
|
||||||
|
// when the local capture itself fails.
|
||||||
|
qCInfo(CrashReportLog) << "Crash report queued, event id:" << eventIdStr;
|
||||||
|
// A failed attachment still captures the event above, but reports failure
|
||||||
|
// so callers offer the local-save fallback instead of silent success.
|
||||||
|
return attachOk && noteAttachOk;
|
||||||
|
#else
|
||||||
|
Q_UNUSED(redactedPreview);
|
||||||
|
Q_UNUSED(userNote);
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CrashReporter::appendAttachmentToEnvelope(const QString &envelopePath,
|
||||||
|
const QString &filename,
|
||||||
|
const QByteArray &payload)
|
||||||
|
{
|
||||||
|
// QIODevice::ReadWrite creates a missing file; appending to a file that was never
|
||||||
|
// written by the SDK would fabricate an envelope, so require it to exist.
|
||||||
|
if (!QFile::exists(envelopePath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
QFile file(envelopePath);
|
||||||
|
if (!file.open(QIODevice::ReadWrite)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Item boundaries are single newlines: make sure the header starts on a fresh line.
|
||||||
|
if (file.size() > 0) {
|
||||||
|
if (!file.seek(file.size() - 1)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
char lastByte = 0;
|
||||||
|
if (file.read(&lastByte, 1) != 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (lastByte != '\n' && file.write("\n", 1) != 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Item shape mirrors sentry__envelope_add_attachment in the SDK. `length` is what the
|
||||||
|
// SDK's envelope re-parser uses to frame the payload (a missing length ends the item at
|
||||||
|
// the next newline, which would truncate a multi-line log and make the rest of the file
|
||||||
|
// fail to parse); `attachment_length` is the logical attachment size the SDK also emits.
|
||||||
|
// The filename is a compile-time ASCII constant from the callers, so no JSON escaping is
|
||||||
|
// needed.
|
||||||
|
const QByteArray header = QByteArrayLiteral("{\"type\":\"attachment\",\"length\":") +
|
||||||
|
QByteArray::number(payload.size()) + QByteArrayLiteral(",\"attachment_length\":") +
|
||||||
|
QByteArray::number(payload.size()) + QByteArrayLiteral(",\"filename\":\"") +
|
||||||
|
filename.toUtf8() + QByteArrayLiteral("\",\"content_type\":\"text/plain\"}") + '\n';
|
||||||
|
if (file.write(header) != header.size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return file.write(payload) == payload.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList CrashReporter::pendingEnvelopePaths(const QString &databasePath)
|
||||||
|
{
|
||||||
|
static const auto isUuid = [](const QString &id) { return id.size() == 36 && id.count(QLatin1Char('-')) == 4; };
|
||||||
|
static const auto isEnvelopeName = [](const QString &name) {
|
||||||
|
if (name == QStringLiteral("__sentry-crash.envelope")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const QString stem = name.left(name.size() - 9); // strip ".envelope"
|
||||||
|
if (isUuid(stem)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Retry cache filename: "<ts>-<count>-<uuid>".
|
||||||
|
const qsizetype firstSep = stem.indexOf(QLatin1Char('-'));
|
||||||
|
const qsizetype secondSep = firstSep > 0 ? stem.indexOf(QLatin1Char('-'), firstSep + 1) : -1;
|
||||||
|
return firstSep > 0 && secondSep > 0 && isUuid(stem.mid(secondSep + 1)) && stem.left(firstSep).toLongLong() > 0;
|
||||||
|
};
|
||||||
|
QStringList result;
|
||||||
|
const QDir dbDir(databasePath);
|
||||||
|
if (!dbDir.exists()) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
// Consent-held envelopes (Ask mode: the SDK cached them at crash time instead
|
||||||
|
// of sending, see sentry__submit_envelope). Bare "<uuid>.envelope" with
|
||||||
|
// cache_keep only, "<ts>-<count>-<uuid>.envelope" with http_retry.
|
||||||
|
const QDir cacheDir(databasePath + QStringLiteral("/cache"));
|
||||||
|
const QStringList cacheNames = cacheDir.entryList({QStringLiteral("*.envelope")}, QDir::Files, QDir::Name);
|
||||||
|
for (const QString &name : cacheNames) {
|
||||||
|
if (isEnvelopeName(name)) {
|
||||||
|
result.append(cacheDir.filePath(name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// In-flight envelopes dumped to the crashed run's own directory (AlwaysSend
|
||||||
|
// mode: consent was already given, so the transport dump wrote them there and
|
||||||
|
// sentry_init's process_old_runs replays them at startup — attach before init).
|
||||||
|
const QStringList runDirs =
|
||||||
|
dbDir.entryList({QStringLiteral("*.run")}, QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name);
|
||||||
|
for (const QString &runDir : runDirs) {
|
||||||
|
const QDir dir(dbDir.filePath(runDir));
|
||||||
|
const QStringList names = dir.entryList({QStringLiteral("*.envelope")}, QDir::Files, QDir::Name);
|
||||||
|
for (const QString &name : names) {
|
||||||
|
if (isEnvelopeName(name)) {
|
||||||
|
result.append(dir.filePath(name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
int CrashReporter::attachToPendingCrashEnvelopes(const QString &redactedLogText, const QString &userNoteText)
|
||||||
|
{
|
||||||
|
return attachToPendingCrashEnvelopes(sentryDatabasePath, redactedLogText, userNoteText);
|
||||||
|
}
|
||||||
|
|
||||||
|
int CrashReporter::attachToPendingCrashEnvelopes(const QString &databasePath,
|
||||||
|
const QString &redactedLogText,
|
||||||
|
const QString &userNoteText)
|
||||||
|
{
|
||||||
|
if (databasePath.isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const QByteArray logBytes = redactedLogText.toUtf8();
|
||||||
|
const QByteArray noteBytes = userNoteText.toUtf8();
|
||||||
|
int attached = 0;
|
||||||
|
const QStringList envelopes = pendingEnvelopePaths(databasePath);
|
||||||
|
for (const QString &path : envelopes) {
|
||||||
|
bool ok = appendAttachmentToEnvelope(path, QStringLiteral("crash-log.txt"), logBytes);
|
||||||
|
if (ok && !noteBytes.isEmpty()) {
|
||||||
|
ok = appendAttachmentToEnvelope(path, QStringLiteral("user-note.txt"), noteBytes);
|
||||||
|
}
|
||||||
|
if (ok) {
|
||||||
|
++attached;
|
||||||
|
} else {
|
||||||
|
qCWarning(CrashReportLog) << "Failed to attach crash log to pending envelope" << path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (attached > 0) {
|
||||||
|
qCInfo(CrashReportLog) << "Attached crash log and user note to" << attached << "pending crash envelope(s)";
|
||||||
|
}
|
||||||
|
return attached;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString CrashReporter::sanitizeLogLine(const QString &line)
|
||||||
|
{
|
||||||
|
static const auto colorEscapeCodePattern = QRegularExpression(QStringLiteral("\033\\[[0-9;]*m"));
|
||||||
|
|
||||||
|
QString sanitized = line;
|
||||||
|
sanitized.replace(colorEscapeCodePattern, "");
|
||||||
|
return sanitized;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString CrashReporter::redactCockatriceUrls(const QString &line)
|
||||||
|
{
|
||||||
|
static auto urlPattern = QRegularExpression(QStringLiteral("cockatrice://\\S+"));
|
||||||
|
|
||||||
|
// Single pass over the original line: rescanning the replaced text would match a redacted
|
||||||
|
// secret-free URL again and loop forever.
|
||||||
|
QString redacted;
|
||||||
|
redacted.reserve(line.size());
|
||||||
|
qsizetype pos = 0;
|
||||||
|
QRegularExpressionMatchIterator it = urlPattern.globalMatch(line);
|
||||||
|
while (it.hasNext()) {
|
||||||
|
QRegularExpressionMatch match = it.next();
|
||||||
|
redacted.append(line.mid(pos, match.capturedStart() - pos));
|
||||||
|
// The pattern only matches cockatrice:// URLs, so the check is folded into the parse:
|
||||||
|
// scheme://host, secrets dropped.
|
||||||
|
const QUrl parsed(match.captured(0));
|
||||||
|
redacted.append(parsed.scheme() + QStringLiteral("://") + parsed.host());
|
||||||
|
pos = match.capturedEnd();
|
||||||
|
}
|
||||||
|
redacted.append(line.mid(pos));
|
||||||
|
return redacted;
|
||||||
|
}
|
||||||
177
cockatrice/src/client/crash/crash_reporter.h
Normal file
177
cockatrice/src/client/crash/crash_reporter.h
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
/**
|
||||||
|
* @file crash_reporter.h
|
||||||
|
* @ingroup Core
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef CRASH_REPORTER_H
|
||||||
|
#define CRASH_REPORTER_H
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QLoggingCategory>
|
||||||
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <libcockatrice/settings/personal_settings.h>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#ifndef COCKATRICE_WITH_CRASH_REPORTING
|
||||||
|
#define COCKATRICE_WITH_CRASH_REPORTING 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef COCKATRICE_SENTRY_DSN
|
||||||
|
#define COCKATRICE_SENTRY_DSN ""
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef COCKATRICE_SENTRY_RELEASE
|
||||||
|
#define COCKATRICE_SENTRY_RELEASE ""
|
||||||
|
#endif
|
||||||
|
|
||||||
|
class QLockFile;
|
||||||
|
|
||||||
|
Q_DECLARE_LOGGING_CATEGORY(CrashReportLog);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Sentry-backed crash-report facade, owned by main().
|
||||||
|
*
|
||||||
|
* An empty DSN (or Never consent) skips the SDK entirely, so the mock dialog
|
||||||
|
* fallback behaves exactly as before. All SDK calls are guarded so OFF builds
|
||||||
|
* compile to no-ops.
|
||||||
|
*
|
||||||
|
* @par Consent gating
|
||||||
|
* init() arms require-user-consent and before_send scrubbing. Stale persisted
|
||||||
|
* consent from a previous AlwaysSend run is cleared before init when the mode is
|
||||||
|
* not AlwaysSend, and cached crash envelopes are held for the dialog instead of
|
||||||
|
* being dropped. giveConsent()/revokeConsent() gate uploads.
|
||||||
|
*
|
||||||
|
* @par Envelope and log helpers
|
||||||
|
* attachToPendingCrashEnvelopes() folds the redacted log and the optional user
|
||||||
|
* note into the pending native crash event(s) before consent releases them, so
|
||||||
|
* dump, stack, log and note arrive as ONE Sentry event. sendReport() is the
|
||||||
|
* fallback for unclean exits that produced no native crash event, capturing a
|
||||||
|
* redacted message event with the log preview attached. The log redaction
|
||||||
|
* helpers are static and SDK-independent.
|
||||||
|
*
|
||||||
|
* @par Session tracking
|
||||||
|
* beginSession()/endSession() are SDK-independent: a QLockFile per process in
|
||||||
|
* the database dir detects unclean exits cross-platform, replacing the legacy
|
||||||
|
* Windows-only .dmp scan. The held lock makes the reporter non-copyable and
|
||||||
|
* non-movable.
|
||||||
|
*/
|
||||||
|
class CrashReporter
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
CrashReporter();
|
||||||
|
CrashReporter(const CrashReporter &) = delete;
|
||||||
|
CrashReporter &operator=(const CrashReporter &) = delete;
|
||||||
|
CrashReporter(CrashReporter &&) = delete;
|
||||||
|
CrashReporter &operator=(CrashReporter &&) = delete;
|
||||||
|
~CrashReporter();
|
||||||
|
|
||||||
|
void init(const QString &dsn,
|
||||||
|
const QString &release,
|
||||||
|
const QString &databasePath,
|
||||||
|
PersonalSettings::CrashReportingMode consentMode);
|
||||||
|
[[nodiscard]] bool isReady() const;
|
||||||
|
void shutdown();
|
||||||
|
void giveConsent();
|
||||||
|
void revokeConsent();
|
||||||
|
bool sendReport(const QString &redactedPreview, const QString &userNote = QString());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Append the redacted log (and optional user note) to every pending
|
||||||
|
* native crash envelope in the local database, so the crashed event uploads
|
||||||
|
* with dump, stack, log and note together instead of the log arriving as a
|
||||||
|
* separate event that has to be correlated by hand. Both storage locations
|
||||||
|
* are covered: consent-held envelopes in <database>/cache (Ask mode) and
|
||||||
|
* in-flight envelopes dumped into <database>/<run>.run (AlwaysSend mode).
|
||||||
|
*
|
||||||
|
* For the AlwaysSend variant run this BEFORE init() (static overload): the
|
||||||
|
* startup replay of *.run envelopes happens inside sentry_init. SDK-independent
|
||||||
|
* plain-file surgery on the Sentry envelope format, so it stays unit-testable
|
||||||
|
* in OFF builds.
|
||||||
|
*
|
||||||
|
* @return the number of envelopes that received the attachments.
|
||||||
|
*/
|
||||||
|
int attachToPendingCrashEnvelopes(const QString &redactedLogText, const QString &userNoteText);
|
||||||
|
static int attachToPendingCrashEnvelopes(const QString &databasePath,
|
||||||
|
const QString &redactedLogText,
|
||||||
|
const QString &userNoteText);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Append one attachment item to a cached Sentry envelope file.
|
||||||
|
*
|
||||||
|
* An envelope is a newline-separated stream of item-header-line + payload
|
||||||
|
* pairs. The retry transport replays cached envelopes byte-for-byte
|
||||||
|
* (sentry-native 0.17.3 reads them raw and serializes raw envelopes back
|
||||||
|
* verbatim), so appending well-formed items associates them with the event
|
||||||
|
* already in the file; the first line (event id header) is never touched.
|
||||||
|
*
|
||||||
|
* @param filename ASCII-only item filename, embedded verbatim in the header.
|
||||||
|
*/
|
||||||
|
static bool
|
||||||
|
appendAttachmentToEnvelope(const QString &envelopePath, const QString &filename, const QByteArray &payload);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Pending crash envelopes in <database>: the consent-held files in
|
||||||
|
* <database>/cache and the in-flight files in <database>/<run>.run.
|
||||||
|
*
|
||||||
|
* Matches the cache filename conventions sentry-native accepts (see
|
||||||
|
* sentry__parse_cache_filename): bare "<uuid>.envelope" and retry
|
||||||
|
* "<ts>-<count>-<uuid>.envelope"; run dirs accept plain "<uuid>.envelope"
|
||||||
|
* minus the SDK's own "__sentry-crash.envelope" marker.
|
||||||
|
*/
|
||||||
|
static QStringList pendingEnvelopePaths(const QString &databasePath);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief SDK-independent gating decision, unit-testable in OFF builds.
|
||||||
|
*/
|
||||||
|
static bool isReportingEnabled(const QString &dsn, PersonalSettings::CrashReportingMode consentMode);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether a DSN has the shape the build and the SDK require:
|
||||||
|
* <scheme>://<key>@<host>/<project-id>.
|
||||||
|
*
|
||||||
|
* Deliberately the same unanchored pattern cmake/CrashReporting.cmake matches (CMake's MATCHES
|
||||||
|
* searches rather than anchoring), so a DSN this build accepted is never rejected here. The SDK
|
||||||
|
* refuses to even build a request for a DSN it considers invalid, so an unchecked typo disables
|
||||||
|
* uploading with no error anywhere.
|
||||||
|
*/
|
||||||
|
static bool isDsnWellFormed(const QString &dsn);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Human-readable destination of a DSN, e.g. "o0.ingest.sentry.io (project 1234567)".
|
||||||
|
*
|
||||||
|
* Safe to log: the public key is never part of the result. Empty for a malformed DSN.
|
||||||
|
*/
|
||||||
|
static QString describeDsn(const QString &dsn);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Describe the captured-for DSN of a pending envelope when it points at a different
|
||||||
|
* destination than the DSN this session would upload it with; empty when they agree.
|
||||||
|
*
|
||||||
|
* sentry-native posts every envelope with the DSN held in the transport state of the session
|
||||||
|
* doing the sending (envelopes restored from disk are raw and their own "dsn" header is never
|
||||||
|
* read), so a cached crash envelope captured for one project can silently be uploaded to
|
||||||
|
* another - the exact way a tester's report lands in the wrong place. Compares destinations
|
||||||
|
* (host and project id), not raw strings, so a different key for the same project stays quiet.
|
||||||
|
*/
|
||||||
|
static QString describeOriginMismatch(const QString &envelopePath, const QString ¤tDsn);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Claim this process's session lock, reporting a previous unclean exit.
|
||||||
|
* @return true if a leftover session lock from a dead process (or an unreadable
|
||||||
|
* garbage file) was found and cleaned; live concurrent instances are ignored.
|
||||||
|
*/
|
||||||
|
bool beginSession(const QString &dir);
|
||||||
|
void endSession();
|
||||||
|
|
||||||
|
static QString sanitizeLogLine(const QString &line);
|
||||||
|
static QString redactCockatriceUrls(const QString &line);
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool ready = false;
|
||||||
|
QString sentryDatabasePath;
|
||||||
|
QString sessionLockPath;
|
||||||
|
std::unique_ptr<QLockFile> sessionLock;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QPoint>
|
#include <QPoint>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
#include <libcockatrice/utility/qt_utils.h>
|
||||||
#include <qnamespace.h>
|
#include <qnamespace.h>
|
||||||
|
|
||||||
LatencyStatusWidget::LatencyStatusWidget(QWidget *parent) : QWidget(parent)
|
LatencyStatusWidget::LatencyStatusWidget(QWidget *parent) : QWidget(parent)
|
||||||
|
|
@ -24,9 +25,7 @@ LatencyStatusWidget::LatencyStatusWidget(QWidget *parent) : QWidget(parent)
|
||||||
latencyGraph = new LatencyGraphWidget(this);
|
latencyGraph = new LatencyGraphWidget(this);
|
||||||
latencyGraph->setFixedSize(90, 14);
|
latencyGraph->setFixedSize(90, 14);
|
||||||
|
|
||||||
auto *layout = new QHBoxLayout(this);
|
auto *layout = QtUtils::makeLayout<QHBoxLayout>(this, 4);
|
||||||
layout->setContentsMargins(0, 0, 0, 0);
|
|
||||||
layout->setSpacing(4);
|
|
||||||
layout->addWidget(latencyGraph);
|
layout->addWidget(latencyGraph);
|
||||||
layout->addWidget(pingLabel);
|
layout->addWidget(pingLabel);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
#include "../../../interface/pixel_map_generator.h"
|
#include "../../../interface/pixel_map_generator.h"
|
||||||
#include "../../settings/cache_settings.h"
|
#include "../../settings/cache_settings.h"
|
||||||
#include "../interface/widgets/dialogs/dlg_connect.h"
|
#include "../interface/widgets/dialogs/dlg_connect.h"
|
||||||
|
#include "../interface/widgets/dialogs/dlg_edit_password.h"
|
||||||
#include "../interface/widgets/dialogs/dlg_forgot_password_challenge.h"
|
#include "../interface/widgets/dialogs/dlg_forgot_password_challenge.h"
|
||||||
#include "../interface/widgets/dialogs/dlg_forgot_password_request.h"
|
#include "../interface/widgets/dialogs/dlg_forgot_password_request.h"
|
||||||
#include "../interface/widgets/dialogs/dlg_forgot_password_reset.h"
|
#include "../interface/widgets/dialogs/dlg_forgot_password_reset.h"
|
||||||
|
|
@ -24,6 +25,7 @@
|
||||||
#include <libcockatrice/network/client/remote/remote_client.h>
|
#include <libcockatrice/network/client/remote/remote_client.h>
|
||||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||||
#include <libcockatrice/settings/servers_settings.h>
|
#include <libcockatrice/settings/servers_settings.h>
|
||||||
|
#include <libcockatrice/utility/qt_utils.h>
|
||||||
#include <qnamespace.h>
|
#include <qnamespace.h>
|
||||||
|
|
||||||
class QWidget;
|
class QWidget;
|
||||||
|
|
@ -95,6 +97,13 @@ void ConnectionController::wireClientSignals()
|
||||||
|
|
||||||
connect(remoteClient, &RemoteClient::sigPromptForForgotPasswordChallenge, this,
|
connect(remoteClient, &RemoteClient::sigPromptForForgotPasswordChallenge, this,
|
||||||
&ConnectionController::onPromptForgotPasswordChallenge);
|
&ConnectionController::onPromptForgotPasswordChallenge);
|
||||||
|
|
||||||
|
connect(remoteClient, &RemoteClient::sigPasswordVerifierReady, this,
|
||||||
|
&ConnectionController::onPasswordVerifierReady);
|
||||||
|
|
||||||
|
connect(remoteClient, &RemoteClient::sigPasswordChangeSuccess, this,
|
||||||
|
&ConnectionController::onPasswordChangeSuccess);
|
||||||
|
connect(remoteClient, &RemoteClient::sigPasswordChangeError, this, &ConnectionController::onPasswordChangeError);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConnectionController::connectToServer()
|
void ConnectionController::connectToServer()
|
||||||
|
|
@ -103,16 +112,32 @@ void ConnectionController::connectToServer()
|
||||||
connect(dlgConnect, &DlgConnect::sigStartForgotPasswordRequest, this, &ConnectionController::forgotPasswordRequest);
|
connect(dlgConnect, &DlgConnect::sigStartForgotPasswordRequest, this, &ConnectionController::forgotPasswordRequest);
|
||||||
|
|
||||||
if (dlgConnect->exec()) {
|
if (dlgConnect->exec()) {
|
||||||
|
pendingSaveName = dlgConnect->getSaveName();
|
||||||
|
pendingSavePassword = dlgConnect->getSavePassword();
|
||||||
|
remoteClient->setStoredVerifier(dlgConnect->getStoredVerifier());
|
||||||
remoteClient->connectToServer(dlgConnect->getHost(), static_cast<unsigned int>(dlgConnect->getPort()),
|
remoteClient->connectToServer(dlgConnect->getHost(), static_cast<unsigned int>(dlgConnect->getPort()),
|
||||||
dlgConnect->getPlayerName(), dlgConnect->getPassword());
|
dlgConnect->getPlayerName(), dlgConnect->getPassword());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ConnectionController::onPasswordVerifierReady(const QString &verifier)
|
||||||
|
{
|
||||||
|
if (pendingSavePassword) {
|
||||||
|
SettingsCache::instance().servers().setServerPassword(pendingSaveName, verifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void ConnectionController::connectToServerDirect(const QString &host,
|
void ConnectionController::connectToServerDirect(const QString &host,
|
||||||
unsigned int port,
|
unsigned int port,
|
||||||
const QString &playerName,
|
const QString &playerName,
|
||||||
const QString &password)
|
const QString &password,
|
||||||
|
const QString &storedVerifier,
|
||||||
|
const QString &saveName,
|
||||||
|
bool savePassword)
|
||||||
{
|
{
|
||||||
|
pendingSaveName = saveName;
|
||||||
|
pendingSavePassword = savePassword;
|
||||||
|
remoteClient->setStoredVerifier(storedVerifier);
|
||||||
remoteClient->connectToServer(host, port, playerName, password);
|
remoteClient->connectToServer(host, port, playerName, password);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -224,17 +249,27 @@ void ConnectionController::onLoginError(int r,
|
||||||
{
|
{
|
||||||
switch (static_cast<Response::ResponseCode>(r)) {
|
switch (static_cast<Response::ResponseCode>(r)) {
|
||||||
case Response::RespClientUpdateRequired: {
|
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);
|
QMessageBox msgBox(dialogParent);
|
||||||
msgBox.setIcon(QMessageBox::Critical);
|
msgBox.setIcon(QMessageBox::Critical);
|
||||||
msgBox.setWindowTitle(tr("Failed Login"));
|
msgBox.setWindowTitle(tr("Failed Login"));
|
||||||
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'."));
|
if (missingFeatures.isEmpty()) {
|
||||||
msgBox.setDetailedText(formatted);
|
// 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.exec();
|
msgBox.exec();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -281,9 +316,8 @@ void ConnectionController::onLoginError(int r,
|
||||||
}
|
}
|
||||||
|
|
||||||
case Response::RespRegistrationRequired: {
|
case Response::RespRegistrationRequired: {
|
||||||
if (QMessageBox::question(dialogParent, tr("Error"),
|
if (QtUtils::confirm(dialogParent, tr("Error"),
|
||||||
tr("This server requires user registration. Do you want to register now?"),
|
tr("This server requires user registration. Do you want to register now?"))) {
|
||||||
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
|
|
||||||
registerToServer();
|
registerToServer();
|
||||||
}
|
}
|
||||||
return; // don't re-prompt connect
|
return; // don't re-prompt connect
|
||||||
|
|
@ -322,10 +356,16 @@ void ConnectionController::onLoginError(int r,
|
||||||
}
|
}
|
||||||
|
|
||||||
case Response::RespPasswordChangeRequired: {
|
case Response::RespPasswordChangeRequired: {
|
||||||
QMessageBox::information(
|
// The credentials were right, but the account may not be used until a new password
|
||||||
dialogParent, tr("Password Change Required"),
|
// is chosen. Let the user complete the change here: sending them off to "ask the
|
||||||
tr("An administrator has reset your password. Please contact your server administrator to obtain "
|
// administrator for your temporary password" sends them in circles, since they just
|
||||||
"your temporary password, then log in and change it via Account -> Change Password."));
|
// logged in with it and the session ends without a way to comply.
|
||||||
|
DlgEditPassword dlg(dialogParent);
|
||||||
|
dlg.setWindowTitle(tr("Password Change Required"));
|
||||||
|
if (dlg.exec()) {
|
||||||
|
remoteClient->changePasswordToServer(dlg.getOldPassword(), dlg.getNewPassword());
|
||||||
|
return; // onPasswordChangeSuccess/onPasswordChangeError decides the next step
|
||||||
|
}
|
||||||
remoteClient->disconnectFromServer();
|
remoteClient->disconnectFromServer();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -488,6 +528,43 @@ void ConnectionController::onNotifyUserAboutUpdate()
|
||||||
"To update your client, go to Help -> Check for Updates."));
|
"To update your client, go to Help -> Check for Updates."));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ConnectionController::onPasswordChangeSuccess()
|
||||||
|
{
|
||||||
|
// The credential saved for this profile is stale now: drop it so the next login asks for
|
||||||
|
// the new password instead of replaying the old verifier.
|
||||||
|
if (!pendingSaveName.isEmpty()) {
|
||||||
|
SettingsCache::instance().servers().setServerPassword(pendingSaveName, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
QMessageBox::information(dialogParent, tr("Password Changed"),
|
||||||
|
tr("Your password has been changed. Please log in again with your new password."));
|
||||||
|
remoteClient->disconnectFromServer();
|
||||||
|
connectToServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConnectionController::onPasswordChangeError(int responseCode)
|
||||||
|
{
|
||||||
|
QString reason;
|
||||||
|
switch (static_cast<Response::ResponseCode>(responseCode)) {
|
||||||
|
case Response::RespWrongPassword:
|
||||||
|
reason = tr("The current password is incorrect.");
|
||||||
|
break;
|
||||||
|
case Response::RespPasswordTooShort:
|
||||||
|
reason = tr("The new password is too short.");
|
||||||
|
break;
|
||||||
|
case Response::RespClientUpdateRequired:
|
||||||
|
reason = tr("The server does not accept the new password format.");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
reason = tr("The password could not be changed (error %1).").arg(responseCode);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
QMessageBox::critical(dialogParent, tr("Password Change Failed"), reason);
|
||||||
|
remoteClient->disconnectFromServer();
|
||||||
|
connectToServer();
|
||||||
|
}
|
||||||
|
|
||||||
void ConnectionController::onForgotPasswordSuccess()
|
void ConnectionController::onForgotPasswordSuccess()
|
||||||
{
|
{
|
||||||
QMessageBox::information(
|
QMessageBox::information(
|
||||||
|
|
@ -498,14 +575,38 @@ void ConnectionController::onForgotPasswordSuccess()
|
||||||
SettingsCache::instance().servers().setFPPlayerName("");
|
SettingsCache::instance().servers().setFPPlayerName("");
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConnectionController::onForgotPasswordError()
|
void ConnectionController::onForgotPasswordError(int responseCode)
|
||||||
{
|
{
|
||||||
QMessageBox::warning(
|
// Rate limiting is retryable, so it must not clear the prefill.
|
||||||
dialogParent, tr("Reset Password"),
|
bool keepPrefill = false;
|
||||||
tr("Failed to reset user account password, please contact the server operator to reset your password."));
|
QString message;
|
||||||
SettingsCache::instance().servers().setFPHostName("");
|
switch (static_cast<Response::ResponseCode>(responseCode)) {
|
||||||
SettingsCache::instance().servers().setFPPort("");
|
case Response::RespTooManyRequests:
|
||||||
SettingsCache::instance().servers().setFPPlayerName("");
|
// 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("");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConnectionController::onPromptForgotPasswordReset()
|
void ConnectionController::onPromptForgotPasswordReset()
|
||||||
|
|
|
||||||
|
|
@ -40,8 +40,13 @@ public:
|
||||||
void registerToServer();
|
void registerToServer();
|
||||||
void forgotPasswordRequest();
|
void forgotPasswordRequest();
|
||||||
void connectToServer();
|
void connectToServer();
|
||||||
void
|
void connectToServerDirect(const QString &host,
|
||||||
connectToServerDirect(const QString &host, unsigned int port, const QString &playerName, const QString &password);
|
unsigned int port,
|
||||||
|
const QString &playerName,
|
||||||
|
const QString &password,
|
||||||
|
const QString &storedVerifier = QString(),
|
||||||
|
const QString &saveName = QString(),
|
||||||
|
bool savePassword = false);
|
||||||
void disconnectFromServer();
|
void disconnectFromServer();
|
||||||
|
|
||||||
void refreshWindowTitle()
|
void refreshWindowTitle()
|
||||||
|
|
@ -82,10 +87,17 @@ private slots:
|
||||||
|
|
||||||
// Forgot-password flow
|
// Forgot-password flow
|
||||||
void onForgotPasswordSuccess();
|
void onForgotPasswordSuccess();
|
||||||
void onForgotPasswordError();
|
void onForgotPasswordError(int responseCode);
|
||||||
void onPromptForgotPasswordReset();
|
void onPromptForgotPasswordReset();
|
||||||
void onPromptForgotPasswordChallenge();
|
void onPromptForgotPasswordChallenge();
|
||||||
|
|
||||||
|
// Persists the derived scrypt verifier after a successful challenge-response login
|
||||||
|
void onPasswordVerifierReady(const QString &verifier);
|
||||||
|
|
||||||
|
// Completes the forced password change the server demanded at login
|
||||||
|
void onPasswordChangeSuccess();
|
||||||
|
void onPasswordChangeError(int responseCode);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void wireClientSignals();
|
void wireClientSignals();
|
||||||
void updateWindowTitle();
|
void updateWindowTitle();
|
||||||
|
|
@ -102,6 +114,10 @@ private:
|
||||||
|
|
||||||
// Kept as a member so the forgot-password signal can be wired to it
|
// Kept as a member so the forgot-password signal can be wired to it
|
||||||
DlgConnect *dlgConnect{nullptr};
|
DlgConnect *dlgConnect{nullptr};
|
||||||
|
|
||||||
|
// Captured from the connect dialog when a connection is initiated
|
||||||
|
QString pendingSaveName;
|
||||||
|
bool pendingSavePassword{false};
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // COCKATRICE_REMOTE_CONNECTION_CONTROLLER_H
|
#endif // COCKATRICE_REMOTE_CONNECTION_CONTROLLER_H
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,12 @@
|
||||||
#include <libcockatrice/card/database/card_database_manager.h>
|
#include <libcockatrice/card/database/card_database_manager.h>
|
||||||
#include <libcockatrice/deck_list/deck_list.h>
|
#include <libcockatrice/deck_list/deck_list.h>
|
||||||
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
|
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
|
||||||
|
#include <libcockatrice/network/network_manager.h>
|
||||||
#include <version_string.h>
|
#include <version_string.h>
|
||||||
|
|
||||||
DeckStatsInterface::DeckStatsInterface(QObject *parent) : QObject(parent)
|
DeckStatsInterface::DeckStatsInterface(QObject *parent) : QObject(parent)
|
||||||
{
|
{
|
||||||
manager = new QNetworkAccessManager(this);
|
manager = NetworkManagerFactory::create(this);
|
||||||
connect(manager, &QNetworkAccessManager::finished, this, &DeckStatsInterface::queryFinished);
|
connect(manager, &QNetworkAccessManager::finished, this, &DeckStatsInterface::queryFinished);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,11 +26,12 @@
|
||||||
#include <libcockatrice/card/database/card_database_manager.h>
|
#include <libcockatrice/card/database/card_database_manager.h>
|
||||||
#include <libcockatrice/deck_list/deck_list.h>
|
#include <libcockatrice/deck_list/deck_list.h>
|
||||||
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
|
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
|
||||||
|
#include <libcockatrice/network/network_manager.h>
|
||||||
#include <version_string.h>
|
#include <version_string.h>
|
||||||
|
|
||||||
TappedOutInterface::TappedOutInterface(QObject *parent) : QObject(parent)
|
TappedOutInterface::TappedOutInterface(QObject *parent) : QObject(parent)
|
||||||
{
|
{
|
||||||
manager = new QNetworkAccessManager(this);
|
manager = NetworkManagerFactory::create(this);
|
||||||
connect(manager, &QNetworkAccessManager::finished, this, &TappedOutInterface::queryFinished);
|
connect(manager, &QNetworkAccessManager::finished, this, &TappedOutInterface::queryFinished);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <libcockatrice/card/database/card_database.h>
|
#include <libcockatrice/card/database/card_database.h>
|
||||||
#include <libcockatrice/card/database/card_database_manager.h>
|
#include <libcockatrice/card/database/card_database_manager.h>
|
||||||
|
#include <libcockatrice/network/network_manager.h>
|
||||||
#include <libcockatrice/settings/download_settings.h>
|
#include <libcockatrice/settings/download_settings.h>
|
||||||
#include <libcockatrice/settings/paths_settings.h>
|
#include <libcockatrice/settings/paths_settings.h>
|
||||||
#include <tuple>
|
#include <tuple>
|
||||||
|
|
@ -56,7 +57,7 @@ void SpoilerBackgroundUpdater::startSpoilerDownloadProcess(QString url, bool sav
|
||||||
|
|
||||||
void SpoilerBackgroundUpdater::downloadFromURL(QUrl url, bool saveResults)
|
void SpoilerBackgroundUpdater::downloadFromURL(QUrl url, bool saveResults)
|
||||||
{
|
{
|
||||||
auto *nam = new QNetworkAccessManager(this);
|
auto *nam = NetworkManagerFactory::create(this);
|
||||||
auto request = QNetworkRequest(url);
|
auto request = QNetworkRequest(url);
|
||||||
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
||||||
QNetworkReply *reply = nam->get(request);
|
QNetworkReply *reply = nam->get(request);
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
#include <QVariantMap>
|
#include <QVariantMap>
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
|
#include <libcockatrice/network/network_manager.h>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <qnamespace.h>
|
#include <qnamespace.h>
|
||||||
#include <qprocessordetection.h>
|
#include <qprocessordetection.h>
|
||||||
|
|
@ -62,7 +63,7 @@ std::optional<int> getProductVersionMajor()
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
ReleaseChannel::ReleaseChannel() : netMan(new QNetworkAccessManager(this)), response(nullptr), lastRelease(nullptr)
|
ReleaseChannel::ReleaseChannel() : netMan(NetworkManagerFactory::create(this)), response(nullptr), lastRelease(nullptr)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,11 @@
|
||||||
#include <QNetworkRequest>
|
#include <QNetworkRequest>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
|
#include <libcockatrice/network/network_manager.h>
|
||||||
|
|
||||||
UpdateDownloader::UpdateDownloader(QObject *parent) : QObject(parent), response(nullptr)
|
UpdateDownloader::UpdateDownloader(QObject *parent) : QObject(parent), response(nullptr)
|
||||||
{
|
{
|
||||||
netMan = new QNetworkAccessManager(this);
|
netMan = NetworkManagerFactory::create(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
void UpdateDownloader::beginDownload(QUrl downloadUrl)
|
void UpdateDownloader::beginDownload(QUrl downloadUrl)
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,9 @@ SettingsCache::SettingsCache()
|
||||||
// before that rename (idempotent, safe to run on every startup, portable builds included).
|
// before that rename (idempotent, safe to run on every startup, portable builds included).
|
||||||
SettingsMigration::migrateGameFiltersToCamelCase(settingsPath);
|
SettingsMigration::migrateGameFiltersToCamelCase(settingsPath);
|
||||||
|
|
||||||
|
// Retire appearance/maxFontSize into per-theme CardText max (idempotent, safe every startup).
|
||||||
|
SettingsMigration::migrateAppearanceMaxFontSizeToTheme(settingsPath);
|
||||||
|
|
||||||
shortcutsSettings = new ShortcutsSettings(settingsPath, this);
|
shortcutsSettings = new ShortcutsSettings(settingsPath, this);
|
||||||
cardDatabaseSettings = new CardDatabaseSettings(settingsPath, this);
|
cardDatabaseSettings = new CardDatabaseSettings(settingsPath, this);
|
||||||
serversSettings = new ServersSettings(settingsPath, this);
|
serversSettings = new ServersSettings(settingsPath, this);
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ inline Q_LOGGING_CATEGORY(SettingsCacheLog, "settings_cache");
|
||||||
|
|
||||||
class ReleaseChannel;
|
class ReleaseChannel;
|
||||||
|
|
||||||
#define DEFAULT_LANG_NAME "English"
|
|
||||||
#define CLIENT_INFO_NOT_SET "notset"
|
#define CLIENT_INFO_NOT_SET "notset"
|
||||||
|
|
||||||
#define DEFAULT_FONT_SIZE 12
|
#define DEFAULT_FONT_SIZE 12
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#include "shortcut_treeview.h"
|
#include "shortcut_treeview.h"
|
||||||
|
|
||||||
|
#include "../../interface/theme_manager.h"
|
||||||
#include "cache_settings.h"
|
#include "cache_settings.h"
|
||||||
#include "shortcuts_settings.h"
|
#include "shortcuts_settings.h"
|
||||||
|
|
||||||
|
|
@ -78,6 +79,18 @@ ShortcutTreeView::ShortcutTreeView(QWidget *parent) : QTreeView(parent)
|
||||||
|
|
||||||
connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this,
|
connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this,
|
||||||
&ShortcutTreeView::refreshShortcuts);
|
&ShortcutTreeView::refreshShortcuts);
|
||||||
|
|
||||||
|
// Group headers store their font on the item; re-apply the role family on
|
||||||
|
// font changes (setFont emits itemChanged, which repaints the rows).
|
||||||
|
connect(themeManager, &ThemeManager::fontsChanged, this, [this] {
|
||||||
|
QFont groupFont = themeManager->appFont(AppFont::InterfaceDefault);
|
||||||
|
groupFont.setBold(true);
|
||||||
|
for (int row = 0; row < shortcutsModel->rowCount(); ++row) {
|
||||||
|
if (auto *groupItem = shortcutsModel->item(row)) {
|
||||||
|
groupItem->setFont(groupFont);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void ShortcutTreeView::populateShortcutsModel()
|
void ShortcutTreeView::populateShortcutsModel()
|
||||||
|
|
@ -93,7 +106,8 @@ void ShortcutTreeView::populateShortcutsModel()
|
||||||
curParent = parentItems.value(group);
|
curParent = parentItems.value(group);
|
||||||
} else {
|
} else {
|
||||||
curParent = new QStandardItem(group);
|
curParent = new QStandardItem(group);
|
||||||
static QFont font = curParent->font();
|
// Family comes from the InterfaceDefault theme role; bold stays caller-side.
|
||||||
|
QFont font = themeManager->appFont(AppFont::InterfaceDefault);
|
||||||
font.setBold(true);
|
font.setBold(true);
|
||||||
curParent->setFont(font);
|
curParent->setFont(font);
|
||||||
parentItems.insert(group, curParent);
|
parentItems.insert(group, curParent);
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,18 @@
|
||||||
#define COCKATRICE_SETTINGS_CARD_PREFERENCE_PROVIDER_H
|
#define COCKATRICE_SETTINGS_CARD_PREFERENCE_PROVIDER_H
|
||||||
#include "../../client/settings/cache_settings.h"
|
#include "../../client/settings/cache_settings.h"
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
#include <libcockatrice/interfaces/interface_card_preference_provider.h>
|
#include <libcockatrice/interfaces/interface_card_preference_provider.h>
|
||||||
#include <libcockatrice/settings/card_override_settings.h>
|
#include <libcockatrice/settings/card_override_settings.h>
|
||||||
#include <libcockatrice/settings/cards_display_settings.h>
|
#include <libcockatrice/settings/cards_display_settings.h>
|
||||||
|
|
||||||
class SettingsCardPreferenceProvider : public ICardPreferenceProvider
|
class SettingsCardPreferenceProvider : public QObject, public ICardPreferenceProvider
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
explicit SettingsCardPreferenceProvider(QObject *parent = nullptr) : QObject(parent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
[[nodiscard]] QString getCardPreferenceOverride(const QString &cardName) const override
|
[[nodiscard]] QString getCardPreferenceOverride(const QString &cardName) const override
|
||||||
{
|
{
|
||||||
return SettingsCache::instance().cardOverrides().getCardPreferenceOverride(cardName);
|
return SettingsCache::instance().cardOverrides().getCardPreferenceOverride(cardName);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
#include "deck_filter_string.h"
|
#include "deck_filter_string.h"
|
||||||
|
|
||||||
#include "../interface/deck_loader/loaded_deck.h"
|
|
||||||
#include "libcockatrice/card/card_localization.h"
|
#include "libcockatrice/card/card_localization.h"
|
||||||
#include "libcockatrice/card/database/card_database_querier.h"
|
#include "libcockatrice/card/database/card_database_querier.h"
|
||||||
#include "libcockatrice/deck_list/deck_list.h"
|
#include "libcockatrice/deck_list/deck_list.h"
|
||||||
|
|
@ -15,6 +14,7 @@
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <any>
|
#include <any>
|
||||||
#include <libcockatrice/card/database/card_database_manager.h>
|
#include <libcockatrice/card/database/card_database_manager.h>
|
||||||
|
#include <libcockatrice/deck_list/loaded_deck.h>
|
||||||
#include <libcockatrice/filters/filter_string.h>
|
#include <libcockatrice/filters/filter_string.h>
|
||||||
#include <libcockatrice/utility/peglib.h>
|
#include <libcockatrice/utility/peglib.h>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <libcockatrice/card/card_localization.h>
|
#include <libcockatrice/card/card_localization.h>
|
||||||
|
#include <libcockatrice/deck_list/loaded_deck.h>
|
||||||
|
|
||||||
struct CardSearchLanguage;
|
struct CardSearchLanguage;
|
||||||
struct LoadedDeck;
|
struct LoadedDeck;
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
#include <libcockatrice/filters/filter_card.h>
|
#include <libcockatrice/filters/filter_card.h>
|
||||||
|
#include <libcockatrice/utility/qt_utils.h>
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
|
|
||||||
FilterBuilder::FilterBuilder(QWidget *parent) : QWidget(parent)
|
FilterBuilder::FilterBuilder(QWidget *parent) : QWidget(parent)
|
||||||
|
|
@ -39,9 +40,8 @@ FilterBuilder::FilterBuilder(QWidget *parent) : QWidget(parent)
|
||||||
edit->setClearButtonEnabled(true);
|
edit->setClearButtonEnabled(true);
|
||||||
edit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
edit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||||
|
|
||||||
QGridLayout *layout = new QGridLayout;
|
QGridLayout *layout = QtUtils::makeLayout<QGridLayout>();
|
||||||
layout->setObjectName("layout");
|
layout->setObjectName("layout");
|
||||||
layout->setContentsMargins(0, 0, 0, 0);
|
|
||||||
|
|
||||||
layout->addWidget(typeCombo, 0, 0, 1, 2);
|
layout->addWidget(typeCombo, 0, 0, 1, 2);
|
||||||
layout->addWidget(filterCombo, 0, 2, 1, 2);
|
layout->addWidget(filterCombo, 0, 2, 1, 2);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
#include "filter_tree_model.h"
|
#include "filter_tree_model.h"
|
||||||
|
|
||||||
|
#include "../interface/theme_manager.h"
|
||||||
|
|
||||||
#include <QFlags>
|
#include <QFlags>
|
||||||
#include <QFont>
|
#include <QFont>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
@ -11,6 +13,27 @@
|
||||||
|
|
||||||
class QObject;
|
class QObject;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @brief Visits every filter item below @p node, depth first.
|
||||||
|
*/
|
||||||
|
template <typename Callback> void forEachFilter(const FilterTreeNode *node, const Callback &callback)
|
||||||
|
{
|
||||||
|
if (node == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const auto *filterItem = dynamic_cast<const FilterItem *>(node)) {
|
||||||
|
callback(filterItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < node->childCount(); ++i) {
|
||||||
|
forEachFilter(node->nodeAt(i), callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
FilterTreeModel::FilterTreeModel(QObject *parent) : QAbstractItemModel(parent)
|
FilterTreeModel::FilterTreeModel(QObject *parent) : QAbstractItemModel(parent)
|
||||||
{
|
{
|
||||||
fTree = new FilterTree;
|
fTree = new FilterTree;
|
||||||
|
|
@ -18,6 +41,12 @@ FilterTreeModel::FilterTreeModel(QObject *parent) : QAbstractItemModel(parent)
|
||||||
connect(fTree, &FilterTree::postInsertRow, this, &FilterTreeModel::proxyEndInsertRow);
|
connect(fTree, &FilterTree::postInsertRow, this, &FilterTreeModel::proxyEndInsertRow);
|
||||||
connect(fTree, &FilterTree::preRemoveRow, this, &FilterTreeModel::proxyBeginRemoveRow);
|
connect(fTree, &FilterTree::preRemoveRow, this, &FilterTreeModel::proxyBeginRemoveRow);
|
||||||
connect(fTree, &FilterTree::postRemoveRow, this, &FilterTreeModel::proxyEndRemoveRow);
|
connect(fTree, &FilterTree::postRemoveRow, this, &FilterTreeModel::proxyEndRemoveRow);
|
||||||
|
// FontRole data reads the DeckList role live; rebroadcast so attached
|
||||||
|
// views re-query instead of waiting for an incidental repaint.
|
||||||
|
connect(themeManager, &ThemeManager::fontsChanged, this, [this] {
|
||||||
|
emit layoutAboutToBeChanged();
|
||||||
|
emit layoutChanged();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
FilterTreeModel::~FilterTreeModel()
|
FilterTreeModel::~FilterTreeModel()
|
||||||
|
|
@ -110,46 +139,31 @@ void FilterTreeModel::clearFiltersOfType(CardFilter::Attr filterType)
|
||||||
QList<const CardFilter *> FilterTreeModel::getFiltersOfType(CardFilter::Attr filterType) const
|
QList<const CardFilter *> FilterTreeModel::getFiltersOfType(CardFilter::Attr filterType) const
|
||||||
{
|
{
|
||||||
QList<const CardFilter *> filters;
|
QList<const CardFilter *> filters;
|
||||||
if (!fTree) {
|
forEachFilter(fTree, [&](const FilterItem *filterItem) {
|
||||||
return filters;
|
if (filterItem->attr() == filterType) {
|
||||||
}
|
filters.append(new CardFilter(filterItem->text(), filterItem->type(), filterItem->attr()));
|
||||||
|
|
||||||
std::function<void(const FilterTreeNode *)> traverse;
|
|
||||||
traverse = [&](const FilterTreeNode *node) {
|
|
||||||
if (const auto *filterItem = dynamic_cast<const FilterItem *>(node)) {
|
|
||||||
if (filterItem->attr() == filterType) {
|
|
||||||
QString text = filterItem->text();
|
|
||||||
filters.append(new CardFilter(text, filterItem->type(), filterItem->attr()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for (int i = 0; i < node->childCount(); ++i) {
|
});
|
||||||
traverse(node->nodeAt(i));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
traverse(fTree);
|
|
||||||
return filters;
|
return filters;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QStringList FilterTreeModel::getFilterTermsOfType(CardFilter::Attr filterType) const
|
||||||
|
{
|
||||||
|
QStringList terms;
|
||||||
|
forEachFilter(fTree, [&](const FilterItem *filterItem) {
|
||||||
|
if (filterItem->attr() == filterType) {
|
||||||
|
terms.append(filterItem->text());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return terms;
|
||||||
|
}
|
||||||
|
|
||||||
QList<const CardFilter *> FilterTreeModel::allFilters() const
|
QList<const CardFilter *> FilterTreeModel::allFilters() const
|
||||||
{
|
{
|
||||||
QList<const CardFilter *> filters;
|
QList<const CardFilter *> filters;
|
||||||
if (!fTree) {
|
forEachFilter(fTree, [&](const FilterItem *filterItem) {
|
||||||
return filters;
|
filters.append(new CardFilter(filterItem->text(), filterItem->type(), filterItem->attr()));
|
||||||
}
|
});
|
||||||
|
|
||||||
std::function<void(const FilterTreeNode *)> traverse;
|
|
||||||
traverse = [&](const FilterTreeNode *node) {
|
|
||||||
if (const auto *filterItem = dynamic_cast<const FilterItem *>(node)) {
|
|
||||||
QString text = filterItem->text();
|
|
||||||
filters.append(new CardFilter(text, filterItem->type(), filterItem->attr()));
|
|
||||||
}
|
|
||||||
for (int i = 0; i < node->childCount(); ++i) {
|
|
||||||
traverse(node->nodeAt(i));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
traverse(fTree);
|
|
||||||
return filters;
|
return filters;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -196,7 +210,8 @@ QVariant FilterTreeModel::data(const QModelIndex &index, int role) const
|
||||||
switch (role) {
|
switch (role) {
|
||||||
case Qt::FontRole:
|
case Qt::FontRole:
|
||||||
if (!node->isLeaf()) {
|
if (!node->isLeaf()) {
|
||||||
QFont f;
|
// Family comes from the DeckList theme role; bold stays caller-side.
|
||||||
|
QFont f = themeManager->appFont(AppFont::DeckList);
|
||||||
f.setBold(true);
|
f.setBold(true);
|
||||||
return f;
|
return f;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,14 @@ public slots:
|
||||||
void addFilter(const CardFilter *f);
|
void addFilter(const CardFilter *f);
|
||||||
void removeFilter(const CardFilter *f);
|
void removeFilter(const CardFilter *f);
|
||||||
void clearFiltersOfType(CardFilter::Attr filterType);
|
void clearFiltersOfType(CardFilter::Attr filterType);
|
||||||
|
/**
|
||||||
|
* @brief Copies every active filter of one attribute into new CardFilter objects.
|
||||||
|
*
|
||||||
|
* The caller owns the returned filters. Prefer getFilterTermsOfType() when only
|
||||||
|
* the terms are needed.
|
||||||
|
*/
|
||||||
[[nodiscard]] QList<const CardFilter *> getFiltersOfType(CardFilter::Attr filterType) const;
|
[[nodiscard]] QList<const CardFilter *> getFiltersOfType(CardFilter::Attr filterType) const;
|
||||||
|
/// @brief Copies every active filter into new CardFilter objects. The caller owns them.
|
||||||
[[nodiscard]] QList<const CardFilter *> allFilters() const;
|
[[nodiscard]] QList<const CardFilter *> allFilters() const;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
|
|
@ -48,6 +55,13 @@ public:
|
||||||
{
|
{
|
||||||
return fTree;
|
return fTree;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The texts of every active filter of one attribute, without allocating filters.
|
||||||
|
*
|
||||||
|
* Prefer this over getFiltersOfType() when only the filter terms are needed.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] QStringList getFilterTermsOfType(CardFilter::Attr filterType) const;
|
||||||
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||||
[[nodiscard]] int columnCount(const QModelIndex & /*parent*/ = QModelIndex()) const override;
|
[[nodiscard]] int columnCount(const QModelIndex & /*parent*/ = QModelIndex()) const override;
|
||||||
[[nodiscard]] QVariant data(const QModelIndex &index, int role) const override;
|
[[nodiscard]] QVariant data(const QModelIndex &index, int role) const override;
|
||||||
|
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
#include "card_state.h"
|
|
||||||
|
|
||||||
#include <QSharedPointer>
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
#ifndef COCKATRICE_CARD_STATE_H
|
|
||||||
#define COCKATRICE_CARD_STATE_H
|
|
||||||
|
|
||||||
#include <QMap>
|
|
||||||
#include <QObject>
|
|
||||||
#include <QString>
|
|
||||||
#include <qtmetamacros.h>
|
|
||||||
|
|
||||||
class CardZoneLogic;
|
|
||||||
class CardItem;
|
|
||||||
|
|
||||||
class CardState : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
private:
|
|
||||||
bool attacking = false;
|
|
||||||
QMap<int, int> counters;
|
|
||||||
QString annotation;
|
|
||||||
QString pt;
|
|
||||||
bool doesntUntap = false;
|
|
||||||
bool destroyOnZoneChange = false;
|
|
||||||
|
|
||||||
CardItem *attachedTo = nullptr;
|
|
||||||
CardZoneLogic *zone = nullptr;
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void stateChanged();
|
|
||||||
|
|
||||||
void attackingChanged(bool newValue);
|
|
||||||
void countersChanged(const QMap<int, int> &newCounters);
|
|
||||||
void annotationChanged(const QString &newAnnotation);
|
|
||||||
void ptChanged(const QString &newPt);
|
|
||||||
void doesntUntapChanged(bool newValue);
|
|
||||||
void destroyOnZoneChangeChanged(bool newValue);
|
|
||||||
void attachedToChanged(CardItem *newAttachedTo);
|
|
||||||
void zoneChanged(CardState *changedCard, CardZoneLogic *newZone);
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit CardState(QObject *parent, CardZoneLogic *_zone) : QObject(parent), zone(_zone)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
void resetState(bool keepAnnotations);
|
|
||||||
|
|
||||||
CardZoneLogic *getZone() const
|
|
||||||
{
|
|
||||||
return zone;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setZone(CardZoneLogic *_zone);
|
|
||||||
|
|
||||||
bool getAttacking() const
|
|
||||||
{
|
|
||||||
return attacking;
|
|
||||||
}
|
|
||||||
void setAttacking(bool _attacking);
|
|
||||||
|
|
||||||
const QMap<int, int> &getCounters() const
|
|
||||||
{
|
|
||||||
return counters;
|
|
||||||
}
|
|
||||||
|
|
||||||
void insertCounter(int id, int value);
|
|
||||||
|
|
||||||
void setCounter(int id, int value);
|
|
||||||
|
|
||||||
void clearCounters();
|
|
||||||
|
|
||||||
QString getAnnotation() const
|
|
||||||
{
|
|
||||||
return annotation;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setAnnotation(const QString &_annotation);
|
|
||||||
|
|
||||||
QString getPT() const
|
|
||||||
{
|
|
||||||
return pt;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setPT(const QString &_pt);
|
|
||||||
|
|
||||||
bool getDoesntUntap() const
|
|
||||||
{
|
|
||||||
return doesntUntap;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setDoesntUntap(bool _doesntUntap);
|
|
||||||
|
|
||||||
bool getDestroyOnZoneChange() const
|
|
||||||
{
|
|
||||||
return destroyOnZoneChange;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setDestroyOnZoneChange(bool _destroyOnZoneChange);
|
|
||||||
|
|
||||||
CardItem *getAttachedTo() const
|
|
||||||
{
|
|
||||||
return attachedTo;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setAttachedTo(CardItem *_attachedTo);
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif // COCKATRICE_CARD_STATE_H
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
/**
|
|
||||||
* @file replay.h
|
|
||||||
* @ingroup GameLogic
|
|
||||||
* @ingroup Replay
|
|
||||||
*/
|
|
||||||
//! \todo Document this file.
|
|
||||||
|
|
||||||
#ifndef COCKATRICE_REPLAY_H
|
|
||||||
#define COCKATRICE_REPLAY_H
|
|
||||||
|
|
||||||
#include "abstract_game.h"
|
|
||||||
|
|
||||||
#include <qtmetamacros.h>
|
|
||||||
|
|
||||||
class GameReplay;
|
|
||||||
class QObject;
|
|
||||||
|
|
||||||
class Replay : public AbstractGame
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit Replay(QObject *_parent, const GameReplay *_replay, bool isLocalGame);
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif // COCKATRICE_REPLAY_H
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
#include <QBrush>
|
#include <QBrush>
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QCursor>
|
#include <QCursor>
|
||||||
|
#include <QGraphicsScene>
|
||||||
#include <QGraphicsSceneMouseEvent>
|
#include <QGraphicsSceneMouseEvent>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QPainterPath>
|
#include <QPainterPath>
|
||||||
|
|
@ -51,7 +52,20 @@ AbstractCardDragItem::AbstractCardDragItem(AbstractCardItem *_item,
|
||||||
update();
|
update();
|
||||||
});
|
});
|
||||||
|
|
||||||
connect(item, &QObject::destroyed, this, &AbstractCardDragItem::deleteLater);
|
// The source card can be destroyed mid-drag (e.g. moved by a server event). deleteLater() alone is not enough:
|
||||||
|
// a scene repaint can run before the deferred delete and paint the dead card. Detach from the scene immediately.
|
||||||
|
connect(_item, &QObject::destroyed, this, [this]() {
|
||||||
|
hide();
|
||||||
|
if (QGraphicsScene *sc = scene()) {
|
||||||
|
sc->removeItem(this);
|
||||||
|
}
|
||||||
|
deleteLater();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
AbstractCardItem *AbstractCardDragItem::getItem() const
|
||||||
|
{
|
||||||
|
return item.data();
|
||||||
}
|
}
|
||||||
|
|
||||||
QPainterPath AbstractCardDragItem::shape() const
|
QPainterPath AbstractCardDragItem::shape() const
|
||||||
|
|
@ -65,6 +79,10 @@ QPainterPath AbstractCardDragItem::shape() const
|
||||||
|
|
||||||
void AbstractCardDragItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
|
void AbstractCardDragItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
|
||||||
{
|
{
|
||||||
|
if (item.isNull()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
item->paint(painter, option, widget);
|
item->paint(painter, option, widget);
|
||||||
|
|
||||||
// adds a mask to the card so it looks like the card hasnt been placed yet
|
// adds a mask to the card so it looks like the card hasnt been placed yet
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QPointF>
|
#include <QPointF>
|
||||||
|
#include <QPointer>
|
||||||
#include <QRectF>
|
#include <QRectF>
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
|
|
||||||
|
|
@ -24,7 +25,8 @@ class AbstractCardDragItem : public QObject, public QGraphicsItem
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
Q_INTERFACES(QGraphicsItem)
|
Q_INTERFACES(QGraphicsItem)
|
||||||
protected:
|
protected:
|
||||||
AbstractCardItem *item;
|
/// Guarded pointer: the source card may be destroyed while this drag item still exists.
|
||||||
|
QPointer<AbstractCardItem> item;
|
||||||
QPointF hotSpot;
|
QPointF hotSpot;
|
||||||
QList<AbstractCardDragItem *> childDrags;
|
QList<AbstractCardDragItem *> childDrags;
|
||||||
|
|
||||||
|
|
@ -44,10 +46,8 @@ public:
|
||||||
}
|
}
|
||||||
[[nodiscard]] QPainterPath shape() const override;
|
[[nodiscard]] QPainterPath shape() const override;
|
||||||
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
|
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
|
||||||
[[nodiscard]] AbstractCardItem *getItem() const
|
/// @return the source card, or nullptr if it has already been destroyed.
|
||||||
{
|
[[nodiscard]] AbstractCardItem *getItem() const;
|
||||||
return item;
|
|
||||||
}
|
|
||||||
[[nodiscard]] QPointF getHotSpot() const
|
[[nodiscard]] QPointF getHotSpot() const
|
||||||
{
|
{
|
||||||
return hotSpot;
|
return hotSpot;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
#include "../../client/settings/cache_settings.h"
|
#include "../../client/settings/cache_settings.h"
|
||||||
#include "../../interface/card_localization.h"
|
#include "../../interface/card_localization.h"
|
||||||
#include "../../interface/card_picture_loader/card_picture_loader.h"
|
#include "../../interface/card_picture_loader/card_picture_loader.h"
|
||||||
|
#include "../../interface/theme_manager.h"
|
||||||
#include "../card_dimensions.h"
|
#include "../card_dimensions.h"
|
||||||
#include "../game_scene.h"
|
#include "../game_scene.h"
|
||||||
#include "../z_values.h"
|
#include "../z_values.h"
|
||||||
|
|
@ -34,8 +35,6 @@
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <libcockatrice/card/database/card_database_manager.h>
|
|
||||||
#include <libcockatrice/settings/appearance_settings.h>
|
|
||||||
#include <libcockatrice/settings/cards_display_settings.h>
|
#include <libcockatrice/settings/cards_display_settings.h>
|
||||||
#include <libcockatrice/settings/debug_settings.h>
|
#include <libcockatrice/settings/debug_settings.h>
|
||||||
#include <qnamespace.h>
|
#include <qnamespace.h>
|
||||||
|
|
@ -43,13 +42,23 @@
|
||||||
class QSizeF;
|
class QSizeF;
|
||||||
|
|
||||||
AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef, PlayerLogic *_owner, int _id)
|
AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef, PlayerLogic *_owner, int _id)
|
||||||
: ArrowTarget(_owner, parent), id(_id), cardRef(cardRef), tapped(false), facedown(false), tapAngle(0),
|
: ArrowTarget(_owner, parent), state(new CardState(this, nullptr, cardRef, _id)), tapAngle(0),
|
||||||
bgColor(Qt::transparent), isHovered(false), realZValue(0), pendingSelectionCollapse(false)
|
bgColor(Qt::transparent), isHovered(false), realZValue(0)
|
||||||
{
|
{
|
||||||
setCursor(Qt::OpenHandCursor);
|
setCursor(Qt::OpenHandCursor);
|
||||||
setFlag(ItemIsSelectable);
|
setFlag(ItemIsSelectable);
|
||||||
setCacheMode(DeviceCoordinateCache);
|
setCacheMode(DeviceCoordinateCache);
|
||||||
|
|
||||||
|
connect(state, &CardState::cardPixmapUpdated, this, &AbstractCardItem::pixmapUpdated);
|
||||||
|
connect(state, &CardState::cardInfoChanged, this, &AbstractCardItem::onCardInfoChanged);
|
||||||
|
connect(state, &CardState::cardRefChanged, this, &AbstractCardItem::onCardRefChanged);
|
||||||
|
connect(state, &CardState::tappedChanged, this, &AbstractCardItem::onTappedChanged);
|
||||||
|
connect(state, &CardState::facedownChanged, this, [this] { update(); });
|
||||||
|
connect(state, &CardState::colorChanged, this, [this] {
|
||||||
|
cacheBgColor();
|
||||||
|
update();
|
||||||
|
});
|
||||||
|
|
||||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::displayCardNamesChanged, this,
|
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::displayCardNamesChanged, this,
|
||||||
[this] { update(); });
|
[this] { update(); });
|
||||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this,
|
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this,
|
||||||
|
|
@ -67,7 +76,7 @@ AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef
|
||||||
|
|
||||||
AbstractCardItem::~AbstractCardItem()
|
AbstractCardItem::~AbstractCardItem()
|
||||||
{
|
{
|
||||||
emit deleteCardInfoPopup(cardRef.name);
|
emit deleteCardInfoPopup(state->getCardRef().name);
|
||||||
}
|
}
|
||||||
|
|
||||||
QRectF AbstractCardItem::boundingRect() const
|
QRectF AbstractCardItem::boundingRect() const
|
||||||
|
|
@ -90,30 +99,29 @@ void AbstractCardItem::pixmapUpdated()
|
||||||
emit sigPixmapUpdated();
|
emit sigPixmapUpdated();
|
||||||
}
|
}
|
||||||
|
|
||||||
void AbstractCardItem::refreshCardInfo()
|
void AbstractCardItem::onCardInfoChanged()
|
||||||
{
|
{
|
||||||
exactCard = CardDatabaseManager::query()->getCard(cardRef);
|
|
||||||
|
|
||||||
if (!exactCard && !cardRef.name.isEmpty()) {
|
|
||||||
CardInfo::UiAttributes attributes = {.tableRow = -1};
|
|
||||||
auto info = CardInfo::newInstance(cardRef.name, "", true, {}, {}, {}, {}, attributes);
|
|
||||||
exactCard = ExactCard(info);
|
|
||||||
}
|
|
||||||
if (exactCard) {
|
|
||||||
connect(exactCard.getCardPtr().data(), &CardInfo::pixmapUpdated, this, &AbstractCardItem::pixmapUpdated);
|
|
||||||
}
|
|
||||||
|
|
||||||
cacheBgColor();
|
cacheBgColor();
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AbstractCardItem::onCardRefChanged(const CardRef &oldCardRef, const CardRef & /*newCardRef*/)
|
||||||
|
{
|
||||||
|
emit deleteCardInfoPopup(oldCardRef.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AbstractCardItem::refreshCardInfo()
|
||||||
|
{
|
||||||
|
state->refreshCardInfo();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convenience method to get the CardInfo of the exactCard
|
* Convenience method to get the CardInfo of the exactCard
|
||||||
* @return A const reference to the CardInfo, or an empty CardInfo if card was null
|
* @return A const reference to the CardInfo, or an empty CardInfo if card was null
|
||||||
*/
|
*/
|
||||||
const CardInfo &AbstractCardItem::getCardInfo() const
|
const CardInfo &AbstractCardItem::getCardInfo() const
|
||||||
{
|
{
|
||||||
return exactCard.getInfo();
|
return state->getCardInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
void AbstractCardItem::setRealZValue(qreal _zValue)
|
void AbstractCardItem::setRealZValue(qreal _zValue)
|
||||||
|
|
@ -135,8 +143,8 @@ QSizeF AbstractCardItem::getTranslatedSize(QPainter *painter) const
|
||||||
|
|
||||||
void AbstractCardItem::transformPainter(QPainter *painter, const QSizeF &translatedSize, int angle)
|
void AbstractCardItem::transformPainter(QPainter *painter, const QSizeF &translatedSize, int angle)
|
||||||
{
|
{
|
||||||
const int MAX_FONT_SIZE = SettingsCache::instance().appearance().getMaxFontSize();
|
const int minSize = std::max(1, themeManager->cardTextMinSize());
|
||||||
const int fontSize = std::max(9, MAX_FONT_SIZE);
|
const int fontSize = std::max(minSize, std::max(1, themeManager->cardTextMaxSize()));
|
||||||
|
|
||||||
QRectF totalBoundingRect = painter->combinedTransform().mapRect(boundingRect());
|
QRectF totalBoundingRect = painter->combinedTransform().mapRect(boundingRect());
|
||||||
|
|
||||||
|
|
@ -146,7 +154,7 @@ void AbstractCardItem::transformPainter(QPainter *painter, const QSizeF &transla
|
||||||
painter->rotate(angle);
|
painter->rotate(angle);
|
||||||
painter->translate(-translatedSize.width() / 2, -translatedSize.height() / 2);
|
painter->translate(-translatedSize.width() / 2, -translatedSize.height() / 2);
|
||||||
|
|
||||||
QFont f;
|
QFont f = themeManager->appFont(AppFont::CardText);
|
||||||
f.setPixelSize(fontSize * scale);
|
f.setPixelSize(fontSize * scale);
|
||||||
|
|
||||||
painter->setFont(f);
|
painter->setFont(f);
|
||||||
|
|
@ -158,13 +166,13 @@ void AbstractCardItem::paintPicture(QPainter *painter, const QSizeF &translatedS
|
||||||
QPixmap translatedPixmap;
|
QPixmap translatedPixmap;
|
||||||
bool paintImage = true;
|
bool paintImage = true;
|
||||||
|
|
||||||
if (facedown || cardRef.name.isEmpty()) {
|
if (state->getFaceDown() || state->getCardRef().name.isEmpty()) {
|
||||||
// never reveal card color, always paint the card back
|
// never reveal card color, always paint the card back
|
||||||
CardPictureLoader::getCardBackPixmap(translatedPixmap, translatedSize.toSize());
|
CardPictureLoader::getCardBackPixmap(translatedPixmap, translatedSize.toSize());
|
||||||
} else {
|
} else {
|
||||||
// don't even spend time trying to load the picture if our size is too small
|
// don't even spend time trying to load the picture if our size is too small
|
||||||
if (translatedSize.width() > 10) {
|
if (translatedSize.width() > 10) {
|
||||||
CardPictureLoader::getPixmap(translatedPixmap, exactCard, translatedSize.toSize());
|
CardPictureLoader::getPixmap(translatedPixmap, state->getCard(), translatedSize.toSize());
|
||||||
if (translatedPixmap.isNull()) {
|
if (translatedPixmap.isNull()) {
|
||||||
paintImage = false;
|
paintImage = false;
|
||||||
}
|
}
|
||||||
|
|
@ -185,19 +193,20 @@ void AbstractCardItem::paintPicture(QPainter *painter, const QSizeF &translatedS
|
||||||
painter->drawPath(shape());
|
painter->drawPath(shape());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (translatedPixmap.isNull() || SettingsCache::instance().cardsDisplay().getDisplayCardNames() || facedown) {
|
if (translatedPixmap.isNull() || SettingsCache::instance().cardsDisplay().getDisplayCardNames() ||
|
||||||
|
state->getFaceDown()) {
|
||||||
painter->save();
|
painter->save();
|
||||||
transformPainter(painter, translatedSize, angle);
|
transformPainter(painter, translatedSize, angle);
|
||||||
painter->setPen(Qt::white);
|
painter->setPen(Qt::white);
|
||||||
painter->setBackground(Qt::black);
|
painter->setBackground(Qt::black);
|
||||||
painter->setBackgroundMode(Qt::OpaqueMode);
|
painter->setBackgroundMode(Qt::OpaqueMode);
|
||||||
QString nameStr;
|
QString nameStr;
|
||||||
if (facedown) {
|
if (state->getFaceDown()) {
|
||||||
nameStr = "# " + QString::number(id);
|
nameStr = "# " + QString::number(state->getId());
|
||||||
} else {
|
} else {
|
||||||
QString prefix = "";
|
QString prefix = "";
|
||||||
if (SettingsCache::instance().debug().getShowCardId()) {
|
if (SettingsCache::instance().debug().getShowCardId()) {
|
||||||
prefix = "#" + QString::number(id) + " ";
|
prefix = "#" + QString::number(state->getId()) + " ";
|
||||||
}
|
}
|
||||||
nameStr = prefix + CardLocalization::displayName(getCardInfo());
|
nameStr = prefix + CardLocalization::displayName(getCardInfo());
|
||||||
}
|
}
|
||||||
|
|
@ -235,21 +244,6 @@ void AbstractCardItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *
|
||||||
painter->restore();
|
painter->restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
void AbstractCardItem::setCardRef(const CardRef &_cardRef)
|
|
||||||
{
|
|
||||||
if (cardRef == _cardRef) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
emit deleteCardInfoPopup(cardRef.name);
|
|
||||||
if (exactCard) {
|
|
||||||
disconnect(exactCard.getCardPtr().data(), nullptr, this, nullptr);
|
|
||||||
}
|
|
||||||
cardRef = _cardRef;
|
|
||||||
|
|
||||||
refreshCardInfo();
|
|
||||||
}
|
|
||||||
|
|
||||||
void AbstractCardItem::setHovered(bool _hovered)
|
void AbstractCardItem::setHovered(bool _hovered)
|
||||||
{
|
{
|
||||||
if (isHovered == _hovered) {
|
if (isHovered == _hovered) {
|
||||||
|
|
@ -275,18 +269,17 @@ void AbstractCardItem::setHovered(bool _hovered)
|
||||||
|
|
||||||
void AbstractCardItem::setColor(const QString &_color)
|
void AbstractCardItem::setColor(const QString &_color)
|
||||||
{
|
{
|
||||||
color = _color;
|
state->setColor(_color);
|
||||||
cacheBgColor();
|
|
||||||
update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void AbstractCardItem::cacheBgColor()
|
void AbstractCardItem::cacheBgColor()
|
||||||
{
|
{
|
||||||
QChar colorChar;
|
QChar colorChar;
|
||||||
if (color.isEmpty()) {
|
const QString cardColor = state->getColor();
|
||||||
colorChar = exactCard.getInfo().getColorChar();
|
if (cardColor.isEmpty()) {
|
||||||
|
colorChar = state->getCardInfo().getColorChar();
|
||||||
} else {
|
} else {
|
||||||
colorChar = color.at(0);
|
colorChar = cardColor.at(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (colorChar.toLower().toLatin1()) {
|
switch (colorChar.toLower().toLatin1()) {
|
||||||
|
|
@ -314,17 +307,12 @@ void AbstractCardItem::cacheBgColor()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AbstractCardItem::setTapped(bool _tapped, bool canAnimate)
|
void AbstractCardItem::onTappedChanged(bool newTapped, bool canAnimate)
|
||||||
{
|
{
|
||||||
if (tapped == _tapped) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
tapped = _tapped;
|
|
||||||
if (SettingsCache::instance().cardsDisplay().getTapAnimation() && canAnimate) {
|
if (SettingsCache::instance().cardsDisplay().getTapAnimation() && canAnimate) {
|
||||||
static_cast<GameScene *>(scene())->registerAnimationItem(this);
|
static_cast<GameScene *>(scene())->registerAnimationItem(this);
|
||||||
} else {
|
} else {
|
||||||
tapAngle = tapped ? 90 : 0;
|
tapAngle = newTapped ? 90 : 0;
|
||||||
setTransform(QTransform()
|
setTransform(QTransform()
|
||||||
.translate(CardDimensions::WIDTH_HALF_F, CardDimensions::HEIGHT_HALF_F)
|
.translate(CardDimensions::WIDTH_HALF_F, CardDimensions::HEIGHT_HALF_F)
|
||||||
.rotate(tapAngle)
|
.rotate(tapAngle)
|
||||||
|
|
@ -338,12 +326,6 @@ bool AbstractCardItem::animationEvent()
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void AbstractCardItem::setFaceDown(bool _facedown)
|
|
||||||
{
|
|
||||||
facedown = _facedown;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
void AbstractCardItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
|
void AbstractCardItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
|
||||||
{
|
{
|
||||||
cancelPendingSelectionCollapse();
|
cancelPendingSelectionCollapse();
|
||||||
|
|
@ -357,7 +339,7 @@ void AbstractCardItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ((event->modifiers() & Qt::AltModifier) && event->button() == Qt::LeftButton) {
|
if ((event->modifiers() & Qt::AltModifier) && event->button() == Qt::LeftButton) {
|
||||||
emit cardShiftClicked(cardRef.name);
|
emit cardShiftClicked(state->getCardRef().name);
|
||||||
} else if ((event->modifiers() & Qt::ControlModifier)) {
|
} else if ((event->modifiers() & Qt::ControlModifier)) {
|
||||||
setSelected(!isSelected());
|
setSelected(!isSelected());
|
||||||
} else if (!isSelected()) {
|
} else if (!isSelected()) {
|
||||||
|
|
@ -371,7 +353,7 @@ void AbstractCardItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
|
||||||
if (event->button() == Qt::LeftButton) {
|
if (event->button() == Qt::LeftButton) {
|
||||||
setCursor(Qt::ClosedHandCursor);
|
setCursor(Qt::ClosedHandCursor);
|
||||||
} else if (event->button() == Qt::MiddleButton) {
|
} else if (event->button() == Qt::MiddleButton) {
|
||||||
emit showCardInfoPopup(event->screenPos(), cardRef);
|
emit showCardInfoPopup(event->screenPos(), state->getCardRef());
|
||||||
}
|
}
|
||||||
event->accept();
|
event->accept();
|
||||||
}
|
}
|
||||||
|
|
@ -405,7 +387,7 @@ void AbstractCardItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event->button() == Qt::MiddleButton) {
|
if (event->button() == Qt::MiddleButton) {
|
||||||
emit deleteCardInfoPopup(cardRef.name);
|
emit deleteCardInfoPopup(state->getCardRef().name);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function ensures the parent function doesn't mess around with our selection.
|
// This function ensures the parent function doesn't mess around with our selection.
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,7 @@
|
||||||
#include <QSizeF>
|
#include <QSizeF>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
#include <libcockatrice/card/printing/exact_card.h>
|
#include <libcockatrice/game/board/card_state.h>
|
||||||
#include <libcockatrice/utility/card_ref.h>
|
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
|
|
||||||
class PlayerLogic;
|
class PlayerLogic;
|
||||||
|
|
@ -29,13 +28,8 @@ class AbstractCardItem : public ArrowTarget, public IAnimatedItem
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
protected:
|
protected:
|
||||||
ExactCard exactCard;
|
CardState *state;
|
||||||
int id;
|
|
||||||
CardRef cardRef;
|
|
||||||
bool tapped;
|
|
||||||
bool facedown;
|
|
||||||
int tapAngle;
|
int tapAngle;
|
||||||
QString color;
|
|
||||||
QColor bgColor;
|
QColor bgColor;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
@ -45,6 +39,9 @@ private:
|
||||||
int selectionCollapseGeneration = 0; ///< Guards the deferred collapse timer against stale firings
|
int selectionCollapseGeneration = 0; ///< Guards the deferred collapse timer against stale firings
|
||||||
private slots:
|
private slots:
|
||||||
void pixmapUpdated();
|
void pixmapUpdated();
|
||||||
|
void onCardInfoChanged();
|
||||||
|
void onCardRefChanged(const CardRef &oldCardRef, const CardRef &newCardRef);
|
||||||
|
void onTappedChanged(bool newTapped, bool canAnimate);
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void refreshCardInfo();
|
void refreshCardInfo();
|
||||||
|
|
@ -80,31 +77,38 @@ public:
|
||||||
QSizeF getTranslatedSize(QPainter *painter) const;
|
QSizeF getTranslatedSize(QPainter *painter) const;
|
||||||
void paintPicture(QPainter *painter, const QSizeF &translatedSize, int angle);
|
void paintPicture(QPainter *painter, const QSizeF &translatedSize, int angle);
|
||||||
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
|
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
|
||||||
|
[[nodiscard]] CardState *getState() const
|
||||||
|
{
|
||||||
|
return state;
|
||||||
|
}
|
||||||
ExactCard getCard() const
|
ExactCard getCard() const
|
||||||
{
|
{
|
||||||
return exactCard;
|
return state->getCard();
|
||||||
}
|
}
|
||||||
const CardInfo &getCardInfo() const;
|
const CardInfo &getCardInfo() const;
|
||||||
int getId() const
|
int getId() const
|
||||||
{
|
{
|
||||||
return id;
|
return state->getId();
|
||||||
}
|
}
|
||||||
void setId(int _id)
|
void setId(int _id)
|
||||||
{
|
{
|
||||||
id = _id;
|
state->setId(_id);
|
||||||
}
|
}
|
||||||
QString getName() const
|
QString getName() const
|
||||||
{
|
{
|
||||||
return cardRef.name;
|
return state->getCardRef().name;
|
||||||
}
|
}
|
||||||
QString getProviderId() const
|
QString getProviderId() const
|
||||||
{
|
{
|
||||||
return cardRef.providerId;
|
return state->getCardRef().providerId;
|
||||||
|
}
|
||||||
|
void setCardRef(const CardRef &_cardRef)
|
||||||
|
{
|
||||||
|
state->setCardRef(_cardRef);
|
||||||
}
|
}
|
||||||
void setCardRef(const CardRef &_cardRef);
|
|
||||||
CardRef getCardRef() const
|
CardRef getCardRef() const
|
||||||
{
|
{
|
||||||
return cardRef;
|
return state->getCardRef();
|
||||||
}
|
}
|
||||||
qreal getRealZValue() const
|
qreal getRealZValue() const
|
||||||
{
|
{
|
||||||
|
|
@ -116,27 +120,33 @@ public:
|
||||||
{
|
{
|
||||||
return isHovered;
|
return isHovered;
|
||||||
}
|
}
|
||||||
QString getColor() const
|
[[nodiscard]] QString getColor() const
|
||||||
{
|
{
|
||||||
return color;
|
return state->getColor();
|
||||||
}
|
}
|
||||||
void setColor(const QString &_color);
|
void setColor(const QString &_color);
|
||||||
bool getTapped() const
|
bool getTapped() const
|
||||||
{
|
{
|
||||||
return tapped;
|
return state->getTapped();
|
||||||
|
}
|
||||||
|
void setTapped(bool _tapped, bool canAnimate = false)
|
||||||
|
{
|
||||||
|
state->setTapped(_tapped, canAnimate);
|
||||||
}
|
}
|
||||||
void setTapped(bool _tapped, bool canAnimate = false);
|
|
||||||
bool getFaceDown() const
|
bool getFaceDown() const
|
||||||
{
|
{
|
||||||
return facedown;
|
return state->getFaceDown();
|
||||||
|
}
|
||||||
|
void setFaceDown(bool _facedown)
|
||||||
|
{
|
||||||
|
state->setFaceDown(_facedown);
|
||||||
}
|
}
|
||||||
void setFaceDown(bool _facedown);
|
|
||||||
void processHoverEvent();
|
void processHoverEvent();
|
||||||
/** @brief Drop a pending collapse and disarm any deferred collapse timer. */
|
/** @brief Drop a pending collapse and disarm any deferred collapse timer. */
|
||||||
void cancelPendingSelectionCollapse();
|
void cancelPendingSelectionCollapse();
|
||||||
void deleteCardInfoPopup()
|
void deleteCardInfoPopup()
|
||||||
{
|
{
|
||||||
emit deleteCardInfoPopup(cardRef.name);
|
emit deleteCardInfoPopup(state->getCardRef().name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @brief Default: no per-tick animation. Subclasses override to animate. */
|
/** @brief Default: no per-tick animation. Subclasses override to animate. */
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,6 @@
|
||||||
|
|
||||||
#include "../../client/settings/cache_settings.h"
|
#include "../../client/settings/cache_settings.h"
|
||||||
#include "../../client/settings/shortcuts_settings.h"
|
#include "../../client/settings/shortcuts_settings.h"
|
||||||
#include "../../game/board/counter_state.h"
|
|
||||||
#include "../../game/player/player_actions.h"
|
|
||||||
#include "../../game/player/player_info.h"
|
|
||||||
#include "../../game/player/player_logic.h"
|
|
||||||
#include "../../game_graphics/board/translate_counter_name.h"
|
#include "../../game_graphics/board/translate_counter_name.h"
|
||||||
#include "../../interface/widgets/menus/tearoff_menu.h"
|
#include "../../interface/widgets/menus/tearoff_menu.h"
|
||||||
|
|
||||||
|
|
@ -23,8 +19,10 @@
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
#include <libcockatrice/protocol/pb/command_inc_counter.pb.h>
|
#include <libcockatrice/game/board/counter_state.h>
|
||||||
#include <libcockatrice/protocol/pb/command_set_counter.pb.h>
|
#include <libcockatrice/game/player/player_actions.h>
|
||||||
|
#include <libcockatrice/game/player/player_info.h>
|
||||||
|
#include <libcockatrice/game/player/player_logic.h>
|
||||||
#include <libcockatrice/utility/expression.h>
|
#include <libcockatrice/utility/expression.h>
|
||||||
#include <qnamespace.h>
|
#include <qnamespace.h>
|
||||||
|
|
||||||
|
|
@ -151,10 +149,7 @@ void AbstractCounter::mousePressEvent(QGraphicsSceneMouseEvent *event)
|
||||||
menu->exec(event->screenPos());
|
menu->exec(event->screenPos());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Command_IncCounter cmd;
|
player->getPlayerActions()->actIncrementCounter(id, event->button() == Qt::LeftButton ? 1 : -1);
|
||||||
cmd.set_counter_id(id);
|
|
||||||
cmd.set_delta(event->button() == Qt::LeftButton ? 1 : -1);
|
|
||||||
player->getPlayerActions()->sendGameCommand(cmd);
|
|
||||||
}
|
}
|
||||||
event->accept();
|
event->accept();
|
||||||
}
|
}
|
||||||
|
|
@ -172,10 +167,7 @@ void AbstractCounter::hoverLeaveEvent(QGraphicsSceneHoverEvent *)
|
||||||
|
|
||||||
void AbstractCounter::incrementCounter()
|
void AbstractCounter::incrementCounter()
|
||||||
{
|
{
|
||||||
Command_IncCounter cmd;
|
player->getPlayerActions()->actIncrementCounter(id, static_cast<QAction *>(sender())->data().toInt());
|
||||||
cmd.set_counter_id(id);
|
|
||||||
cmd.set_delta(static_cast<QAction *>(sender())->data().toInt());
|
|
||||||
player->getPlayerActions()->sendGameCommand(cmd);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void AbstractCounter::setCounter()
|
void AbstractCounter::setCounter()
|
||||||
|
|
@ -199,10 +191,7 @@ void AbstractCounter::setCounter()
|
||||||
}
|
}
|
||||||
|
|
||||||
Expression exp(value);
|
Expression exp(value);
|
||||||
Command_SetCounter cmd;
|
player->getPlayerActions()->actSetCounter(id, static_cast<int>(exp.parse(dlg.textValue())));
|
||||||
cmd.set_counter_id(id);
|
|
||||||
cmd.set_value(static_cast<int>(exp.parse(dlg.textValue())));
|
|
||||||
player->getPlayerActions()->sendGameCommand(cmd);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AbstractCounterDialog::AbstractCounterDialog(const QString &name, const QString &value, QWidget *parent)
|
AbstractCounterDialog::AbstractCounterDialog(const QString &name, const QString &value, QWidget *parent)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
#include "abstract_graphics_item.h"
|
#include "abstract_graphics_item.h"
|
||||||
|
|
||||||
|
#include "../../interface/theme_manager.h"
|
||||||
|
|
||||||
#include <QBrush>
|
#include <QBrush>
|
||||||
#include <QFont>
|
#include <QFont>
|
||||||
#include <QFontMetrics>
|
#include <QFontMetrics>
|
||||||
|
|
@ -20,7 +22,7 @@ void AbstractGraphicsItem::paintNumberEllipse(int number,
|
||||||
painter->save();
|
painter->save();
|
||||||
|
|
||||||
QString numStr = QString::number(number);
|
QString numStr = QString::number(number);
|
||||||
QFont font("Serif");
|
QFont font = themeManager->cardTextFont();
|
||||||
font.setPixelSize(fontSize);
|
font.setPixelSize(fontSize);
|
||||||
font.setWeight(QFont::Bold);
|
font.setWeight(QFont::Bold);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,6 @@
|
||||||
#include "arrow_item.h"
|
#include "arrow_item.h"
|
||||||
|
|
||||||
#include "../../client/settings/cache_settings.h"
|
#include "../../client/settings/cache_settings.h"
|
||||||
#include "../../game/board/arrow_data.h"
|
|
||||||
#include "../../game/player/player_actions.h"
|
|
||||||
#include "../../game/player/player_info.h"
|
|
||||||
#include "../../game/player/player_logic.h"
|
|
||||||
#include "../../game/zones/card_zone_logic.h"
|
|
||||||
#include "../game_scene.h"
|
#include "../game_scene.h"
|
||||||
#include "../player/player_target.h"
|
#include "../player/player_target.h"
|
||||||
#include "../z_values.h"
|
#include "../z_values.h"
|
||||||
|
|
@ -27,12 +22,12 @@
|
||||||
#include <QTransform>
|
#include <QTransform>
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
#include <QtMath>
|
#include <QtMath>
|
||||||
#include <libcockatrice/card/card_info.h>
|
#include <libcockatrice/game/board/arrow_data.h>
|
||||||
#include <libcockatrice/protocol/pb/command_attach_card.pb.h>
|
#include <libcockatrice/game/player/player_actions.h>
|
||||||
#include <libcockatrice/protocol/pb/command_create_arrow.pb.h>
|
#include <libcockatrice/game/player/player_info.h>
|
||||||
|
#include <libcockatrice/game/player/player_logic.h>
|
||||||
|
#include <libcockatrice/game/zones/card_zone_logic.h>
|
||||||
#include <libcockatrice/settings/cards_display_settings.h>
|
#include <libcockatrice/settings/cards_display_settings.h>
|
||||||
#include <libcockatrice/settings/interface_settings.h>
|
|
||||||
#include <libcockatrice/utility/color.h>
|
|
||||||
#include <libcockatrice/utility/zone_names.h>
|
#include <libcockatrice/utility/zone_names.h>
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
#include <qnamespace.h>
|
#include <qnamespace.h>
|
||||||
|
|
@ -426,19 +421,19 @@ void ArrowDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
||||||
|
|
||||||
CardZoneLogic *startZone = startCard->getZone();
|
CardZoneLogic *startZone = startCard->getZone();
|
||||||
|
|
||||||
Command_CreateArrow cmd;
|
ArrowData arrow;
|
||||||
cmd.mutable_arrow_color()->CopyFrom(convertQColorToColor(data->color));
|
arrow.color = data->color;
|
||||||
cmd.set_start_player_id(startZone->getPlayer()->getPlayerInfo()->getId());
|
arrow.startPlayerId = startZone->getPlayer()->getPlayerInfo()->getId();
|
||||||
cmd.set_start_zone(startZone->getName().toStdString());
|
arrow.startZone = startZone->getName();
|
||||||
cmd.set_start_card_id(startCard->getId());
|
arrow.startCardId = startCard->getId();
|
||||||
|
|
||||||
if (auto *targetCard = qgraphicsitem_cast<CardItem *>(targetItem)) {
|
if (auto *targetCard = qgraphicsitem_cast<CardItem *>(targetItem)) {
|
||||||
CardZoneLogic *targetZone = targetCard->getZone();
|
CardZoneLogic *targetZone = targetCard->getZone();
|
||||||
cmd.set_target_player_id(targetZone->getPlayer()->getPlayerInfo()->getId());
|
arrow.targetPlayerId = targetZone->getPlayer()->getPlayerInfo()->getId();
|
||||||
cmd.set_target_zone(targetZone->getName().toStdString());
|
arrow.targetZone = targetZone->getName();
|
||||||
cmd.set_target_card_id(targetCard->getId());
|
arrow.targetCardId = targetCard->getId();
|
||||||
} else if (auto *targetPlayer = qgraphicsitem_cast<PlayerTarget *>(targetItem)) {
|
} else if (auto *targetPlayer = qgraphicsitem_cast<PlayerTarget *>(targetItem)) {
|
||||||
cmd.set_target_player_id(targetPlayer->getOwner()->getPlayerInfo()->getId());
|
arrow.targetPlayerId = targetPlayer->getOwner()->getPlayerInfo()->getId();
|
||||||
} else {
|
} else {
|
||||||
delArrow();
|
delArrow();
|
||||||
return;
|
return;
|
||||||
|
|
@ -447,22 +442,9 @@ void ArrowDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
||||||
// if the card is in hand then we will move the card to stack or table as part of drawing the arrow
|
// if the card is in hand then we will move the card to stack or table as part of drawing the arrow
|
||||||
if (startZone->getName() == ZoneNames::HAND) {
|
if (startZone->getName() == ZoneNames::HAND) {
|
||||||
startCard->playCard(false);
|
startCard->playCard(false);
|
||||||
CardInfoPtr ci = startCard->getCard().getCardPtr();
|
|
||||||
bool playToStack = SettingsCache::instance().userInterface().getPlayToStack();
|
|
||||||
if (ci && ((!playToStack && ci->getUiAttributes().tableRow == 3) ||
|
|
||||||
(playToStack && ci->getUiAttributes().tableRow != 0 &&
|
|
||||||
startCard->getZone()->getName() != ZoneNames::STACK))) {
|
|
||||||
cmd.set_start_zone(ZoneNames::STACK);
|
|
||||||
} else {
|
|
||||||
cmd.set_start_zone(playToStack ? ZoneNames::STACK : ZoneNames::TABLE);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deleteInPhase != 0) {
|
player->getPlayerActions()->createArrow(startCard->getState(), arrow, deleteInPhase);
|
||||||
cmd.set_delete_in_phase(deleteInPhase);
|
|
||||||
}
|
|
||||||
|
|
||||||
player->getPlayerActions()->sendGameCommand(cmd);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
delArrow();
|
delArrow();
|
||||||
|
|
@ -554,7 +536,7 @@ void ArrowAttachItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
||||||
auto *startCard = qgraphicsitem_cast<CardItem *>(startItem);
|
auto *startCard = qgraphicsitem_cast<CardItem *>(startItem);
|
||||||
auto *targetCard = qgraphicsitem_cast<CardItem *>(targetItem);
|
auto *targetCard = qgraphicsitem_cast<CardItem *>(targetItem);
|
||||||
if (startCard && targetCard) {
|
if (startCard && targetCard) {
|
||||||
attachCards(startCard, targetCard);
|
player->getPlayerActions()->attachCards(startCard->getState(), targetCard->getState());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -563,23 +545,3 @@ void ArrowAttachItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
||||||
child->mouseReleaseEvent(event);
|
child->mouseReleaseEvent(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ArrowAttachItem::attachCards(CardItem *startCard, const CardItem *targetCard)
|
|
||||||
{
|
|
||||||
if (targetCard->getAttachedTo() || targetCard->getZone()->getName() != ZoneNames::TABLE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// move card onto table first if attaching from some other zone
|
|
||||||
if (startCard->getZone()->getName() != ZoneNames::TABLE) {
|
|
||||||
player->getPlayerActions()->playCardToTable(startCard, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
Command_AttachCard cmd;
|
|
||||||
cmd.set_start_zone(ZoneNames::TABLE);
|
|
||||||
cmd.set_card_id(startCard->getId());
|
|
||||||
cmd.set_target_player_id(targetCard->getZone()->getPlayer()->getPlayerInfo()->getId());
|
|
||||||
cmd.set_target_zone(targetCard->getZone()->getName().toStdString());
|
|
||||||
cmd.set_target_card_id(targetCard->getId());
|
|
||||||
player->getPlayerActions()->sendGameCommand(cmd);
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
#ifndef ARROWITEM_H
|
#ifndef ARROWITEM_H
|
||||||
#define ARROWITEM_H
|
#define ARROWITEM_H
|
||||||
|
|
||||||
#include "../../game/board/arrow_data.h"
|
|
||||||
#include "../animated_item.h"
|
#include "../animated_item.h"
|
||||||
#include "arrow_target.h"
|
#include "arrow_target.h"
|
||||||
#include "graphics_item_type.h"
|
#include "graphics_item_type.h"
|
||||||
|
|
@ -18,6 +17,7 @@
|
||||||
#include <QRectF>
|
#include <QRectF>
|
||||||
#include <QSharedPointer>
|
#include <QSharedPointer>
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
|
#include <libcockatrice/game/board/arrow_data.h>
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
|
|
||||||
class CardItem;
|
class CardItem;
|
||||||
|
|
@ -131,7 +131,6 @@ private:
|
||||||
PlayerLogic *player;
|
PlayerLogic *player;
|
||||||
QList<ArrowAttachItem *> childArrows;
|
QList<ArrowAttachItem *> childArrows;
|
||||||
QMetaObject::Connection positionConnection;
|
QMetaObject::Connection positionConnection;
|
||||||
void attachCards(CardItem *startCard, const CardItem *targetCard);
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ArrowAttachItem(ArrowTarget *_startItem);
|
explicit ArrowAttachItem(ArrowTarget *_startItem);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
#include "arrow_registry.h"
|
#include "arrow_registry.h"
|
||||||
|
|
||||||
#include "../game_graphics/board/arrow_item.h"
|
#include "arrow_item.h"
|
||||||
#include "board/arrow_data.h"
|
|
||||||
|
|
||||||
#include <QSharedPointer>
|
#include <QSharedPointer>
|
||||||
|
#include <libcockatrice/game/board/arrow_data.h>
|
||||||
|
|
||||||
void ArrowRegistry::insert(QSharedPointer<ArrowData> data, ArrowItem *arrow)
|
void ArrowRegistry::insert(QSharedPointer<ArrowData> data, ArrowItem *arrow)
|
||||||
{
|
{
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QMap>
|
#include <QMap>
|
||||||
#include <QSet>
|
#include <QSet>
|
||||||
|
#include <libcockatrice/game/board/arrow_data.h>
|
||||||
|
|
||||||
class ArrowItem;
|
class ArrowItem;
|
||||||
struct ArrowData;
|
struct ArrowData;
|
||||||
|
|
@ -130,8 +130,9 @@ void CardDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
||||||
sc->removeItem(this);
|
sc->removeItem(this);
|
||||||
|
|
||||||
QList<CardDragItem *> dragItemList;
|
QList<CardDragItem *> dragItemList;
|
||||||
CardZoneLogic *startZone = static_cast<CardItem *>(item)->getZone();
|
CardZoneLogic *startZone = static_cast<CardItem *>(item.data())->getZone();
|
||||||
if (currentZone && !(static_cast<CardItem *>(item)->getAttachedTo() && (startZone == currentZone->getLogic()))) {
|
if (currentZone &&
|
||||||
|
!(static_cast<CardItem *>(item.data())->getAttachedTo() && (startZone == currentZone->getLogic()))) {
|
||||||
if (!occupied) {
|
if (!occupied) {
|
||||||
dragItemList.append(this);
|
dragItemList.append(this);
|
||||||
}
|
}
|
||||||
|
|
@ -139,7 +140,7 @@ void CardDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
||||||
for (int i = 0; i < childDrags.size(); i++) {
|
for (int i = 0; i < childDrags.size(); i++) {
|
||||||
CardDragItem *c = static_cast<CardDragItem *>(childDrags[i]);
|
CardDragItem *c = static_cast<CardDragItem *>(childDrags[i]);
|
||||||
if (!occupied &&
|
if (!occupied &&
|
||||||
!(static_cast<CardItem *>(c->item)->getAttachedTo() && (startZone == currentZone->getLogic())) &&
|
!(static_cast<CardItem *>(c->item.data())->getAttachedTo() && (startZone == currentZone->getLogic())) &&
|
||||||
!c->occupied) {
|
!c->occupied) {
|
||||||
dragItemList.append(c);
|
dragItemList.append(c);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,6 @@
|
||||||
#include "card_item.h"
|
#include "card_item.h"
|
||||||
|
|
||||||
#include "../../client/settings/cache_settings.h"
|
#include "../../client/settings/cache_settings.h"
|
||||||
#include "../../game/abstract_game.h"
|
|
||||||
#include "../../game/board/card_state.h"
|
|
||||||
#include "../../game/game_state.h"
|
|
||||||
#include "../../game/phase.h"
|
|
||||||
#include "../../game/player/player_actions.h"
|
|
||||||
#include "../../game/player/player_info.h"
|
|
||||||
#include "../../game/player/player_logic.h"
|
|
||||||
#include "../../game/player/player_manager.h"
|
|
||||||
#include "../../game/zones/card_zone_logic.h"
|
|
||||||
#include "../../game/zones/table_zone_logic.h"
|
|
||||||
#include "../../game/zones/view_zone_logic.h"
|
|
||||||
#include "../card_dimensions.h"
|
#include "../card_dimensions.h"
|
||||||
#include "../game_scene.h"
|
#include "../game_scene.h"
|
||||||
#include "arrow_item.h"
|
#include "arrow_item.h"
|
||||||
|
|
@ -41,6 +30,17 @@
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
#include <libcockatrice/card/card_info.h>
|
#include <libcockatrice/card/card_info.h>
|
||||||
|
#include <libcockatrice/game/abstract_game.h>
|
||||||
|
#include <libcockatrice/game/board/card_state.h>
|
||||||
|
#include <libcockatrice/game/game_state.h>
|
||||||
|
#include <libcockatrice/game/phase.h>
|
||||||
|
#include <libcockatrice/game/player/player_actions.h>
|
||||||
|
#include <libcockatrice/game/player/player_info.h>
|
||||||
|
#include <libcockatrice/game/player/player_logic.h>
|
||||||
|
#include <libcockatrice/game/player/player_manager.h>
|
||||||
|
#include <libcockatrice/game/zones/card_zone_logic.h>
|
||||||
|
#include <libcockatrice/game/zones/table_zone_logic.h>
|
||||||
|
#include <libcockatrice/game/zones/view_zone_logic.h>
|
||||||
#include <libcockatrice/protocol/pb/serverinfo_card.pb.h>
|
#include <libcockatrice/protocol/pb/serverinfo_card.pb.h>
|
||||||
#include <libcockatrice/settings/interface_settings.h>
|
#include <libcockatrice/settings/interface_settings.h>
|
||||||
#include <libcockatrice/utility/card_ref.h>
|
#include <libcockatrice/utility/card_ref.h>
|
||||||
|
|
@ -53,8 +53,9 @@ CardItem::CardItem(PlayerLogic *_owner,
|
||||||
const CardRef &cardRef,
|
const CardRef &cardRef,
|
||||||
int _cardid,
|
int _cardid,
|
||||||
CardZoneLogic *_zone)
|
CardZoneLogic *_zone)
|
||||||
: AbstractCardItem(parent, cardRef, _owner, _cardid), state(new CardState(this, _zone)), dragItem(nullptr)
|
: AbstractCardItem(parent, cardRef, _owner, _cardid), dragItem(nullptr)
|
||||||
{
|
{
|
||||||
|
state->setZone(_zone);
|
||||||
owner->addCard(this);
|
owner->addCard(this);
|
||||||
|
|
||||||
connect(&SettingsCache::instance().cardCounters(), &CardCounterSettings::colorChanged, this, [this](int counterId) {
|
connect(&SettingsCache::instance().cardCounters(), &CardCounterSettings::colorChanged, this, [this](int counterId) {
|
||||||
|
|
@ -62,25 +63,44 @@ CardItem::CardItem(PlayerLogic *_owner,
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
connect(state, &CardState::stateReset, this, [this] {
|
||||||
|
if (scene()) {
|
||||||
|
static_cast<GameScene *>(scene())->unregisterAnimationItem(this);
|
||||||
|
}
|
||||||
|
update();
|
||||||
|
});
|
||||||
|
|
||||||
|
// The retyped logic layer drives CardState setters directly (bypassing the CardItem wrappers that called
|
||||||
|
// update()), so repaint on every state change: attacking, annotation, doesntUntap, P/T and counters.
|
||||||
|
connect(state, &CardState::stateChanged, this, [this] { update(); });
|
||||||
|
|
||||||
|
connect(state, &CardState::viewDeleteRequested, this, &CardItem::deleteLater);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CardItem::prepareDelete()
|
void CardItem::prepareDelete()
|
||||||
{
|
{
|
||||||
if (owner != nullptr) {
|
if (owner != nullptr) {
|
||||||
if (owner->getGame()->getActiveCard() == this) {
|
auto *gameScene = qobject_cast<GameScene *>(scene());
|
||||||
|
if (gameScene && gameScene->getActiveCard() == this) {
|
||||||
emit owner->requestCardMenuUpdate(nullptr);
|
emit owner->requestCardMenuUpdate(nullptr);
|
||||||
owner->getGame()->setActiveCard(nullptr);
|
gameScene->setActiveCard(nullptr);
|
||||||
}
|
}
|
||||||
owner = nullptr;
|
owner = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
while (!attachedCards.isEmpty()) {
|
const QList<CardState *> &attachedStates = state->getAttachedCards();
|
||||||
attachedCards.first()->setZone(nullptr); // so that it won't try to call reorganizeCards()
|
while (!attachedStates.isEmpty()) {
|
||||||
attachedCards.first()->setAttachedTo(nullptr);
|
auto *attachedItem = qobject_cast<CardItem *>(attachedStates.first()->parent());
|
||||||
|
if (attachedItem == nullptr) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
attachedItem->setZone(nullptr); // so that it won't try to call reorganizeCards()
|
||||||
|
attachedItem->setAttachedTo(nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state->getAttachedTo() != nullptr) {
|
if (state->getAttachedTo() != nullptr) {
|
||||||
state->getAttachedTo()->removeAttachedCard(this);
|
state->getAttachedTo()->removeAttachedCard(state);
|
||||||
state->setAttachedTo(nullptr);
|
state->setAttachedTo(nullptr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -127,7 +147,7 @@ void CardItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option,
|
||||||
painter->save();
|
painter->save();
|
||||||
transformPainter(painter, translatedSize, tapAngle);
|
transformPainter(painter, translatedSize, tapAngle);
|
||||||
|
|
||||||
if (!getFaceDown() && state->getPT() == exactCard.getInfo().getPowTough()) {
|
if (!getFaceDown() && state->getPT() == state->getCardInfo().getPowTough()) {
|
||||||
painter->setPen(Qt::white);
|
painter->setPen(Qt::white);
|
||||||
} else {
|
} else {
|
||||||
painter->setPen(QColor(255, 150, 0)); // dark orange
|
painter->setPen(QColor(255, 150, 0)); // dark orange
|
||||||
|
|
@ -209,36 +229,13 @@ void CardItem::setPT(const QString &_pt)
|
||||||
|
|
||||||
void CardItem::setAttachedTo(CardItem *_attachedTo)
|
void CardItem::setAttachedTo(CardItem *_attachedTo)
|
||||||
{
|
{
|
||||||
if (state->getAttachedTo() != nullptr) {
|
CardState *newParent = _attachedTo == nullptr ? nullptr : _attachedTo->getState();
|
||||||
state->getAttachedTo()->removeAttachedCard(this);
|
state->setAttachedTo(newParent);
|
||||||
}
|
|
||||||
|
|
||||||
gridPoint.setX(-1);
|
// If the zone is being torn down, it might already be null by the time a card tries to un-attach all its attached
|
||||||
state->setAttachedTo(_attachedTo);
|
// cards
|
||||||
if (state->getAttachedTo() != nullptr) {
|
if (newParent != nullptr ? newParent->getZone() == nullptr : state->getZone() == nullptr) {
|
||||||
// If the zone is being torn down, it might already be null by the time a card tries to un-attach all its
|
deleteLater();
|
||||||
// attached cards
|
|
||||||
if (state->getAttachedTo()->getZone() == nullptr) {
|
|
||||||
deleteLater();
|
|
||||||
} else {
|
|
||||||
emit state->getAttachedTo()->getZone()->cardAdded(this);
|
|
||||||
state->getAttachedTo()->addAttachedCard(this);
|
|
||||||
if (state->getZone() != state->getAttachedTo()->getZone()) {
|
|
||||||
state->getAttachedTo()->getZone()->reorganizeCards();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If the zone is being torn down, it might already be null by the time a card tries to un-attach all its
|
|
||||||
// attached cards
|
|
||||||
if (state->getZone() == nullptr) {
|
|
||||||
deleteLater();
|
|
||||||
} else {
|
|
||||||
emit state->getZone()->cardAdded(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state->getZone() != nullptr) {
|
|
||||||
state->getZone()->reorganizeCards();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -248,13 +245,6 @@ void CardItem::setAttachedTo(CardItem *_attachedTo)
|
||||||
void CardItem::resetState(bool keepAnnotations)
|
void CardItem::resetState(bool keepAnnotations)
|
||||||
{
|
{
|
||||||
state->resetState(keepAnnotations);
|
state->resetState(keepAnnotations);
|
||||||
attachedCards.clear();
|
|
||||||
setTapped(false, false);
|
|
||||||
setDoesntUntap(false);
|
|
||||||
if (scene()) {
|
|
||||||
static_cast<GameScene *>(scene())->unregisterAnimationItem(this);
|
|
||||||
}
|
|
||||||
update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void CardItem::processCardInfo(const ServerInfo_Card &_info)
|
void CardItem::processCardInfo(const ServerInfo_Card &_info)
|
||||||
|
|
@ -390,7 +380,7 @@ void CardItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
|
||||||
|
|
||||||
// Use the buttonDownPos to align the hot spot with the position when
|
// Use the buttonDownPos to align the hot spot with the position when
|
||||||
// the user originally clicked
|
// the user originally clicked
|
||||||
createDragItem(id, event->buttonDownPos(Qt::LeftButton), event->scenePos(), forceFaceDown);
|
createDragItem(state->getId(), event->buttonDownPos(Qt::LeftButton), event->scenePos(), forceFaceDown);
|
||||||
dragItem->grabMouse();
|
dragItem->grabMouse();
|
||||||
|
|
||||||
int childIndex = 0;
|
int childIndex = 0;
|
||||||
|
|
@ -433,42 +423,11 @@ void CardItem::playCard(bool faceDown)
|
||||||
emit playSelected(this);
|
emit playSelected(this);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
state->getZone()->getPlayer()->getPlayerActions()->playCard(this, faceDown);
|
state->getZone()->getPlayer()->getPlayerActions()->playCard(state, faceDown);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariantList CardItem::parsePT(const QString &pt)
|
|
||||||
{
|
|
||||||
QVariantList ptList = QVariantList();
|
|
||||||
if (!pt.isEmpty()) {
|
|
||||||
int sep = pt.indexOf('/');
|
|
||||||
if (sep == 0) {
|
|
||||||
ptList.append(QVariant(pt.mid(1))); // cut off starting '/' and take full string
|
|
||||||
} else {
|
|
||||||
int start = 0;
|
|
||||||
for (;;) {
|
|
||||||
QString item = pt.mid(start, sep - start);
|
|
||||||
if (item.isEmpty()) {
|
|
||||||
ptList.append(QVariant(QString()));
|
|
||||||
} else if (item[0] == '+') {
|
|
||||||
ptList.append(QVariant(item.mid(1).toInt())); // add as int
|
|
||||||
} else if (item[0] == '-') {
|
|
||||||
ptList.append(QVariant(item.toInt())); // add as int
|
|
||||||
} else {
|
|
||||||
ptList.append(QVariant(item)); // add as qstring
|
|
||||||
}
|
|
||||||
if (sep == -1) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
start = sep + 1;
|
|
||||||
sep = pt.indexOf('/', start);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ptList;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief returns true if the zone is a unwritable reveal zone view (eg a card reveal window). Will return false if zone
|
* @brief returns true if the zone is a unwritable reveal zone view (eg a card reveal window). Will return false if zone
|
||||||
* is nullptr.
|
* is nullptr.
|
||||||
|
|
@ -493,7 +452,7 @@ void CardItem::handleClickedToPlay(bool shiftHeld)
|
||||||
if (SettingsCache::instance().userInterface().getClickPlaysAllSelected()) {
|
if (SettingsCache::instance().userInterface().getClickPlaysAllSelected()) {
|
||||||
emit hideSelected(this);
|
emit hideSelected(this);
|
||||||
} else {
|
} else {
|
||||||
state->getZone()->removeCard(this);
|
state->getZone()->removeCard(state);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
playCard(shiftHeld);
|
playCard(shiftHeld);
|
||||||
|
|
@ -537,16 +496,16 @@ bool CardItem::animationEvent()
|
||||||
{
|
{
|
||||||
int rotation = ROTATION_DEGREES_PER_FRAME;
|
int rotation = ROTATION_DEGREES_PER_FRAME;
|
||||||
bool animationIncomplete = true;
|
bool animationIncomplete = true;
|
||||||
if (!tapped) {
|
if (!state->getTapped()) {
|
||||||
rotation *= -1;
|
rotation *= -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
tapAngle += rotation;
|
tapAngle += rotation;
|
||||||
if (tapped && (tapAngle > 90)) {
|
if (state->getTapped() && (tapAngle > 90)) {
|
||||||
tapAngle = 90;
|
tapAngle = 90;
|
||||||
animationIncomplete = false;
|
animationIncomplete = false;
|
||||||
}
|
}
|
||||||
if (!tapped && (tapAngle < 0)) {
|
if (!state->getTapped() && (tapAngle < 0)) {
|
||||||
tapAngle = 0;
|
tapAngle = 0;
|
||||||
animationIncomplete = false;
|
animationIncomplete = false;
|
||||||
}
|
}
|
||||||
|
|
@ -567,7 +526,9 @@ QVariant CardItem::itemChange(GraphicsItemChange change, const QVariant &value)
|
||||||
bool selected = value.toBool();
|
bool selected = value.toBool();
|
||||||
|
|
||||||
if (selected) {
|
if (selected) {
|
||||||
owner->getGame()->setActiveCard(this);
|
if (auto *gameScene = qobject_cast<GameScene *>(scene())) {
|
||||||
|
gameScene->setActiveCard(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
emit selectionChanged(this, selected);
|
emit selectionChanged(this, selected);
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@
|
||||||
#ifndef CARDITEM_H
|
#ifndef CARDITEM_H
|
||||||
#define CARDITEM_H
|
#define CARDITEM_H
|
||||||
|
|
||||||
#include "../../game/board/card_state.h"
|
|
||||||
#include "abstract_card_item.h"
|
#include "abstract_card_item.h"
|
||||||
#include "graphics_item_type.h"
|
#include "graphics_item_type.h"
|
||||||
|
|
||||||
|
|
@ -16,6 +15,7 @@
|
||||||
#include <QPointF>
|
#include <QPointF>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QVariantList>
|
#include <QVariantList>
|
||||||
|
#include <libcockatrice/game/board/card_state.h>
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
|
|
||||||
class CardDragItem;
|
class CardDragItem;
|
||||||
|
|
@ -33,11 +33,7 @@ class CardItem : public AbstractCardItem
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
private:
|
private:
|
||||||
CardState *state;
|
|
||||||
|
|
||||||
QPoint gridPoint;
|
|
||||||
CardDragItem *dragItem;
|
CardDragItem *dragItem;
|
||||||
QList<CardItem *> attachedCards;
|
|
||||||
|
|
||||||
void prepareDelete();
|
void prepareDelete();
|
||||||
void handleClickedToPlay(bool shiftHeld);
|
void handleClickedToPlay(bool shiftHeld);
|
||||||
|
|
@ -60,10 +56,6 @@ public:
|
||||||
CardZoneLogic *_zone = nullptr);
|
CardZoneLogic *_zone = nullptr);
|
||||||
|
|
||||||
void retranslateUi();
|
void retranslateUi();
|
||||||
[[nodiscard]] CardState *getState() const
|
|
||||||
{
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
[[nodiscard]] CardZoneLogic *getZone() const
|
[[nodiscard]] CardZoneLogic *getZone() const
|
||||||
{
|
{
|
||||||
return state->getZone();
|
return state->getZone();
|
||||||
|
|
@ -72,15 +64,15 @@ public:
|
||||||
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
|
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
|
||||||
[[nodiscard]] QPoint getGridPoint() const
|
[[nodiscard]] QPoint getGridPoint() const
|
||||||
{
|
{
|
||||||
return gridPoint;
|
return state->getGridPoint();
|
||||||
}
|
}
|
||||||
void setGridPoint(const QPoint &_gridPoint)
|
void setGridPoint(const QPoint &_gridPoint)
|
||||||
{
|
{
|
||||||
gridPoint = _gridPoint;
|
state->setGridPoint(_gridPoint);
|
||||||
}
|
}
|
||||||
[[nodiscard]] QPoint getGridPos() const
|
[[nodiscard]] QPoint getGridPos() const
|
||||||
{
|
{
|
||||||
return gridPoint;
|
return state->getGridPoint();
|
||||||
}
|
}
|
||||||
[[nodiscard]] PlayerLogic *getOwner() const
|
[[nodiscard]] PlayerLogic *getOwner() const
|
||||||
{
|
{
|
||||||
|
|
@ -123,22 +115,22 @@ public:
|
||||||
{
|
{
|
||||||
state->setDestroyOnZoneChange(_destroy);
|
state->setDestroyOnZoneChange(_destroy);
|
||||||
}
|
}
|
||||||
[[nodiscard]] CardItem *getAttachedTo() const
|
[[nodiscard]] CardState *getAttachedTo() const
|
||||||
{
|
{
|
||||||
return state->getAttachedTo();
|
return state->getAttachedTo();
|
||||||
}
|
}
|
||||||
void setAttachedTo(CardItem *_attachedTo);
|
void setAttachedTo(CardItem *_attachedTo);
|
||||||
void addAttachedCard(CardItem *card)
|
void addAttachedCard(CardState *card)
|
||||||
{
|
{
|
||||||
attachedCards.append(card);
|
state->addAttachedCard(card);
|
||||||
}
|
}
|
||||||
void removeAttachedCard(CardItem *card)
|
void removeAttachedCard(CardState *card)
|
||||||
{
|
{
|
||||||
attachedCards.removeOne(card);
|
state->removeAttachedCard(card);
|
||||||
}
|
}
|
||||||
[[nodiscard]] const QList<CardItem *> &getAttachedCards() const
|
[[nodiscard]] const QList<CardState *> &getAttachedCards() const
|
||||||
{
|
{
|
||||||
return attachedCards;
|
return state->getAttachedCards();
|
||||||
}
|
}
|
||||||
void resetState(bool keepAnnotations = false);
|
void resetState(bool keepAnnotations = false);
|
||||||
void processCardInfo(const ServerInfo_Card &_info);
|
void processCardInfo(const ServerInfo_Card &_info);
|
||||||
|
|
@ -150,26 +142,6 @@ public:
|
||||||
void drawAttachArrow();
|
void drawAttachArrow();
|
||||||
void playCard(bool faceDown);
|
void playCard(bool faceDown);
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Parses a string representing a p/t in order to extract the values from it.
|
|
||||||
*
|
|
||||||
* If the string contains '/', the string will be split at the '/' and each side will be parsed separately,
|
|
||||||
* which means the result list will have two elements.
|
|
||||||
*
|
|
||||||
* If '/' is not found, then the entire string is parsed together, which means the result list will
|
|
||||||
* have a single element.
|
|
||||||
*
|
|
||||||
* If either side of the split is empty, there will also only be a single element in the result list.
|
|
||||||
*
|
|
||||||
* This function will attempt to parse each substring as an int first, handling plus and minus prefixes.
|
|
||||||
* If successful, it will put the parsed value into the QVariant as an int.
|
|
||||||
* If failed, it will just put the substring into the QVariant as a QString.
|
|
||||||
*
|
|
||||||
* @param pt The p/t string
|
|
||||||
* @return A QVariantList that can contain one or two elements, where each QVariant can be either int or QString
|
|
||||||
*/
|
|
||||||
static QVariantList parsePT(const QString &pt);
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
|
void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
|
||||||
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;
|
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#include "counter_general.h"
|
#include "counter_general.h"
|
||||||
|
|
||||||
#include "../../interface/pixel_map_generator.h"
|
#include "../../interface/pixel_map_generator.h"
|
||||||
|
#include "../../interface/theme_manager.h"
|
||||||
#include "abstract_graphics_item.h"
|
#include "abstract_graphics_item.h"
|
||||||
|
|
||||||
#include <QFont>
|
#include <QFont>
|
||||||
|
|
@ -41,7 +42,7 @@ void GeneralCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * /
|
||||||
painter->drawPixmap(QPoint(0, 0), pixmap);
|
painter->drawPixmap(QPoint(0, 0), pixmap);
|
||||||
|
|
||||||
if (value) {
|
if (value) {
|
||||||
QFont f("Serif");
|
QFont f = themeManager->cardTextFont();
|
||||||
f.setPixelSize(qMax((int)(radius * scaleFactor), 10));
|
f.setPixelSize(qMax((int)(radius * scaleFactor), 10));
|
||||||
f.setWeight(QFont::Bold);
|
f.setWeight(QFont::Bold);
|
||||||
painter->setPen(Qt::black);
|
painter->setPen(Qt::black);
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ void DeckViewCardDragItem::updatePosition(const QPointF &cursorScenePos)
|
||||||
|
|
||||||
void DeckViewCardDragItem::handleDrop(DeckViewCardContainer *target)
|
void DeckViewCardDragItem::handleDrop(DeckViewCardContainer *target)
|
||||||
{
|
{
|
||||||
auto *card = static_cast<DeckViewCard *>(item);
|
auto *card = static_cast<DeckViewCard *>(item.data());
|
||||||
auto *start = static_cast<DeckViewCardContainer *>(item->parentItem());
|
auto *start = static_cast<DeckViewCardContainer *>(item->parentItem());
|
||||||
start->removeCard(card);
|
start->removeCard(card);
|
||||||
target->addCard(card);
|
target->addCard(card);
|
||||||
|
|
@ -240,8 +240,8 @@ void DeckViewCardContainer::paint(QPainter *painter, const QStyleOptionGraphicsI
|
||||||
painter->drawLine(QPointF(0, separatorY), QPointF(width, separatorY));
|
painter->drawLine(QPointF(0, separatorY), QPointF(width, separatorY));
|
||||||
|
|
||||||
painter->setPen(QColor(Qt::white));
|
painter->setPen(QColor(Qt::white));
|
||||||
QFont f("Serif");
|
// Family comes from the DeckList theme role; pixel sizes/weights unchanged.
|
||||||
f.setStyleHint(QFont::Serif);
|
QFont f = themeManager->deckListFont();
|
||||||
f.setPixelSize(24);
|
f.setPixelSize(24);
|
||||||
f.setWeight(QFont::Bold);
|
f.setWeight(QFont::Bold);
|
||||||
painter->setFont(f);
|
painter->setFont(f);
|
||||||
|
|
@ -292,8 +292,8 @@ QList<QPair<int, int>> DeckViewCardContainer::getRowsAndCols() const
|
||||||
|
|
||||||
int DeckViewCardContainer::getCardTypeTextWidth() const
|
int DeckViewCardContainer::getCardTypeTextWidth() const
|
||||||
{
|
{
|
||||||
QFont f("Serif");
|
// Family comes from the DeckList theme role; pixel size/weight unchanged.
|
||||||
f.setStyleHint(QFont::Serif);
|
QFont f = themeManager->deckListFont();
|
||||||
f.setPixelSize(16);
|
f.setPixelSize(16);
|
||||||
f.setWeight(QFont::Bold);
|
f.setWeight(QFont::Bold);
|
||||||
QFontMetrics fm(f);
|
QFontMetrics fm(f);
|
||||||
|
|
@ -366,6 +366,15 @@ void DeckViewCardContainer::setWidth(qreal _width)
|
||||||
|
|
||||||
DeckViewScene::DeckViewScene(QObject *parent) : QGraphicsScene(parent), locked(true), deck(0), optimalAspectRatio(1.0)
|
DeckViewScene::DeckViewScene(QObject *parent) : QGraphicsScene(parent), locked(true), deck(0), optimalAspectRatio(1.0)
|
||||||
{
|
{
|
||||||
|
// Zone headers read the DeckList role at paint time and cache their
|
||||||
|
// rendering, so touch every item to invalidate the caches and pick up the
|
||||||
|
// new family on font changes (containers cannot connect this themselves:
|
||||||
|
// QGraphicsItem is not a QObject).
|
||||||
|
connect(themeManager, &ThemeManager::fontsChanged, this, [this] {
|
||||||
|
for (auto *item : items()) {
|
||||||
|
item->update();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
DeckViewScene::~DeckViewScene()
|
DeckViewScene::~DeckViewScene()
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,8 @@
|
||||||
|
|
||||||
#include "../../client/settings/cache_settings.h"
|
#include "../../client/settings/cache_settings.h"
|
||||||
#include "../../client/settings/shortcuts_settings.h"
|
#include "../../client/settings/shortcuts_settings.h"
|
||||||
#include "../../game/abstract_game.h"
|
|
||||||
#include "../../game/game_event_handler.h"
|
|
||||||
#include "../../game/game_state.h"
|
|
||||||
#include "../../interface/card_picture_loader/card_picture_loader.h"
|
#include "../../interface/card_picture_loader/card_picture_loader.h"
|
||||||
#include "../../interface/deck_loader/deck_file_format.h"
|
|
||||||
#include "../../interface/deck_loader/deck_loader.h"
|
#include "../../interface/deck_loader/deck_loader.h"
|
||||||
#include "../../interface/deck_loader/loaded_deck.h"
|
|
||||||
#include "../../interface/widgets/dialogs/dlg_load_deck.h"
|
#include "../../interface/widgets/dialogs/dlg_load_deck.h"
|
||||||
#include "../../interface/widgets/dialogs/dlg_load_deck_from_clipboard.h"
|
#include "../../interface/widgets/dialogs/dlg_load_deck_from_clipboard.h"
|
||||||
#include "../../interface/widgets/dialogs/dlg_load_deck_from_website.h"
|
#include "../../interface/widgets/dialogs/dlg_load_deck_from_website.h"
|
||||||
|
|
@ -33,16 +28,17 @@
|
||||||
#include <QRect>
|
#include <QRect>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <libcockatrice/card/database/card_database_manager.h>
|
#include <libcockatrice/card/database/card_database_manager.h>
|
||||||
|
#include <libcockatrice/deck_list/deck_file_format.h>
|
||||||
|
#include <libcockatrice/deck_list/loaded_deck.h>
|
||||||
#include <libcockatrice/deck_list/playmat_resolver.h>
|
#include <libcockatrice/deck_list/playmat_resolver.h>
|
||||||
#include <libcockatrice/protocol/pb/command_deck_select.pb.h>
|
#include <libcockatrice/game/abstract_game.h>
|
||||||
#include <libcockatrice/protocol/pb/command_ready_start.pb.h>
|
#include <libcockatrice/game/game_event_handler.h>
|
||||||
#include <libcockatrice/protocol/pb/command_set_playmat.pb.h>
|
#include <libcockatrice/game/game_state.h>
|
||||||
#include <libcockatrice/protocol/pb/command_set_sideboard_lock.pb.h>
|
|
||||||
#include <libcockatrice/protocol/pb/command_set_sideboard_plan.pb.h>
|
|
||||||
#include <libcockatrice/protocol/pb/response_deck_download.pb.h>
|
#include <libcockatrice/protocol/pb/response_deck_download.pb.h>
|
||||||
#include <libcockatrice/protocol/pending_command.h>
|
#include <libcockatrice/protocol/pending_command.h>
|
||||||
#include <libcockatrice/settings/interface_settings.h>
|
#include <libcockatrice/settings/interface_settings.h>
|
||||||
#include <libcockatrice/settings/visual_deck_storage_settings.h>
|
#include <libcockatrice/settings/visual_deck_storage_settings.h>
|
||||||
|
#include <libcockatrice/utility/qt_utils.h>
|
||||||
#include <libcockatrice/utility/string_limits.h>
|
#include <libcockatrice/utility/string_limits.h>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <qnamespace.h>
|
#include <qnamespace.h>
|
||||||
|
|
@ -111,10 +107,9 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent)
|
||||||
connect(deckView, &DeckView::newCardAdded, this, &DeckViewContainer::newCardAdded);
|
connect(deckView, &DeckView::newCardAdded, this, &DeckViewContainer::newCardAdded);
|
||||||
connect(deckView, &DeckView::sideboardPlanChanged, this, &DeckViewContainer::sideboardPlanChanged);
|
connect(deckView, &DeckView::sideboardPlanChanged, this, &DeckViewContainer::sideboardPlanChanged);
|
||||||
|
|
||||||
deckViewLayout = new QVBoxLayout;
|
deckViewLayout = QtUtils::makeLayout<QVBoxLayout>();
|
||||||
deckViewLayout->addLayout(buttonHBox);
|
deckViewLayout->addLayout(buttonHBox);
|
||||||
deckViewLayout->addWidget(deckView);
|
deckViewLayout->addWidget(deckView);
|
||||||
deckViewLayout->setContentsMargins(0, 0, 0, 0);
|
|
||||||
setLayout(deckViewLayout);
|
setLayout(deckViewLayout);
|
||||||
|
|
||||||
retranslateUi();
|
retranslateUi();
|
||||||
|
|
@ -314,9 +309,7 @@ void DeckViewContainer::loadDeckFromDeckList(const DeckList &deck)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Command_DeckSelect cmd;
|
PendingCommand *pend = parentGame->getGame()->getGameEventHandler()->prepareDeckSelect(deckString);
|
||||||
cmd.set_deck(deckString.toStdString());
|
|
||||||
PendingCommand *pend = parentGame->getGame()->getGameEventHandler()->prepareGameCommand(cmd);
|
|
||||||
connect(pend, &PendingCommand::finished, this, &DeckViewContainer::deckSelectFinished);
|
connect(pend, &PendingCommand::finished, this, &DeckViewContainer::deckSelectFinished);
|
||||||
parentGame->getGame()->getGameEventHandler()->sendGameCommand(pend, playerId);
|
parentGame->getGame()->getGameEventHandler()->sendGameCommand(pend, playerId);
|
||||||
|
|
||||||
|
|
@ -345,15 +338,20 @@ void DeckViewContainer::resolveAndSendPlaymat()
|
||||||
|
|
||||||
lastResolvedPlaymat = resolved;
|
lastResolvedPlaymat = resolved;
|
||||||
|
|
||||||
Command_SetPlaymat playmatCmd;
|
PendingCommand *playmatPend = parentGame->getGame()->getGameEventHandler()->prepareSetPlaymat(resolved);
|
||||||
auto *pp = playmatCmd.mutable_playmat_params();
|
const QString sentName = resolved.card.name;
|
||||||
pp->set_card_name(resolved.card.name.toStdString());
|
connect(playmatPend, &PendingCommand::finished, this, [this, sentName](const Response &r) {
|
||||||
pp->set_card_provider_id(resolved.card.providerId.toStdString());
|
// Only RespFunctionNotAllowed signals a card art DENY rule. Older servers that predate
|
||||||
pp->set_margin_pct_l(resolved.params.marginPctL);
|
// playmats answer RespInvalidCommand and strip the mat from DeckSelect, which must stay silent.
|
||||||
pp->set_margin_pct_r(resolved.params.marginPctR);
|
if (r.response_code() == Response::RespFunctionNotAllowed) {
|
||||||
pp->set_vertical_offset(resolved.params.verticalOffset);
|
if (!sentName.isEmpty()) {
|
||||||
pp->set_zoom(resolved.params.zoom);
|
notifyPlaymatBlacklisted(
|
||||||
PendingCommand *playmatPend = parentGame->getGame()->getGameEventHandler()->prepareGameCommand(playmatCmd);
|
sentName, tr("The selected playmat card is blacklisted on this server, so it was not applied."));
|
||||||
|
}
|
||||||
|
} else if (r.response_code() == Response::RespOk && !sentName.isEmpty()) {
|
||||||
|
lastBlacklistedPlaymatNotice.clear();
|
||||||
|
}
|
||||||
|
});
|
||||||
parentGame->getGame()->getGameEventHandler()->sendGameCommand(playmatPend, playerId);
|
parentGame->getGame()->getGameEventHandler()->sendGameCommand(playmatPend, playerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -371,9 +369,7 @@ void DeckViewContainer::loadRemoteDeck()
|
||||||
{
|
{
|
||||||
DlgLoadRemoteDeck dlg(parentGame->getGame()->getClientForPlayer(playerId), this);
|
DlgLoadRemoteDeck dlg(parentGame->getGame()->getClientForPlayer(playerId), this);
|
||||||
if (dlg.exec()) {
|
if (dlg.exec()) {
|
||||||
Command_DeckSelect cmd;
|
PendingCommand *pend = parentGame->getGame()->getGameEventHandler()->prepareDeckSelectId(dlg.getDeckId());
|
||||||
cmd.set_deck_id(dlg.getDeckId());
|
|
||||||
PendingCommand *pend = parentGame->getGame()->getGameEventHandler()->prepareGameCommand(cmd);
|
|
||||||
connect(pend, &PendingCommand::finished, this, &DeckViewContainer::deckSelectFinished);
|
connect(pend, &PendingCommand::finished, this, &DeckViewContainer::deckSelectFinished);
|
||||||
parentGame->getGame()->getGameEventHandler()->sendGameCommand(pend, playerId);
|
parentGame->getGame()->getGameEventHandler()->sendGameCommand(pend, playerId);
|
||||||
}
|
}
|
||||||
|
|
@ -412,6 +408,15 @@ void DeckViewContainer::deckSelectFinished(const Response &r)
|
||||||
switchToDeckLoadedView();
|
switchToDeckLoadedView();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DeckViewContainer::notifyPlaymatBlacklisted(const QString &cardName, const QString &message)
|
||||||
|
{
|
||||||
|
if (cardName.isEmpty() || cardName == lastBlacklistedPlaymatNotice) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastBlacklistedPlaymatNotice = cardName;
|
||||||
|
QMessageBox::critical(this, tr("Error"), message);
|
||||||
|
}
|
||||||
|
|
||||||
void DeckViewContainer::readyStart()
|
void DeckViewContainer::readyStart()
|
||||||
{
|
{
|
||||||
sendReadyStartCommand(!readyStartButton->getState());
|
sendReadyStartCommand(!readyStartButton->getState());
|
||||||
|
|
@ -420,34 +425,21 @@ void DeckViewContainer::readyStart()
|
||||||
void DeckViewContainer::forceStart()
|
void DeckViewContainer::forceStart()
|
||||||
{
|
{
|
||||||
const auto msg = tr("Are you sure you want to force start?\nThis will kick all non-ready players from the game.");
|
const auto msg = tr("Are you sure you want to force start?\nThis will kick all non-ready players from the game.");
|
||||||
const auto res =
|
if (!QtUtils::confirm(this, tr("Cockatrice"), msg, QMessageBox::Yes)) {
|
||||||
QMessageBox::question(this, tr("Cockatrice"), msg, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
|
|
||||||
if (res == QMessageBox::No) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Command_ReadyStart cmd;
|
parentGame->getGame()->getGameEventHandler()->sendForceStart(playerId);
|
||||||
cmd.set_force_start(true);
|
|
||||||
cmd.set_ready(true);
|
|
||||||
parentGame->getGame()->getGameEventHandler()->sendGameCommand(cmd, playerId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DeckViewContainer::sideboardLockButtonClicked()
|
void DeckViewContainer::sideboardLockButtonClicked()
|
||||||
{
|
{
|
||||||
Command_SetSideboardLock cmd;
|
parentGame->getGame()->getGameEventHandler()->sendSetSideboardLock(sideboardLockButton->getState(), playerId);
|
||||||
cmd.set_locked(sideboardLockButton->getState());
|
|
||||||
|
|
||||||
parentGame->getGame()->getGameEventHandler()->sendGameCommand(cmd, playerId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DeckViewContainer::sideboardPlanChanged()
|
void DeckViewContainer::sideboardPlanChanged()
|
||||||
{
|
{
|
||||||
Command_SetSideboardPlan cmd;
|
parentGame->getGame()->getGameEventHandler()->sendSetSideboardPlan(deckView->getSideboardPlan(), playerId);
|
||||||
const QList<MoveCard_ToZone> &newPlan = deckView->getSideboardPlan();
|
|
||||||
for (const auto &i : newPlan) {
|
|
||||||
cmd.add_move_list()->CopyFrom(i);
|
|
||||||
}
|
|
||||||
parentGame->getGame()->getGameEventHandler()->sendGameCommand(cmd, playerId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -459,9 +451,7 @@ void DeckViewContainer::sendReadyStartCommand(bool ready)
|
||||||
resolveAndSendPlaymat();
|
resolveAndSendPlaymat();
|
||||||
}
|
}
|
||||||
|
|
||||||
Command_ReadyStart cmd;
|
parentGame->getGame()->getGameEventHandler()->sendReadyStart(ready, playerId);
|
||||||
cmd.set_ready(ready);
|
|
||||||
parentGame->getGame()->getGameEventHandler()->sendGameCommand(cmd, playerId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -60,12 +60,14 @@ private:
|
||||||
VisualDeckStorageWidget *visualDeckStorageWidget;
|
VisualDeckStorageWidget *visualDeckStorageWidget;
|
||||||
TabGame *parentGame;
|
TabGame *parentGame;
|
||||||
int playerId;
|
int playerId;
|
||||||
int playmatRotationIndex = 0; ///< Per-match cursor for round-robin playmat mode.
|
int playmatRotationIndex = 0; ///< Per-match cursor for round-robin playmat mode.
|
||||||
DeckList currentDeck; ///< Cached deck for live settings re-resolution.
|
DeckList currentDeck; ///< Cached deck for live settings re-resolution.
|
||||||
PlaymatInfo lastResolvedPlaymat; ///< Tracks last sent playmat to avoid repeats in random mode.
|
PlaymatInfo lastResolvedPlaymat; ///< Tracks last sent playmat to avoid repeats in random mode.
|
||||||
|
QString lastBlacklistedPlaymatNotice; ///< Card name already reported as blacklisted, to avoid repeat popups.
|
||||||
|
|
||||||
void tryCreateVisualDeckStorageWidget();
|
void tryCreateVisualDeckStorageWidget();
|
||||||
void sendReadyStartCommand(bool ready);
|
void sendReadyStartCommand(bool ready);
|
||||||
|
void notifyPlaymatBlacklisted(const QString &cardName, const QString &message);
|
||||||
private slots:
|
private slots:
|
||||||
void switchToDeckSelectView();
|
void switchToDeckSelectView();
|
||||||
void switchToDeckLoadedView();
|
void switchToDeckLoadedView();
|
||||||
|
|
|
||||||
13
cockatrice/src/game_graphics/dialogs/dlg_annotation.cpp
Normal file
13
cockatrice/src/game_graphics/dialogs/dlg_annotation.cpp
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
#include "dlg_annotation.h"
|
||||||
|
|
||||||
|
#include <QKeyEvent>
|
||||||
|
|
||||||
|
void AnnotationDialog::keyPressEvent(QKeyEvent *event)
|
||||||
|
{
|
||||||
|
if (event->key() == Qt::Key_Return && event->modifiers() & Qt::ControlModifier) {
|
||||||
|
event->accept();
|
||||||
|
accept();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QInputDialog::keyPressEvent(event);
|
||||||
|
}
|
||||||
22
cockatrice/src/game_graphics/dialogs/dlg_annotation.h
Normal file
22
cockatrice/src/game_graphics/dialogs/dlg_annotation.h
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
/**
|
||||||
|
* @file dlg_annotation.h
|
||||||
|
* @ingroup GameDialogs
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef COCKATRICE_DLG_ANNOTATION_H
|
||||||
|
#define COCKATRICE_DLG_ANNOTATION_H
|
||||||
|
|
||||||
|
#include <QInputDialog>
|
||||||
|
|
||||||
|
class AnnotationDialog : public QInputDialog
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
void keyPressEvent(QKeyEvent *e) override;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit AnnotationDialog(QWidget *parent = nullptr) : QInputDialog(parent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // COCKATRICE_DLG_ANNOTATION_H
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
|
#include <libcockatrice/game/player/token_info.h>
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
|
|
||||||
class QLabel;
|
class QLabel;
|
||||||
|
|
@ -25,17 +26,6 @@ class CardInfoPictureWidget;
|
||||||
class QModelIndex;
|
class QModelIndex;
|
||||||
class QWidget;
|
class QWidget;
|
||||||
|
|
||||||
struct TokenInfo
|
|
||||||
{
|
|
||||||
QString name;
|
|
||||||
QString color;
|
|
||||||
QString pt;
|
|
||||||
QString annotation;
|
|
||||||
bool destroy = true;
|
|
||||||
bool faceDown = false;
|
|
||||||
QString providerId;
|
|
||||||
};
|
|
||||||
|
|
||||||
class DlgCreateToken : public QDialog
|
class DlgCreateToken : public QDialog
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
|
#include <libcockatrice/game/player/move_top_cards_until_options.h>
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
|
|
||||||
class FilterString;
|
class FilterString;
|
||||||
|
|
@ -21,13 +22,6 @@ class QLabel;
|
||||||
class QSpinBox;
|
class QSpinBox;
|
||||||
class QWidget;
|
class QWidget;
|
||||||
|
|
||||||
struct MoveTopCardsUntilOptions
|
|
||||||
{
|
|
||||||
QStringList exprs = {};
|
|
||||||
int numberOfHits = 1;
|
|
||||||
bool autoPlay = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
class DlgMoveTopCardsUntil : public QDialog
|
class DlgMoveTopCardsUntil : public QDialog
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,11 @@
|
||||||
#include "game_scene.h"
|
#include "game_scene.h"
|
||||||
|
|
||||||
#include "../client/settings/cache_settings.h"
|
#include "../client/settings/cache_settings.h"
|
||||||
#include "../game/abstract_game.h"
|
#include "../interface/theme_manager.h"
|
||||||
#include "../game/arrow_registry.h"
|
|
||||||
#include "../game/board/arrow_data.h"
|
|
||||||
#include "../game/player/player_actions.h"
|
|
||||||
#include "../game/player/player_event_handler.h"
|
|
||||||
#include "../game/player/player_info.h"
|
|
||||||
#include "../game/player/player_logic.h"
|
|
||||||
#include "../game/zones/card_zone_logic.h"
|
|
||||||
#include "../game/zones/view_zone_logic.h"
|
|
||||||
#include "animated_item.h"
|
#include "animated_item.h"
|
||||||
#include "board/abstract_card_item.h"
|
#include "board/abstract_card_item.h"
|
||||||
#include "board/arrow_item.h"
|
#include "board/arrow_item.h"
|
||||||
|
#include "board/arrow_registry.h"
|
||||||
#include "board/card_item.h"
|
#include "board/card_item.h"
|
||||||
#include "phases_toolbar.h"
|
#include "phases_toolbar.h"
|
||||||
#include "player/menu/player_menu.h"
|
#include "player/menu/player_menu.h"
|
||||||
|
|
@ -40,6 +33,15 @@
|
||||||
#include <QSharedPointer>
|
#include <QSharedPointer>
|
||||||
#include <QtMath>
|
#include <QtMath>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <functional>
|
||||||
|
#include <libcockatrice/game/abstract_game.h>
|
||||||
|
#include <libcockatrice/game/board/arrow_data.h>
|
||||||
|
#include <libcockatrice/game/player/player_actions.h>
|
||||||
|
#include <libcockatrice/game/player/player_event_handler.h>
|
||||||
|
#include <libcockatrice/game/player/player_info.h>
|
||||||
|
#include <libcockatrice/game/player/player_logic.h>
|
||||||
|
#include <libcockatrice/game/zones/card_zone_logic.h>
|
||||||
|
#include <libcockatrice/game/zones/view_zone_logic.h>
|
||||||
#include <libcockatrice/settings/interface_settings.h>
|
#include <libcockatrice/settings/interface_settings.h>
|
||||||
#include <libcockatrice/utility/zone_names.h>
|
#include <libcockatrice/utility/zone_names.h>
|
||||||
#include <numeric>
|
#include <numeric>
|
||||||
|
|
@ -63,6 +65,14 @@ GameScene::GameScene(PhasesToolbar *_phasesToolbar, QObject *parent)
|
||||||
addItem(phasesToolbar);
|
addItem(phasesToolbar);
|
||||||
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::minPlayersForMultiColumnLayoutChanged, this,
|
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::minPlayersForMultiColumnLayoutChanged, this,
|
||||||
&GameScene::rearrange);
|
&GameScene::rearrange);
|
||||||
|
// CardText items (P/T boxes, counters, player names) read the role at
|
||||||
|
// paint time and cache their rendering, so touch every item to invalidate
|
||||||
|
// the caches and pick up the new family on font changes.
|
||||||
|
connect(themeManager, &ThemeManager::fontsChanged, this, [this] {
|
||||||
|
for (auto *item : items()) {
|
||||||
|
item->update();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
rearrange();
|
rearrange();
|
||||||
}
|
}
|
||||||
|
|
@ -112,6 +122,15 @@ void GameScene::retranslateUi()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QList<CardState *> GameScene::selectedCardStates() const
|
||||||
|
{
|
||||||
|
QList<CardState *> selectedStates;
|
||||||
|
for (auto *card : selectedCards()) {
|
||||||
|
selectedStates.append(card->getState());
|
||||||
|
}
|
||||||
|
return selectedStates;
|
||||||
|
}
|
||||||
|
|
||||||
QList<CardItem *> GameScene::selectedCards() const
|
QList<CardItem *> GameScene::selectedCards() const
|
||||||
{
|
{
|
||||||
QList<CardItem *> selectedCards;
|
QList<CardItem *> selectedCards;
|
||||||
|
|
@ -124,6 +143,67 @@ QList<CardItem *> GameScene::selectedCards() const
|
||||||
return selectedCards;
|
return selectedCards;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Selects every card of @p zone for which @p filter holds.
|
||||||
|
* @param filter A predicate to filter which cards are selected.
|
||||||
|
*/
|
||||||
|
static void selectCardsInZone(
|
||||||
|
const CardZoneLogic *zone,
|
||||||
|
std::function<bool(const CardItem *)> filter = [](const CardItem *) { return true; })
|
||||||
|
{
|
||||||
|
if (!zone) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto *cardState : zone->getCards()) {
|
||||||
|
auto *cardItem = cardState == nullptr ? nullptr : qobject_cast<CardItem *>(cardState->parent());
|
||||||
|
if (cardItem && filter(cardItem)) {
|
||||||
|
cardItem->setSelected(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameScene::selectAllInZone(const CardItem *card)
|
||||||
|
{
|
||||||
|
if (!card) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectCardsInZone(card->getZone());
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameScene::selectSameRow(const CardItem *card)
|
||||||
|
{
|
||||||
|
if (!card) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto isSameRow = [card](const CardItem *cardItem) {
|
||||||
|
return qAbs(card->scenePos().y() - cardItem->scenePos().y()) < 50;
|
||||||
|
};
|
||||||
|
selectCardsInZone(card->getZone(), isSameRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameScene::selectSameColumn(const CardItem *card)
|
||||||
|
{
|
||||||
|
if (!card) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto isSameColumn = [card](const CardItem *cardItem) { return cardItem->x() == card->x(); };
|
||||||
|
selectCardsInZone(card->getZone(), isSameColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
CardItem *GameScene::getActiveCard() const
|
||||||
|
{
|
||||||
|
return activeCard;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameScene::setActiveCard(CardItem *card)
|
||||||
|
{
|
||||||
|
activeCard = card;
|
||||||
|
}
|
||||||
|
|
||||||
void GameScene::onCardSelectionChanged(AbstractCardItem *abstractCard, bool selected)
|
void GameScene::onCardSelectionChanged(AbstractCardItem *abstractCard, bool selected)
|
||||||
{
|
{
|
||||||
CardItem *card = qobject_cast<CardItem *>(abstractCard);
|
CardItem *card = qobject_cast<CardItem *>(abstractCard);
|
||||||
|
|
@ -134,12 +214,12 @@ void GameScene::onCardSelectionChanged(AbstractCardItem *abstractCard, bool sele
|
||||||
auto *owner = card->getOwner();
|
auto *owner = card->getOwner();
|
||||||
|
|
||||||
if (selected) {
|
if (selected) {
|
||||||
owner->requestCardMenuUpdate(card);
|
owner->requestCardMenuUpdate(card->getState());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedItems().isEmpty()) {
|
if (selectedItems().isEmpty()) {
|
||||||
owner->getGame()->setActiveCard(nullptr);
|
setActiveCard(nullptr);
|
||||||
owner->requestCardMenuUpdate(nullptr);
|
owner->requestCardMenuUpdate(nullptr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -158,9 +238,9 @@ void GameScene::onCardRightClicked(AbstractCardItem *abstractCard, QPoint screen
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
card->getOwner()->getGame()->setActiveCard(card);
|
setActiveCard(card);
|
||||||
|
|
||||||
if (auto *menu = view->getPlayerMenu()->updateCardMenu(card)) {
|
if (auto *menu = view->getPlayerMenu()->updateCardMenu(card->getState())) {
|
||||||
menu->popup(screenPos);
|
menu->popup(screenPos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -173,7 +253,7 @@ void GameScene::playSelected(AbstractCardItem *card)
|
||||||
if (!card->getOwner()) {
|
if (!card->getOwner()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
card->getOwner()->getPlayerActions()->actPlay(selectedCards());
|
card->getOwner()->getPlayerActions()->actPlay(selectedCardStates());
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameScene::playSelectedFaceDown(AbstractCardItem *card)
|
void GameScene::playSelectedFaceDown(AbstractCardItem *card)
|
||||||
|
|
@ -184,7 +264,7 @@ void GameScene::playSelectedFaceDown(AbstractCardItem *card)
|
||||||
if (!card->getOwner()) {
|
if (!card->getOwner()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
card->getOwner()->getPlayerActions()->actPlayFacedown(selectedCards());
|
card->getOwner()->getPlayerActions()->actPlayFacedown(selectedCardStates());
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameScene::hideSelected(AbstractCardItem *card)
|
void GameScene::hideSelected(AbstractCardItem *card)
|
||||||
|
|
@ -195,7 +275,7 @@ void GameScene::hideSelected(AbstractCardItem *card)
|
||||||
if (!card->getOwner()) {
|
if (!card->getOwner()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
card->getOwner()->getPlayerActions()->actHide(selectedCards());
|
card->getOwner()->getPlayerActions()->actHide(selectedCardStates());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -230,6 +310,8 @@ void GameScene::addPlayer(PlayerLogic *player)
|
||||||
[this, id = player->getPlayerInfo()->getId()]() { clearArrowsForPlayerLocally(id); });
|
[this, id = player->getPlayerInfo()->getId()]() { clearArrowsForPlayerLocally(id); });
|
||||||
|
|
||||||
connect(player->getPlayerEventHandler(), &PlayerEventHandler::cardZoneChanged, this, &GameScene::onCardZoneChanged);
|
connect(player->getPlayerEventHandler(), &PlayerEventHandler::cardZoneChanged, this, &GameScene::onCardZoneChanged);
|
||||||
|
connect(player->getPlayerEventHandler(), &PlayerEventHandler::cardViewRefreshRequested, this,
|
||||||
|
&GameScene::onCardViewRefreshRequested);
|
||||||
|
|
||||||
rearrange();
|
rearrange();
|
||||||
}
|
}
|
||||||
|
|
@ -533,7 +615,11 @@ void GameScene::addArrow(QSharedPointer<ArrowData> data)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
CardItem *startCard = startZone->getCard(data->startCardId);
|
CardState *startCardState = startZone->getCard(data->startCardId);
|
||||||
|
if (!startCardState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CardItem *startCard = qobject_cast<CardItem *>(startCardState->parent());
|
||||||
if (!startCard) {
|
if (!startCard) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -544,7 +630,8 @@ void GameScene::addArrow(QSharedPointer<ArrowData> data)
|
||||||
} else {
|
} else {
|
||||||
auto *zone = targetView->getLogic()->getZones().value(data->targetZone);
|
auto *zone = targetView->getLogic()->getZones().value(data->targetZone);
|
||||||
if (zone) {
|
if (zone) {
|
||||||
targetItem = zone->getCard(data->targetCardId);
|
CardState *targetCardState = zone->getCard(data->targetCardId);
|
||||||
|
targetItem = targetCardState == nullptr ? nullptr : qobject_cast<CardItem *>(targetCardState->parent());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!targetItem) {
|
if (!targetItem) {
|
||||||
|
|
@ -572,11 +659,15 @@ void GameScene::requestArrowDeletion(int playerId, int arrowId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameScene::onCardZoneChanged(CardItem *card, bool sameZone)
|
void GameScene::onCardZoneChanged(CardState *card, bool sameZone)
|
||||||
{
|
{
|
||||||
|
auto *item = qobject_cast<CardItem *>(card->parent());
|
||||||
|
if (!item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
QList<ArrowItem *> toDelete;
|
QList<ArrowItem *> toDelete;
|
||||||
for (auto *arrow : arrowRegistry.all()) {
|
for (auto *arrow : arrowRegistry.all()) {
|
||||||
if (arrow->getStartItem() == card || arrow->getTargetItem() == card) {
|
if (arrow->getStartItem() == item || arrow->getTargetItem() == item) {
|
||||||
if (sameZone) {
|
if (sameZone) {
|
||||||
arrow->updatePath();
|
arrow->updatePath();
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -589,6 +680,25 @@ void GameScene::onCardZoneChanged(CardItem *card, bool sameZone)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameScene::onCardViewRefreshRequested(CardState *card, bool zoneChanged, PlayerLogic *newOwner)
|
||||||
|
{
|
||||||
|
auto *item = qobject_cast<CardItem *>(card->parent());
|
||||||
|
if (!item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (zoneChanged) {
|
||||||
|
item->deleteCardInfoPopup();
|
||||||
|
}
|
||||||
|
item->deleteDragItem();
|
||||||
|
if (zoneChanged) {
|
||||||
|
item->setBeingPointedAt(false);
|
||||||
|
item->setHovered(false);
|
||||||
|
}
|
||||||
|
if (newOwner) {
|
||||||
|
item->setOwner(newOwner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void GameScene::clearArrowsForPlayer(int playerId)
|
void GameScene::clearArrowsForPlayer(int playerId)
|
||||||
{
|
{
|
||||||
for (int arrowId : arrowRegistry.idsForPlayer(playerId)) {
|
for (int arrowId : arrowRegistry.idsForPlayer(playerId)) {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
#ifndef GAMESCENE_H
|
#ifndef GAMESCENE_H
|
||||||
#define GAMESCENE_H
|
#define GAMESCENE_H
|
||||||
|
|
||||||
#include "../game/arrow_registry.h"
|
#include "animated_item.h"
|
||||||
|
#include "board/arrow_item.h"
|
||||||
|
#include "board/arrow_registry.h"
|
||||||
|
|
||||||
#include <QGraphicsScene>
|
#include <QGraphicsScene>
|
||||||
#include <QHash>
|
#include <QHash>
|
||||||
|
|
@ -16,6 +18,8 @@
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QTransform>
|
#include <QTransform>
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
|
#include <libcockatrice/game/board/arrow_data.h>
|
||||||
|
#include <libcockatrice/game/zones/card_zone_logic.h>
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
|
|
||||||
// IWYU pragma: keep
|
// IWYU pragma: keep
|
||||||
|
|
@ -39,6 +43,7 @@ class ZoneViewWidget;
|
||||||
class CardZone;
|
class CardZone;
|
||||||
class AbstractCardItem;
|
class AbstractCardItem;
|
||||||
class CardItem;
|
class CardItem;
|
||||||
|
class CardState;
|
||||||
class ServerInfo_Card;
|
class ServerInfo_Card;
|
||||||
class PhasesToolbar;
|
class PhasesToolbar;
|
||||||
class QBasicTimer;
|
class QBasicTimer;
|
||||||
|
|
@ -69,6 +74,7 @@ private:
|
||||||
QList<ZoneViewWidget *> zoneViews; ///< Active zone view widgets
|
QList<ZoneViewWidget *> zoneViews; ///< Active zone view widgets
|
||||||
QSize viewSize; ///< Current view size
|
QSize viewSize; ///< Current view size
|
||||||
QPointer<CardItem> hoveredCard; ///< Currently hovered card
|
QPointer<CardItem> hoveredCard; ///< Currently hovered card
|
||||||
|
QPointer<CardItem> activeCard; ///< Card the currently open context menu targets
|
||||||
QBasicTimer *animationTimer; ///< Timer for scene animations
|
QBasicTimer *animationTimer; ///< Timer for scene animations
|
||||||
QHash<QObject *, IAnimatedItem *> animatedItems; ///< Items currently animating
|
QHash<QObject *, IAnimatedItem *> animatedItems; ///< Items currently animating
|
||||||
QHash<QObject *, QMetaObject::Connection>
|
QHash<QObject *, QMetaObject::Connection>
|
||||||
|
|
@ -107,6 +113,22 @@ public:
|
||||||
|
|
||||||
/** @brief Gets all selected CardItems. */
|
/** @brief Gets all selected CardItems. */
|
||||||
QList<CardItem *> selectedCards() const;
|
QList<CardItem *> selectedCards() const;
|
||||||
|
QList<CardState *> selectedCardStates() const;
|
||||||
|
|
||||||
|
/** @brief Selects every card sharing the zone of @p card. */
|
||||||
|
void selectAllInZone(const CardItem *card);
|
||||||
|
|
||||||
|
/** @brief Selects the cards laid out in the same visual row as @p card. */
|
||||||
|
void selectSameRow(const CardItem *card);
|
||||||
|
|
||||||
|
/** @brief Selects the cards laid out in the same visual column as @p card. */
|
||||||
|
void selectSameColumn(const CardItem *card);
|
||||||
|
|
||||||
|
/** @brief Card the currently open context menu targets, or null. */
|
||||||
|
CardItem *getActiveCard() const;
|
||||||
|
|
||||||
|
/** @brief Sets the card the currently open context menu targets. */
|
||||||
|
void setActiveCard(CardItem *card);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Adds a player to the scene and stores their graphics item.
|
* @brief Adds a player to the scene and stores their graphics item.
|
||||||
|
|
@ -261,7 +283,15 @@ public slots:
|
||||||
/// Queues up arrow deletion but doesn't directly modify the scene
|
/// Queues up arrow deletion but doesn't directly modify the scene
|
||||||
void requestArrowDeletion(int playerId, int arrowId);
|
void requestArrowDeletion(int playerId, int arrowId);
|
||||||
|
|
||||||
void onCardZoneChanged(CardItem *card, bool sameZone);
|
void onCardZoneChanged(CardState *card, bool sameZone);
|
||||||
|
/**
|
||||||
|
* @brief Refreshes the view of a card that the server just moved.
|
||||||
|
*
|
||||||
|
* @param card The card that moved.
|
||||||
|
* @param zoneChanged Whether the card crossed a zone boundary.
|
||||||
|
* @param newOwner The player the card now belongs to, or nullptr to leave the owner alone.
|
||||||
|
*/
|
||||||
|
void onCardViewRefreshRequested(CardState *card, bool zoneChanged, PlayerLogic *newOwner);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
/** @brief Handles hover updates. */
|
/** @brief Handles hover updates. */
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,14 @@
|
||||||
|
|
||||||
#include "../client/settings/cache_settings.h"
|
#include "../client/settings/cache_settings.h"
|
||||||
#include "../client/settings/shortcuts_settings.h"
|
#include "../client/settings/shortcuts_settings.h"
|
||||||
|
#include "../interface/theme_manager.h"
|
||||||
#include "game_scene.h"
|
#include "game_scene.h"
|
||||||
|
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
#include <QBrush>
|
#include <QBrush>
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QEvent>
|
#include <QEvent>
|
||||||
|
#include <QFont>
|
||||||
#include <QGraphicsScene>
|
#include <QGraphicsScene>
|
||||||
#include <QGridLayout>
|
#include <QGridLayout>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
|
|
@ -24,6 +26,21 @@
|
||||||
#include <libcockatrice/utility/qt_utils.h>
|
#include <libcockatrice/utility/qt_utils.h>
|
||||||
#include <qnamespace.h>
|
#include <qnamespace.h>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
QFont deckOverlayFont(int pixelSize, bool bold)
|
||||||
|
{
|
||||||
|
// The drag/total counters and the tally columns are in-game overlays, not
|
||||||
|
// deck list text: keep the monospace family the stylesheets used to set so
|
||||||
|
// digit widths stay stable as counts tick and `white-space: pre` lines up.
|
||||||
|
QFont font(QStringLiteral("monospace"));
|
||||||
|
font.setStyleHint(QFont::Monospace);
|
||||||
|
font.setPixelSize(pixelSize);
|
||||||
|
font.setBold(bold);
|
||||||
|
return font;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
// QRubberBand calls raise() in showEvent() and changeEvent() to stay on top of siblings.
|
// QRubberBand calls raise() in showEvent() and changeEvent() to stay on top of siblings.
|
||||||
// This subclass disables that behavior so dragCountLabel can appear above it.
|
// This subclass disables that behavior so dragCountLabel can appear above it.
|
||||||
class SelectionRubberBand : public QRubberBand
|
class SelectionRubberBand : public QRubberBand
|
||||||
|
|
@ -75,15 +92,14 @@ GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, par
|
||||||
rubberBand = new SelectionRubberBand(QRubberBand::Rectangle, this);
|
rubberBand = new SelectionRubberBand(QRubberBand::Rectangle, this);
|
||||||
|
|
||||||
const QString baseProperties = "color: white; "
|
const QString baseProperties = "color: white; "
|
||||||
"font-family: monospace; "
|
|
||||||
"background-color: rgba(0, 0, 0, 160); "
|
"background-color: rgba(0, 0, 0, 160); "
|
||||||
"border-radius: 3px; "
|
"border-radius: 3px; "
|
||||||
"padding: 1px 2px; "
|
"padding: 1px 2px; "
|
||||||
"white-space: pre;";
|
"white-space: pre;";
|
||||||
|
|
||||||
const QString dragCountLabelStyle = baseProperties + "font-size: 14px; font-weight: bold;";
|
const QString dragCountLabelStyle = baseProperties;
|
||||||
const QString totalCountLabelStyle = baseProperties + "font-size: 16px; font-weight: bold;";
|
const QString totalCountLabelStyle = baseProperties;
|
||||||
const QString subtypeTallyLabelStyle = baseProperties + "font-size: 12px;";
|
const QString subtypeTallyLabelStyle = baseProperties;
|
||||||
|
|
||||||
dragCountLabel = new QLabel(this);
|
dragCountLabel = new QLabel(this);
|
||||||
dragCountLabel->setStyleSheet(dragCountLabelStyle);
|
dragCountLabel->setStyleSheet(dragCountLabelStyle);
|
||||||
|
|
@ -100,6 +116,27 @@ GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, par
|
||||||
tallyLayout->setContentsMargins(2, 2, 2, 2);
|
tallyLayout->setContentsMargins(2, 2, 2, 2);
|
||||||
tallyLayout->setSpacing(2);
|
tallyLayout->setSpacing(2);
|
||||||
tallyContainer->hide();
|
tallyContainer->hide();
|
||||||
|
|
||||||
|
applyDeckOverlayFonts();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameView::applyDeckOverlayFonts()
|
||||||
|
{
|
||||||
|
dragCountLabel->setFont(deckOverlayFont(14, true));
|
||||||
|
totalCountLabel->setFont(deckOverlayFont(16, true));
|
||||||
|
tallyContainer->setFont(deckOverlayFont(12, false));
|
||||||
|
for (int i = 0; i < tallyLayout->count(); ++i) {
|
||||||
|
if (auto *label = qobject_cast<QLabel *>(tallyLayout->itemAt(i)->widget())) {
|
||||||
|
// Count column (odd positions) keeps the bold emphasis.
|
||||||
|
const bool count = i % 2 == 1;
|
||||||
|
label->setFont(deckOverlayFont(count ? 14 : 12, count));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The container keeps the size computed from the previous fonts, so force
|
||||||
|
// rebuildTallyLabels() to re-measure it instead of reusing cachedTallyRows.
|
||||||
|
cachedTallyRows.clear();
|
||||||
|
updateTotalSelectionCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameView::resizeEvent(QResizeEvent *event)
|
void GameView::resizeEvent(QResizeEvent *event)
|
||||||
|
|
@ -219,9 +256,8 @@ QSize GameView::rebuildTallyLabels(const QList<TallyRow> &entries)
|
||||||
{
|
{
|
||||||
clearTallyLabels();
|
clearTallyLabels();
|
||||||
|
|
||||||
const QString nameStyle = QStringLiteral("color: white; font-size: 12px; background: transparent;");
|
const QString nameStyle = QStringLiteral("color: white; background: transparent;");
|
||||||
const QString countStyle =
|
const QString countStyle = QStringLiteral("color: white; background: transparent;");
|
||||||
QStringLiteral("color: white; font-size: 14px; font-weight: bold; background: transparent;");
|
|
||||||
|
|
||||||
int totalHeight = 0;
|
int totalHeight = 0;
|
||||||
int maxNameWidth = 0;
|
int maxNameWidth = 0;
|
||||||
|
|
@ -231,11 +267,13 @@ QSize GameView::rebuildTallyLabels(const QList<TallyRow> &entries)
|
||||||
for (const TallyRow &entry : entries) {
|
for (const TallyRow &entry : entries) {
|
||||||
auto *nameLabel = new QLabel(entry.name, tallyContainer);
|
auto *nameLabel = new QLabel(entry.name, tallyContainer);
|
||||||
nameLabel->setStyleSheet(nameStyle);
|
nameLabel->setStyleSheet(nameStyle);
|
||||||
|
nameLabel->setFont(deckOverlayFont(12, false));
|
||||||
nameLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
nameLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||||||
tallyLayout->addWidget(nameLabel, row, 0);
|
tallyLayout->addWidget(nameLabel, row, 0);
|
||||||
|
|
||||||
auto *countLabel = new QLabel(entry.value, tallyContainer);
|
auto *countLabel = new QLabel(entry.value, tallyContainer);
|
||||||
countLabel->setStyleSheet(countStyle);
|
countLabel->setStyleSheet(countStyle);
|
||||||
|
countLabel->setFont(deckOverlayFont(14, true));
|
||||||
countLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
countLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||||||
tallyLayout->addWidget(countLabel, row, 1);
|
tallyLayout->addWidget(countLabel, row, 1);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ private slots:
|
||||||
void refreshShortcuts();
|
void refreshShortcuts();
|
||||||
void updateTotalSelectionCount(const QSize &viewSize = QSize());
|
void updateTotalSelectionCount(const QSize &viewSize = QSize());
|
||||||
void setFocusDisabled(bool disabled);
|
void setFocusDisabled(bool disabled);
|
||||||
|
void applyDeckOverlayFonts();
|
||||||
public slots:
|
public slots:
|
||||||
void updateSceneRect(const QRectF &rect);
|
void updateSceneRect(const QRectF &rect);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
#include "hand_counter.h"
|
#include "hand_counter.h"
|
||||||
|
|
||||||
#include "../game/board/card_list.h"
|
|
||||||
#include "../game/zones/card_zone_logic.h"
|
|
||||||
#include "../interface/pixel_map_generator.h"
|
#include "../interface/pixel_map_generator.h"
|
||||||
|
|
||||||
#include <QGraphicsSceneMouseEvent>
|
#include <QGraphicsSceneMouseEvent>
|
||||||
|
|
@ -14,6 +12,8 @@
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QStringLiteral>
|
#include <QStringLiteral>
|
||||||
#include <QTransform>
|
#include <QTransform>
|
||||||
|
#include <libcockatrice/game/board/card_list.h>
|
||||||
|
#include <libcockatrice/game/zones/card_zone_logic.h>
|
||||||
#include <qnamespace.h>
|
#include <qnamespace.h>
|
||||||
|
|
||||||
class QGraphicsItem;
|
class QGraphicsItem;
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,6 @@
|
||||||
#include "../../client/settings/cache_settings.h"
|
#include "../../client/settings/cache_settings.h"
|
||||||
#include "../../client/settings/card_counter_settings.h"
|
#include "../../client/settings/card_counter_settings.h"
|
||||||
#include "../../client/sound_engine.h"
|
#include "../../client/sound_engine.h"
|
||||||
#include "../../client/translation.h"
|
|
||||||
#include "../../game/abstract_game.h"
|
|
||||||
#include "../../game/board/card_list.h"
|
|
||||||
#include "../../game/game_state.h"
|
|
||||||
#include "../../game/phase.h"
|
|
||||||
#include "../../game/player/player_event_handler.h"
|
|
||||||
#include "../../game/player/player_info.h"
|
|
||||||
#include "../../game/player/player_logic.h"
|
|
||||||
#include "../../game/player/player_manager.h"
|
|
||||||
#include "../../game/zones/card_zone_logic.h"
|
|
||||||
#include "../../interface/card_localization.h"
|
#include "../../interface/card_localization.h"
|
||||||
#include "../../interface/widgets/server/chat_view/chat_view.h"
|
#include "../../interface/widgets/server/chat_view/chat_view.h"
|
||||||
#include "../board/card_item.h"
|
#include "../board/card_item.h"
|
||||||
|
|
@ -24,6 +14,16 @@
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QSharedPointer>
|
#include <QSharedPointer>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
|
#include <libcockatrice/game/abstract_game.h>
|
||||||
|
#include <libcockatrice/game/board/card_list.h>
|
||||||
|
#include <libcockatrice/game/game_state.h>
|
||||||
|
#include <libcockatrice/game/phase.h>
|
||||||
|
#include <libcockatrice/game/player/player_event_handler.h>
|
||||||
|
#include <libcockatrice/game/player/player_info.h>
|
||||||
|
#include <libcockatrice/game/player/player_logic.h>
|
||||||
|
#include <libcockatrice/game/player/player_manager.h>
|
||||||
|
#include <libcockatrice/game/translation.h>
|
||||||
|
#include <libcockatrice/game/zones/card_zone_logic.h>
|
||||||
#include <libcockatrice/protocol/pb/context_move_card.pb.h>
|
#include <libcockatrice/protocol/pb/context_move_card.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/context_mulligan.pb.h>
|
#include <libcockatrice/protocol/pb/context_mulligan.pb.h>
|
||||||
#include <libcockatrice/settings/chat_settings.h>
|
#include <libcockatrice/settings/chat_settings.h>
|
||||||
|
|
@ -283,7 +283,7 @@ void MessageLogWidget::logDestroyCard(PlayerLogic *player, QString cardName)
|
||||||
}
|
}
|
||||||
|
|
||||||
void MessageLogWidget::logMoveCard(PlayerLogic *player,
|
void MessageLogWidget::logMoveCard(PlayerLogic *player,
|
||||||
CardItem *card,
|
CardState *card,
|
||||||
CardZoneLogic *startZone,
|
CardZoneLogic *startZone,
|
||||||
int oldX,
|
int oldX,
|
||||||
CardZoneLogic *targetZone,
|
CardZoneLogic *targetZone,
|
||||||
|
|
@ -662,7 +662,7 @@ void MessageLogWidget::logSetActivePlayer(PlayerLogic *player)
|
||||||
QString(tr("%1's turn.")).arg(player->getPlayerInfo()->getName()) + "</b></font><br>");
|
QString(tr("%1's turn.")).arg(player->getPlayerInfo()->getName()) + "</b></font><br>");
|
||||||
}
|
}
|
||||||
|
|
||||||
void MessageLogWidget::logSetAnnotation(PlayerLogic *player, CardItem *card, QString newAnnotation)
|
void MessageLogWidget::logSetAnnotation(PlayerLogic *player, CardState *card, QString newAnnotation)
|
||||||
{
|
{
|
||||||
appendHtmlServerMessage(
|
appendHtmlServerMessage(
|
||||||
QString(tr("%1 sets annotation of %2 to %3."))
|
QString(tr("%1 sets annotation of %2 to %3."))
|
||||||
|
|
@ -706,7 +706,7 @@ void MessageLogWidget::logSetCounter(PlayerLogic *player, QString counterName, i
|
||||||
.arg(value - oldValue));
|
.arg(value - oldValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
void MessageLogWidget::logSetDoesntUntap(PlayerLogic *player, CardItem *card, bool doesntUntap)
|
void MessageLogWidget::logSetDoesntUntap(PlayerLogic *player, CardState *card, bool doesntUntap)
|
||||||
{
|
{
|
||||||
QString str;
|
QString str;
|
||||||
if (doesntUntap) {
|
if (doesntUntap) {
|
||||||
|
|
@ -717,7 +717,7 @@ void MessageLogWidget::logSetDoesntUntap(PlayerLogic *player, CardItem *card, bo
|
||||||
appendHtmlServerMessage(str.arg(sanitizeHtml(player->getPlayerInfo()->getName())).arg(cardLink(card->getName())));
|
appendHtmlServerMessage(str.arg(sanitizeHtml(player->getPlayerInfo()->getName())).arg(cardLink(card->getName())));
|
||||||
}
|
}
|
||||||
|
|
||||||
void MessageLogWidget::logSetPT(PlayerLogic *player, CardItem *card, QString newPT)
|
void MessageLogWidget::logSetPT(PlayerLogic *player, CardState *card, QString newPT)
|
||||||
{
|
{
|
||||||
if (currentContext == MessageContext_MoveCard) {
|
if (currentContext == MessageContext_MoveCard) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -755,7 +755,7 @@ void MessageLogWidget::logSetSideboardLock(PlayerLogic *player, bool locked)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MessageLogWidget::logSetTapped(PlayerLogic *player, CardItem *card, bool tapped)
|
void MessageLogWidget::logSetTapped(PlayerLogic *player, CardState *card, bool tapped)
|
||||||
{
|
{
|
||||||
if (currentContext == MessageContext_MoveCard) {
|
if (currentContext == MessageContext_MoveCard) {
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
|
|
||||||
|
class CardState;
|
||||||
class AbstractGame;
|
class AbstractGame;
|
||||||
class CardItem;
|
class CardItem;
|
||||||
class GameEventContext;
|
class GameEventContext;
|
||||||
|
|
@ -76,7 +77,7 @@ public slots:
|
||||||
void logLeaveSpectator(QString name, QString reason);
|
void logLeaveSpectator(QString name, QString reason);
|
||||||
void logNotReadyStart(PlayerLogic *player);
|
void logNotReadyStart(PlayerLogic *player);
|
||||||
void logMoveCard(PlayerLogic *player,
|
void logMoveCard(PlayerLogic *player,
|
||||||
CardItem *card,
|
CardState *card,
|
||||||
CardZoneLogic *startZone,
|
CardZoneLogic *startZone,
|
||||||
int oldX,
|
int oldX,
|
||||||
CardZoneLogic *targetZone,
|
CardZoneLogic *targetZone,
|
||||||
|
|
@ -97,13 +98,13 @@ public slots:
|
||||||
void logSay(PlayerLogic *player, QString message);
|
void logSay(PlayerLogic *player, QString message);
|
||||||
void logSetActivePhase(int phase);
|
void logSetActivePhase(int phase);
|
||||||
void logSetActivePlayer(PlayerLogic *player);
|
void logSetActivePlayer(PlayerLogic *player);
|
||||||
void logSetAnnotation(PlayerLogic *player, CardItem *card, QString newAnnotation);
|
void logSetAnnotation(PlayerLogic *player, CardState *card, QString newAnnotation);
|
||||||
void logSetCardCounter(PlayerLogic *player, QString cardName, int counterId, int value, int oldValue);
|
void logSetCardCounter(PlayerLogic *player, QString cardName, int counterId, int value, int oldValue);
|
||||||
void logSetCounter(PlayerLogic *player, QString counterName, int value, int oldValue);
|
void logSetCounter(PlayerLogic *player, QString counterName, int value, int oldValue);
|
||||||
void logSetDoesntUntap(PlayerLogic *player, CardItem *card, bool doesntUntap);
|
void logSetDoesntUntap(PlayerLogic *player, CardState *card, bool doesntUntap);
|
||||||
void logSetPT(PlayerLogic *player, CardItem *card, QString newPT);
|
void logSetPT(PlayerLogic *player, CardState *card, QString newPT);
|
||||||
void logSetSideboardLock(PlayerLogic *player, bool locked);
|
void logSetSideboardLock(PlayerLogic *player, bool locked);
|
||||||
void logSetTapped(PlayerLogic *player, CardItem *card, bool tapped);
|
void logSetTapped(PlayerLogic *player, CardState *card, bool tapped);
|
||||||
void logShuffle(PlayerLogic *player, CardZoneLogic *zone, int start, int end);
|
void logShuffle(PlayerLogic *player, CardZoneLogic *zone, int start, int end);
|
||||||
void logSpectatorSay(const ServerInfo_User &spectator, QString message);
|
void logSpectatorSay(const ServerInfo_User &spectator, QString message);
|
||||||
void logUnattachCard(PlayerLogic *player, QString cardName);
|
void logUnattachCard(PlayerLogic *player, QString cardName);
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,6 @@
|
||||||
#include <QTransform>
|
#include <QTransform>
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
#include <google/protobuf/stubs/port.h>
|
#include <google/protobuf/stubs/port.h>
|
||||||
#include <libcockatrice/protocol/pb/command_draw_cards.pb.h>
|
|
||||||
#include <libcockatrice/protocol/pb/command_next_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/utility/zone_names.h>
|
|
||||||
#include <qnamespace.h>
|
#include <qnamespace.h>
|
||||||
#include <qnumeric.h>
|
#include <qnumeric.h>
|
||||||
|
|
||||||
|
|
@ -120,9 +115,9 @@ PhasesToolbar::PhasesToolbar(QGraphicsItem *parent)
|
||||||
: QGraphicsItem(parent), width(100), height(100), ySpacing(1), symbolSize(8)
|
: QGraphicsItem(parent), width(100), height(100), ySpacing(1), symbolSize(8)
|
||||||
{
|
{
|
||||||
auto *aUntapAll = new QAction(this);
|
auto *aUntapAll = new QAction(this);
|
||||||
connect(aUntapAll, &QAction::triggered, this, &PhasesToolbar::actUntapAll);
|
connect(aUntapAll, &QAction::triggered, this, &PhasesToolbar::untapAllRequested);
|
||||||
auto *aDrawCard = new QAction(this);
|
auto *aDrawCard = new QAction(this);
|
||||||
connect(aDrawCard, &QAction::triggered, this, &PhasesToolbar::actDrawCard);
|
connect(aDrawCard, &QAction::triggered, this, &PhasesToolbar::drawCardRequested);
|
||||||
|
|
||||||
PhaseButton *untapButton = new PhaseButton("untap", this, aUntapAll);
|
PhaseButton *untapButton = new PhaseButton("untap", this, aUntapAll);
|
||||||
PhaseButton *upkeepButton = new PhaseButton("upkeep", this);
|
PhaseButton *upkeepButton = new PhaseButton("upkeep", this);
|
||||||
|
|
@ -144,7 +139,7 @@ PhasesToolbar::PhasesToolbar(QGraphicsItem *parent)
|
||||||
}
|
}
|
||||||
|
|
||||||
nextTurnButton = new PhaseButton("nextturn", this, nullptr, false);
|
nextTurnButton = new PhaseButton("nextturn", this, nullptr, false);
|
||||||
connect(nextTurnButton, &PhaseButton::clicked, this, &PhasesToolbar::actNextTurn);
|
connect(nextTurnButton, &PhaseButton::clicked, this, &PhasesToolbar::nextTurnRequested);
|
||||||
|
|
||||||
rearrangeButtons();
|
rearrangeButtons();
|
||||||
|
|
||||||
|
|
@ -265,31 +260,5 @@ void PhasesToolbar::phaseButtonClicked()
|
||||||
button->triggerDoubleClickAction();
|
button->triggerDoubleClickAction();
|
||||||
}
|
}
|
||||||
|
|
||||||
Command_SetActivePhase cmd;
|
emit setActivePhaseRequested(buttonList.indexOf(button));
|
||||||
cmd.set_phase(static_cast<google::protobuf::uint32>(buttonList.indexOf(button)));
|
|
||||||
|
|
||||||
emit sendGameCommand(cmd, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PhasesToolbar::actNextTurn()
|
|
||||||
{
|
|
||||||
emit sendGameCommand(Command_NextTurn(), -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PhasesToolbar::actUntapAll()
|
|
||||||
{
|
|
||||||
Command_SetCardAttr cmd;
|
|
||||||
cmd.set_zone(ZoneNames::TABLE);
|
|
||||||
cmd.set_attribute(AttrTapped);
|
|
||||||
cmd.set_attr_value("0");
|
|
||||||
|
|
||||||
emit sendGameCommand(cmd, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PhasesToolbar::actDrawCard()
|
|
||||||
{
|
|
||||||
Command_DrawCards cmd;
|
|
||||||
cmd.set_number(1);
|
|
||||||
|
|
||||||
emit sendGameCommand(cmd, -1);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,14 +17,6 @@
|
||||||
class QAction;
|
class QAction;
|
||||||
class QTimer;
|
class QTimer;
|
||||||
|
|
||||||
namespace google
|
|
||||||
{
|
|
||||||
namespace protobuf
|
|
||||||
{
|
|
||||||
class Message;
|
|
||||||
}
|
|
||||||
} // namespace google
|
|
||||||
|
|
||||||
class PhaseButton : public QObject, public QGraphicsItem
|
class PhaseButton : public QObject, public QGraphicsItem
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
@ -95,11 +87,16 @@ public slots:
|
||||||
void triggerPhaseAction(int phase);
|
void triggerPhaseAction(int phase);
|
||||||
private slots:
|
private slots:
|
||||||
void phaseButtonClicked();
|
void phaseButtonClicked();
|
||||||
void actNextTurn();
|
|
||||||
void actUntapAll();
|
|
||||||
void actDrawCard();
|
|
||||||
signals:
|
signals:
|
||||||
void sendGameCommand(const ::google::protobuf::Message &command, int playerId);
|
/** @name Intent signals
|
||||||
|
* The view reports what the user asked for; GameEventHandler builds the commands.
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
void setActivePhaseRequested(int phase);
|
||||||
|
void nextTurnRequested();
|
||||||
|
void untapAllRequested();
|
||||||
|
void drawCardRequested();
|
||||||
|
/** @} */
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) override;
|
void paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) override;
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,8 @@
|
||||||
#include "../../../client/settings/cache_settings.h"
|
#include "../../../client/settings/cache_settings.h"
|
||||||
#include "../../../client/settings/card_counter_settings.h"
|
#include "../../../client/settings/card_counter_settings.h"
|
||||||
#include "../../../client/settings/shortcuts_settings.h"
|
#include "../../../client/settings/shortcuts_settings.h"
|
||||||
#include "../../../game/abstract_game.h"
|
|
||||||
#include "../../../game/player/player_info.h"
|
|
||||||
#include "../../../game/player/player_manager.h"
|
|
||||||
#include "../../../game/zones/card_zone_logic.h"
|
|
||||||
#include "../../board/card_item.h"
|
#include "../../board/card_item.h"
|
||||||
#include "../../game/player/player_actions.h"
|
|
||||||
#include "../../game/player/player_logic.h"
|
|
||||||
#include "../../game/zones/view_zone_logic.h"
|
|
||||||
#include "../../game_scene.h"
|
#include "../../game_scene.h"
|
||||||
#include "../card_menu_action_type.h"
|
|
||||||
#include "../player_graphics_item.h"
|
#include "../player_graphics_item.h"
|
||||||
#include "libcockatrice/card/card_info.h"
|
#include "libcockatrice/card/card_info.h"
|
||||||
#include "libcockatrice/card/database/card_database_querier.h"
|
#include "libcockatrice/card/database/card_database_querier.h"
|
||||||
|
|
@ -35,6 +27,14 @@
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
#include <libcockatrice/card/database/card_database_manager.h>
|
#include <libcockatrice/card/database/card_database_manager.h>
|
||||||
#include <libcockatrice/card/relation/card_relation.h>
|
#include <libcockatrice/card/relation/card_relation.h>
|
||||||
|
#include <libcockatrice/game/abstract_game.h>
|
||||||
|
#include <libcockatrice/game/player/card_menu_action_type.h>
|
||||||
|
#include <libcockatrice/game/player/player_actions.h>
|
||||||
|
#include <libcockatrice/game/player/player_info.h>
|
||||||
|
#include <libcockatrice/game/player/player_logic.h>
|
||||||
|
#include <libcockatrice/game/player/player_manager.h>
|
||||||
|
#include <libcockatrice/game/zones/card_zone_logic.h>
|
||||||
|
#include <libcockatrice/game/zones/view_zone_logic.h>
|
||||||
#include <libcockatrice/utility/zone_names.h>
|
#include <libcockatrice/utility/zone_names.h>
|
||||||
#include <qnamespace.h>
|
#include <qnamespace.h>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
@ -67,7 +67,7 @@ static QAction *makeAction(QObject *parent, Slot &&slot, bool checkable = false,
|
||||||
return a;
|
return a;
|
||||||
}
|
}
|
||||||
|
|
||||||
CardMenu::CardMenu(PlayerGraphicsItem *_player, const CardItem *_card, bool _shortcutsActive)
|
CardMenu::CardMenu(PlayerGraphicsItem *_player, CardItem *_card, bool _shortcutsActive)
|
||||||
: player(_player), card(_card), shortcutsActive(_shortcutsActive)
|
: player(_player), card(_card), shortcutsActive(_shortcutsActive)
|
||||||
{
|
{
|
||||||
const QList<PlayerLogic *> &players = player->getLogic()->getGame()->getPlayerManager()->getPlayers().values();
|
const QList<PlayerLogic *> &players = player->getLogic()->getGame()->getPlayerManager()->getPlayers().values();
|
||||||
|
|
@ -86,7 +86,7 @@ CardMenu::CardMenu(PlayerGraphicsItem *_player, const CardItem *_card, bool _sho
|
||||||
auto *gameScene = player->getGameScene();
|
auto *gameScene = player->getGameScene();
|
||||||
|
|
||||||
// Single selection resolver used by all lambdas — called at trigger time
|
// Single selection resolver used by all lambdas — called at trigger time
|
||||||
auto sel = [gameScene]() { return gameScene->selectedCards(); };
|
auto sel = [gameScene]() { return gameScene->selectedCardStates(); };
|
||||||
|
|
||||||
// Unified dispatcher for card menu actions
|
// Unified dispatcher for card menu actions
|
||||||
auto invoke = [actions, sel](CardMenuActionType type) {
|
auto invoke = [actions, sel](CardMenuActionType type) {
|
||||||
|
|
@ -108,18 +108,18 @@ CardMenu::CardMenu(PlayerGraphicsItem *_player, const CardItem *_card, bool _sho
|
||||||
aHide = makeAction(this, [actions, sel]() { actions->actHide(sel()); });
|
aHide = makeAction(this, [actions, sel]() { actions->actHide(sel()); });
|
||||||
aReduceLifeByPower = makeAction(this, [actions, sel]() { actions->actReduceLifeByPower(sel()); });
|
aReduceLifeByPower = makeAction(this, [actions, sel]() { actions->actReduceLifeByPower(sel()); });
|
||||||
|
|
||||||
// Actions that use activeCard, not selection — direct connection
|
// Actions bound to the card this menu was opened for, not to the current selection
|
||||||
aAttach = new QAction(this);
|
aAttach = new QAction(this);
|
||||||
aDrawArrow = new QAction(this);
|
aDrawArrow = new QAction(this);
|
||||||
aSelectAll = new QAction(this);
|
aSelectAll = new QAction(this);
|
||||||
aSelectRow = new QAction(this);
|
aSelectRow = new QAction(this);
|
||||||
aSelectColumn = new QAction(this);
|
aSelectColumn = new QAction(this);
|
||||||
|
|
||||||
connect(aAttach, &QAction::triggered, actions, &PlayerActions::actAttach);
|
connect(aAttach, &QAction::triggered, this, &CardMenu::attachCard);
|
||||||
connect(aDrawArrow, &QAction::triggered, actions, &PlayerActions::actDrawArrow);
|
connect(aDrawArrow, &QAction::triggered, this, &CardMenu::drawArrow);
|
||||||
connect(aSelectAll, &QAction::triggered, actions, &PlayerActions::actSelectAll);
|
connect(aSelectAll, &QAction::triggered, this, [this, gameScene]() { gameScene->selectAllInZone(card); });
|
||||||
connect(aSelectRow, &QAction::triggered, actions, &PlayerActions::actSelectRow);
|
connect(aSelectRow, &QAction::triggered, this, [this, gameScene]() { gameScene->selectSameRow(card); });
|
||||||
connect(aSelectColumn, &QAction::triggered, actions, &PlayerActions::actSelectColumn);
|
connect(aSelectColumn, &QAction::triggered, this, [this, gameScene]() { gameScene->selectSameColumn(card); });
|
||||||
|
|
||||||
aRevealToAll = new QAction(this);
|
aRevealToAll = new QAction(this);
|
||||||
|
|
||||||
|
|
@ -337,7 +337,7 @@ void CardMenu::createHandOrCustomZoneMenu(bool canModifyCard)
|
||||||
initContextualPlayersMenu(revealMenu, aRevealToAll);
|
initContextualPlayersMenu(revealMenu, aRevealToAll);
|
||||||
|
|
||||||
connect(revealMenu, &QMenu::triggered, this, [this](QAction *action) {
|
connect(revealMenu, &QMenu::triggered, this, [this](QAction *action) {
|
||||||
player->getLogic()->getPlayerActions()->actReveal(player->getGameScene()->selectedCards(), action);
|
player->getLogic()->getPlayerActions()->actReveal(player->getGameScene()->selectedCardStates(), action);
|
||||||
});
|
});
|
||||||
|
|
||||||
addSeparator();
|
addSeparator();
|
||||||
|
|
@ -441,6 +441,7 @@ void CardMenu::addRelatedCardActions()
|
||||||
if (relatedCards.isEmpty()) {
|
if (relatedCards.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
auto *playerActions = player->getLogic()->getPlayerActions();
|
||||||
|
|
||||||
addSeparator();
|
addSeparator();
|
||||||
int index = 0;
|
int index = 0;
|
||||||
|
|
@ -484,9 +485,10 @@ void CardMenu::addRelatedCardActions()
|
||||||
}
|
}
|
||||||
|
|
||||||
auto *createRelated = new QAction(text, this);
|
auto *createRelated = new QAction(text, this);
|
||||||
createRelated->setData(QVariant(index++));
|
const int relatedIndex = index++;
|
||||||
connect(createRelated, &QAction::triggered, player->getLogic()->getPlayerActions(),
|
connect(createRelated, &QAction::triggered, this, [this, playerActions, relatedIndex] {
|
||||||
&PlayerActions::actCreateRelatedCard);
|
playerActions->actCreateRelatedCard(card->getState(), relatedIndex);
|
||||||
|
});
|
||||||
addAction(createRelated);
|
addAction(createRelated);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -495,12 +497,26 @@ void CardMenu::addRelatedCardActions()
|
||||||
createRelatedCards->setShortcuts(
|
createRelatedCards->setShortcuts(
|
||||||
SettingsCache::instance().shortcuts().getShortcut("Player/aCreateRelatedTokens"));
|
SettingsCache::instance().shortcuts().getShortcut("Player/aCreateRelatedTokens"));
|
||||||
}
|
}
|
||||||
connect(createRelatedCards, &QAction::triggered, player->getLogic()->getPlayerActions(),
|
connect(createRelatedCards, &QAction::triggered, this,
|
||||||
&PlayerActions::actCreateAllRelatedCards);
|
[this, playerActions] { playerActions->actCreateAllRelatedCards(card->getState()); });
|
||||||
addAction(createRelatedCards);
|
addAction(createRelatedCards);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CardMenu::attachCard()
|
||||||
|
{
|
||||||
|
if (card) {
|
||||||
|
card->drawAttachArrow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CardMenu::drawArrow()
|
||||||
|
{
|
||||||
|
if (card) {
|
||||||
|
card->drawArrow(Qt::red);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void CardMenu::retranslateUi()
|
void CardMenu::retranslateUi()
|
||||||
{
|
{
|
||||||
aSelectAll->setText(tr("&Select All"));
|
aSelectAll->setText(tr("&Select All"));
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ signals:
|
||||||
void cardInfoRequested(const CardRef &cardRef);
|
void cardInfoRequested(const CardRef &cardRef);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit CardMenu(PlayerGraphicsItem *player, const CardItem *card, bool shortcutsActive);
|
explicit CardMenu(PlayerGraphicsItem *player, CardItem *card, bool shortcutsActive);
|
||||||
void removePlayer(PlayerLogic *playerToRemove);
|
void removePlayer(PlayerLogic *playerToRemove);
|
||||||
void createTableMenu(bool canModifyCard);
|
void createTableMenu(bool canModifyCard);
|
||||||
void createStackMenu(bool canModifyCard);
|
void createStackMenu(bool canModifyCard);
|
||||||
|
|
@ -53,11 +53,13 @@ public:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
PlayerGraphicsItem *player;
|
PlayerGraphicsItem *player;
|
||||||
const CardItem *card;
|
CardItem *card;
|
||||||
QList<QPair<QString, int>> playersInfo;
|
QList<QPair<QString, int>> playersInfo;
|
||||||
bool shortcutsActive;
|
bool shortcutsActive;
|
||||||
|
|
||||||
void addRelatedCardActions();
|
void addRelatedCardActions();
|
||||||
|
void attachCard();
|
||||||
|
void drawArrow();
|
||||||
void retranslateUi();
|
void retranslateUi();
|
||||||
void initContextualPlayersMenu(QMenu *menu, QAction *allPlayersAction);
|
void initContextualPlayersMenu(QMenu *menu, QAction *allPlayersAction);
|
||||||
void setShortcutsActive();
|
void setShortcutsActive();
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
#include "custom_zone_menu.h"
|
#include "custom_zone_menu.h"
|
||||||
|
|
||||||
#include "../../../game/player/player_info.h"
|
|
||||||
#include "../../game/player/player_logic.h"
|
|
||||||
#include "../../game_scene.h"
|
#include "../../game_scene.h"
|
||||||
#include "../player_graphics_item.h"
|
#include "../player_graphics_item.h"
|
||||||
|
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
|
#include <libcockatrice/game/player/player_info.h>
|
||||||
|
#include <libcockatrice/game/player/player_logic.h>
|
||||||
|
|
||||||
CustomZoneMenu::CustomZoneMenu(PlayerGraphicsItem *_player) : player(_player)
|
CustomZoneMenu::CustomZoneMenu(PlayerGraphicsItem *_player) : player(_player)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,7 @@
|
||||||
|
|
||||||
#include "../../../client/settings/cache_settings.h"
|
#include "../../../client/settings/cache_settings.h"
|
||||||
#include "../../../client/settings/shortcuts_settings.h"
|
#include "../../../client/settings/shortcuts_settings.h"
|
||||||
#include "../../../game/player/player_info.h"
|
|
||||||
#include "../../../game/player/player_manager.h"
|
|
||||||
#include "../../../game/zones/pile_zone_logic.h"
|
|
||||||
#include "../../../interface/widgets/menus/tearoff_menu.h"
|
#include "../../../interface/widgets/menus/tearoff_menu.h"
|
||||||
#include "../../game/abstract_game.h"
|
|
||||||
#include "../../game/player/player_actions.h"
|
|
||||||
#include "../../game/player/player_logic.h"
|
|
||||||
#include "../player_graphics_item.h"
|
#include "../player_graphics_item.h"
|
||||||
|
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
|
|
@ -19,6 +13,12 @@
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
|
#include <libcockatrice/game/abstract_game.h>
|
||||||
|
#include <libcockatrice/game/player/player_actions.h>
|
||||||
|
#include <libcockatrice/game/player/player_info.h>
|
||||||
|
#include <libcockatrice/game/player/player_logic.h>
|
||||||
|
#include <libcockatrice/game/player/player_manager.h>
|
||||||
|
#include <libcockatrice/game/zones/pile_zone_logic.h>
|
||||||
#include <libcockatrice/utility/zone_names.h>
|
#include <libcockatrice/utility/zone_names.h>
|
||||||
|
|
||||||
class QWidget;
|
class QWidget;
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,7 @@
|
||||||
|
|
||||||
#include "../../../client/settings/cache_settings.h"
|
#include "../../../client/settings/cache_settings.h"
|
||||||
#include "../../../client/settings/shortcuts_settings.h"
|
#include "../../../client/settings/shortcuts_settings.h"
|
||||||
#include "../../../game/board/card_list.h"
|
|
||||||
#include "../../../game/player/player_info.h"
|
|
||||||
#include "../../../game/player/player_manager.h"
|
|
||||||
#include "../../../game/zones/hand_zone_logic.h"
|
|
||||||
#include "../../../interface/widgets/menus/tearoff_menu.h"
|
#include "../../../interface/widgets/menus/tearoff_menu.h"
|
||||||
#include "../../game/abstract_game.h"
|
|
||||||
#include "../../game/player/player_actions.h"
|
|
||||||
#include "../../game/player/player_logic.h"
|
|
||||||
#include "../player_graphics_item.h"
|
#include "../player_graphics_item.h"
|
||||||
|
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
|
|
@ -20,6 +13,13 @@
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
|
#include <libcockatrice/game/abstract_game.h>
|
||||||
|
#include <libcockatrice/game/board/card_list.h>
|
||||||
|
#include <libcockatrice/game/player/player_actions.h>
|
||||||
|
#include <libcockatrice/game/player/player_info.h>
|
||||||
|
#include <libcockatrice/game/player/player_logic.h>
|
||||||
|
#include <libcockatrice/game/player/player_manager.h>
|
||||||
|
#include <libcockatrice/game/zones/hand_zone_logic.h>
|
||||||
#include <libcockatrice/utility/zone_names.h>
|
#include <libcockatrice/utility/zone_names.h>
|
||||||
|
|
||||||
class QWidget;
|
class QWidget;
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,7 @@
|
||||||
|
|
||||||
#include "../../../client/settings/cache_settings.h"
|
#include "../../../client/settings/cache_settings.h"
|
||||||
#include "../../../client/settings/shortcuts_settings.h"
|
#include "../../../client/settings/shortcuts_settings.h"
|
||||||
#include "../../../game/board/card_list.h"
|
|
||||||
#include "../../../game/player/player_info.h"
|
|
||||||
#include "../../../game/player/player_manager.h"
|
|
||||||
#include "../../../game/zones/pile_zone_logic.h"
|
|
||||||
#include "../../../interface/widgets/menus/tearoff_menu.h"
|
#include "../../../interface/widgets/menus/tearoff_menu.h"
|
||||||
#include "../../game/abstract_game.h"
|
|
||||||
#include "../../game/player/player_actions.h"
|
|
||||||
#include "../../game/player/player_logic.h"
|
|
||||||
#include "../player_graphics_item.h"
|
#include "../player_graphics_item.h"
|
||||||
|
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
|
|
@ -23,6 +16,13 @@
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
|
#include <libcockatrice/game/abstract_game.h>
|
||||||
|
#include <libcockatrice/game/board/card_list.h>
|
||||||
|
#include <libcockatrice/game/player/player_actions.h>
|
||||||
|
#include <libcockatrice/game/player/player_info.h>
|
||||||
|
#include <libcockatrice/game/player/player_logic.h>
|
||||||
|
#include <libcockatrice/game/player/player_manager.h>
|
||||||
|
#include <libcockatrice/game/zones/pile_zone_logic.h>
|
||||||
|
|
||||||
class QWidget;
|
class QWidget;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@
|
||||||
|
|
||||||
#include "../../../client/settings/cache_settings.h"
|
#include "../../../client/settings/cache_settings.h"
|
||||||
#include "../../../client/settings/shortcuts_settings.h"
|
#include "../../../client/settings/shortcuts_settings.h"
|
||||||
#include "../../game/player/player_actions.h"
|
|
||||||
#include "../../game/player/player_logic.h"
|
|
||||||
#include "../../game_scene.h"
|
#include "../../game_scene.h"
|
||||||
#include "../card_menu_action_type.h"
|
|
||||||
#include "../player_graphics_item.h"
|
#include "../player_graphics_item.h"
|
||||||
|
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
|
#include <libcockatrice/game/player/card_menu_action_type.h>
|
||||||
|
#include <libcockatrice/game/player/player_actions.h>
|
||||||
|
#include <libcockatrice/game/player/player_logic.h>
|
||||||
|
|
||||||
MoveMenu::MoveMenu(PlayerGraphicsItem *player) : QMenu(tr("Move to"))
|
MoveMenu::MoveMenu(PlayerGraphicsItem *player) : QMenu(tr("Move to"))
|
||||||
{
|
{
|
||||||
|
|
@ -30,7 +30,7 @@ MoveMenu::MoveMenu(PlayerGraphicsItem *player) : QMenu(tr("Move to"))
|
||||||
|
|
||||||
auto invoke = [player](CardMenuActionType type) {
|
auto invoke = [player](CardMenuActionType type) {
|
||||||
return [type, player]() {
|
return [type, player]() {
|
||||||
player->getLogic()->getPlayerActions()->cardMenuAction(player->getGameScene()->selectedCards(), type);
|
player->getLogic()->getPlayerActions()->cardMenuAction(player->getGameScene()->selectedCardStates(), type);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,6 @@
|
||||||
|
|
||||||
#include "../../../client/settings/cache_settings.h"
|
#include "../../../client/settings/cache_settings.h"
|
||||||
#include "../../../client/settings/shortcuts_settings.h"
|
#include "../../../client/settings/shortcuts_settings.h"
|
||||||
#include "../../../game/abstract_game.h"
|
|
||||||
#include "../../../game/player/player_info.h"
|
|
||||||
#include "../../../game/player/player_logic.h"
|
|
||||||
#include "../../../game/player/player_manager.h"
|
|
||||||
#include "../../../game_graphics/zones/hand_zone.h"
|
#include "../../../game_graphics/zones/hand_zone.h"
|
||||||
#include "../../../game_graphics/zones/pile_zone.h"
|
#include "../../../game_graphics/zones/pile_zone.h"
|
||||||
#include "../../../game_graphics/zones/table_zone.h"
|
#include "../../../game_graphics/zones/table_zone.h"
|
||||||
|
|
@ -26,6 +22,10 @@
|
||||||
|
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
#include <libcockatrice/game/abstract_game.h>
|
||||||
|
#include <libcockatrice/game/player/player_info.h>
|
||||||
|
#include <libcockatrice/game/player/player_logic.h>
|
||||||
|
#include <libcockatrice/game/player/player_manager.h>
|
||||||
|
|
||||||
PlayerMenu::PlayerMenu(PlayerGraphicsItem *_player) : QObject(_player), player(_player)
|
PlayerMenu::PlayerMenu(PlayerGraphicsItem *_player) : QObject(_player), player(_player)
|
||||||
{
|
{
|
||||||
|
|
@ -87,22 +87,24 @@ void PlayerMenu::setMenusForGraphicItems()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QMenu *PlayerMenu::updateCardMenu(const CardItem *card)
|
QMenu *PlayerMenu::updateCardMenu(CardState *card)
|
||||||
{
|
{
|
||||||
if (!card) {
|
if (!card) {
|
||||||
emit cardMenuUpdated(nullptr);
|
emit cardMenuUpdated(nullptr);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto *cardItem = qobject_cast<CardItem *>(card->parent());
|
||||||
|
|
||||||
// If is spectator (as spectators don't need card menus), return
|
// If is spectator (as spectators don't need card menus), return
|
||||||
// only update the menu if the card is actually selected
|
// only update the menu if the card is actually selected
|
||||||
if ((player->getLogic()->getGame()->getPlayerManager()->isSpectator() &&
|
if ((player->getLogic()->getGame()->getPlayerManager()->isSpectator() &&
|
||||||
!player->getLogic()->getGame()->getPlayerManager()->isJudge()) ||
|
!player->getLogic()->getGame()->getPlayerManager()->isJudge()) ||
|
||||||
player->getLogic()->getGame()->getActiveCard() != card) {
|
cardItem == nullptr || player->getGameScene()->getActiveCard() != cardItem) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
CardMenu *menu = new CardMenu(player, card, shortcutsActive);
|
CardMenu *menu = new CardMenu(player, cardItem, shortcutsActive);
|
||||||
connect(menu, &CardMenu::cardInfoRequested, this, &PlayerMenu::cardInfoRequested);
|
connect(menu, &CardMenu::cardInfoRequested, this, &PlayerMenu::cardInfoRequested);
|
||||||
emit cardMenuUpdated(menu);
|
emit cardMenuUpdated(menu);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
class CardItem;
|
class CardState;
|
||||||
class CardMenu;
|
class CardMenu;
|
||||||
class PlayerGraphicsItem;
|
class PlayerGraphicsItem;
|
||||||
class AbstractPlayerComponent;
|
class AbstractPlayerComponent;
|
||||||
|
|
@ -43,7 +43,7 @@ signals:
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void setMenusForGraphicItems();
|
void setMenusForGraphicItems();
|
||||||
QMenu *updateCardMenu(const CardItem *card);
|
QMenu *updateCardMenu(CardState *card);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void refreshShortcuts();
|
void refreshShortcuts();
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,12 @@
|
||||||
|
|
||||||
#include "../../../client/settings/cache_settings.h"
|
#include "../../../client/settings/cache_settings.h"
|
||||||
#include "../../../client/settings/shortcuts_settings.h"
|
#include "../../../client/settings/shortcuts_settings.h"
|
||||||
#include "../../game/player/player_actions.h"
|
|
||||||
#include "../../game/player/player_logic.h"
|
|
||||||
#include "../../game_scene.h"
|
#include "../../game_scene.h"
|
||||||
#include "../player_graphics_item.h"
|
#include "../player_graphics_item.h"
|
||||||
|
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
|
#include <libcockatrice/game/player/player_actions.h>
|
||||||
|
#include <libcockatrice/game/player/player_logic.h>
|
||||||
|
|
||||||
PtMenu::PtMenu(PlayerGraphicsItem *player) : QMenu(tr("Power / toughness"))
|
PtMenu::PtMenu(PlayerGraphicsItem *player) : QMenu(tr("Power / toughness"))
|
||||||
{
|
{
|
||||||
|
|
@ -15,34 +15,35 @@ PtMenu::PtMenu(PlayerGraphicsItem *player) : QMenu(tr("Power / toughness"))
|
||||||
|
|
||||||
aIncP = new QAction(this);
|
aIncP = new QAction(this);
|
||||||
connect(aIncP, &QAction::triggered, playerActions,
|
connect(aIncP, &QAction::triggered, playerActions,
|
||||||
[player, playerActions] { playerActions->actIncP(player->getGameScene()->selectedCards()); });
|
[player, playerActions] { playerActions->actIncP(player->getGameScene()->selectedCardStates()); });
|
||||||
aDecP = new QAction(this);
|
aDecP = new QAction(this);
|
||||||
connect(aDecP, &QAction::triggered, playerActions,
|
connect(aDecP, &QAction::triggered, playerActions,
|
||||||
[player, playerActions] { playerActions->actDecP(player->getGameScene()->selectedCards()); });
|
[player, playerActions] { playerActions->actDecP(player->getGameScene()->selectedCardStates()); });
|
||||||
aIncT = new QAction(this);
|
aIncT = new QAction(this);
|
||||||
connect(aIncT, &QAction::triggered, playerActions,
|
connect(aIncT, &QAction::triggered, playerActions,
|
||||||
[player, playerActions] { playerActions->actIncT(player->getGameScene()->selectedCards()); });
|
[player, playerActions] { playerActions->actIncT(player->getGameScene()->selectedCardStates()); });
|
||||||
aDecT = new QAction(this);
|
aDecT = new QAction(this);
|
||||||
connect(aDecT, &QAction::triggered, playerActions,
|
connect(aDecT, &QAction::triggered, playerActions,
|
||||||
[player, playerActions] { playerActions->actDecT(player->getGameScene()->selectedCards()); });
|
[player, playerActions] { playerActions->actDecT(player->getGameScene()->selectedCardStates()); });
|
||||||
aIncPT = new QAction(this);
|
aIncPT = new QAction(this);
|
||||||
connect(aIncPT, &QAction::triggered, playerActions,
|
connect(aIncPT, &QAction::triggered, playerActions,
|
||||||
[player, playerActions] { playerActions->actIncPT(player->getGameScene()->selectedCards()); });
|
[player, playerActions] { playerActions->actIncPT(player->getGameScene()->selectedCardStates()); });
|
||||||
aDecPT = new QAction(this);
|
aDecPT = new QAction(this);
|
||||||
connect(aDecPT, &QAction::triggered, playerActions,
|
connect(aDecPT, &QAction::triggered, playerActions,
|
||||||
[player, playerActions] { playerActions->actDecPT(player->getGameScene()->selectedCards()); });
|
[player, playerActions] { playerActions->actDecPT(player->getGameScene()->selectedCardStates()); });
|
||||||
aFlowP = new QAction(this);
|
aFlowP = new QAction(this);
|
||||||
connect(aFlowP, &QAction::triggered, playerActions,
|
connect(aFlowP, &QAction::triggered, playerActions,
|
||||||
[player, playerActions] { playerActions->actFlowP(player->getGameScene()->selectedCards()); });
|
[player, playerActions] { playerActions->actFlowP(player->getGameScene()->selectedCardStates()); });
|
||||||
aFlowT = new QAction(this);
|
aFlowT = new QAction(this);
|
||||||
connect(aFlowT, &QAction::triggered, playerActions,
|
connect(aFlowT, &QAction::triggered, playerActions,
|
||||||
[player, playerActions] { playerActions->actFlowT(player->getGameScene()->selectedCards()); });
|
[player, playerActions] { playerActions->actFlowT(player->getGameScene()->selectedCardStates()); });
|
||||||
aSetPT = new QAction(this);
|
aSetPT = new QAction(this);
|
||||||
connect(aSetPT, &QAction::triggered, playerActions,
|
connect(aSetPT, &QAction::triggered, playerActions, [player, playerActions] {
|
||||||
[player, playerActions] { playerActions->actRequestSetPTDialog(player->getGameScene()->selectedCards()); });
|
playerActions->actRequestSetPTDialog(player->getGameScene()->selectedCardStates());
|
||||||
|
});
|
||||||
aResetPT = new QAction(this);
|
aResetPT = new QAction(this);
|
||||||
connect(aResetPT, &QAction::triggered, playerActions,
|
connect(aResetPT, &QAction::triggered, playerActions,
|
||||||
[player, playerActions] { playerActions->actResetPT(player->getGameScene()->selectedCards()); });
|
[player, playerActions] { playerActions->actResetPT(player->getGameScene()->selectedCardStates()); });
|
||||||
|
|
||||||
addAction(aIncP);
|
addAction(aIncP);
|
||||||
addAction(aDecP);
|
addAction(aDecP);
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
#include "rfg_menu.h"
|
#include "rfg_menu.h"
|
||||||
|
|
||||||
#include "../../../game/player/player_info.h"
|
|
||||||
#include "../../../game/zones/pile_zone_logic.h"
|
|
||||||
#include "../../../interface/widgets/menus/tearoff_menu.h"
|
#include "../../../interface/widgets/menus/tearoff_menu.h"
|
||||||
#include "../../game/player/player_actions.h"
|
|
||||||
#include "../../game/player/player_logic.h"
|
|
||||||
#include "../player_graphics_item.h"
|
#include "../player_graphics_item.h"
|
||||||
|
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
|
|
@ -12,6 +8,10 @@
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
|
#include <libcockatrice/game/player/player_actions.h>
|
||||||
|
#include <libcockatrice/game/player/player_info.h>
|
||||||
|
#include <libcockatrice/game/player/player_logic.h>
|
||||||
|
#include <libcockatrice/game/zones/pile_zone_logic.h>
|
||||||
#include <libcockatrice/utility/zone_names.h>
|
#include <libcockatrice/utility/zone_names.h>
|
||||||
|
|
||||||
class QWidget;
|
class QWidget;
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
#include "say_menu.h"
|
#include "say_menu.h"
|
||||||
|
|
||||||
#include "../../../client/settings/cache_settings.h"
|
#include "../../../client/settings/cache_settings.h"
|
||||||
#include "../../game/player/player_actions.h"
|
|
||||||
#include "../../game/player/player_logic.h"
|
|
||||||
#include "../player_graphics_item.h"
|
#include "../player_graphics_item.h"
|
||||||
|
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
#include <QKeySequence>
|
#include <QKeySequence>
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
#include <libcockatrice/game/player/player_actions.h>
|
||||||
|
#include <libcockatrice/game/player/player_logic.h>
|
||||||
#include <libcockatrice/settings/message_settings.h>
|
#include <libcockatrice/settings/message_settings.h>
|
||||||
|
|
||||||
SayMenu::SayMenu(PlayerGraphicsItem *_player) : player(_player)
|
SayMenu::SayMenu(PlayerGraphicsItem *_player) : player(_player)
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@
|
||||||
|
|
||||||
#include "../../../client/settings/cache_settings.h"
|
#include "../../../client/settings/cache_settings.h"
|
||||||
#include "../../../client/settings/shortcuts_settings.h"
|
#include "../../../client/settings/shortcuts_settings.h"
|
||||||
#include "../../../game/player/player_info.h"
|
|
||||||
#include "../../game/player/player_actions.h"
|
|
||||||
#include "../../game/player/player_logic.h"
|
|
||||||
#include "../player_graphics_item.h"
|
#include "../player_graphics_item.h"
|
||||||
|
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
#include <QKeySequence>
|
#include <QKeySequence>
|
||||||
|
#include <libcockatrice/game/player/player_actions.h>
|
||||||
|
#include <libcockatrice/game/player/player_info.h>
|
||||||
|
#include <libcockatrice/game/player/player_logic.h>
|
||||||
|
|
||||||
SideboardMenu::SideboardMenu(PlayerGraphicsItem *player, QMenu *playerMenu) : QMenu(playerMenu)
|
SideboardMenu::SideboardMenu(PlayerGraphicsItem *player, QMenu *playerMenu) : QMenu(playerMenu)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,6 @@
|
||||||
|
|
||||||
#include "../../../client/settings/cache_settings.h"
|
#include "../../../client/settings/cache_settings.h"
|
||||||
#include "../../../client/settings/shortcuts_settings.h"
|
#include "../../../client/settings/shortcuts_settings.h"
|
||||||
#include "../../../game/player/player_info.h"
|
|
||||||
#include "../../game/player/player_actions.h"
|
|
||||||
#include "../../game/player/player_logic.h"
|
|
||||||
#include "../../game_scene.h"
|
#include "../../game_scene.h"
|
||||||
#include "../player_graphics_item.h"
|
#include "../player_graphics_item.h"
|
||||||
#include "libcockatrice/card/card_info.h"
|
#include "libcockatrice/card/card_info.h"
|
||||||
|
|
@ -14,6 +11,9 @@
|
||||||
#include <QSet>
|
#include <QSet>
|
||||||
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
|
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
|
||||||
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
|
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
|
||||||
|
#include <libcockatrice/game/player/player_actions.h>
|
||||||
|
#include <libcockatrice/game/player/player_info.h>
|
||||||
|
#include <libcockatrice/game/player/player_logic.h>
|
||||||
|
|
||||||
UtilityMenu::UtilityMenu(PlayerGraphicsItem *_player, QMenu *playerMenu) : QMenu(playerMenu), player(_player)
|
UtilityMenu::UtilityMenu(PlayerGraphicsItem *_player, QMenu *playerMenu) : QMenu(playerMenu), player(_player)
|
||||||
{
|
{
|
||||||
|
|
@ -44,7 +44,7 @@ UtilityMenu::UtilityMenu(PlayerGraphicsItem *_player, QMenu *playerMenu) : QMenu
|
||||||
aIncrementAllCardCounters = new QAction(this);
|
aIncrementAllCardCounters = new QAction(this);
|
||||||
connect(aIncrementAllCardCounters, &QAction::triggered, playerActions, [this]() {
|
connect(aIncrementAllCardCounters, &QAction::triggered, playerActions, [this]() {
|
||||||
player->getLogic()->getPlayerActions()->actIncrementAllCardCounters(
|
player->getLogic()->getPlayerActions()->actIncrementAllCardCounters(
|
||||||
player->getGameScene()->selectedCards());
|
player->getGameScene()->selectedCardStates());
|
||||||
});
|
});
|
||||||
|
|
||||||
createPredefinedTokenMenu = new QMenu(QString());
|
createPredefinedTokenMenu = new QMenu(QString());
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,10 @@
|
||||||
|
|
||||||
#include "../../client/settings/cache_settings.h"
|
#include "../../client/settings/cache_settings.h"
|
||||||
#include "../../client/settings/card_counter_settings.h"
|
#include "../../client/settings/card_counter_settings.h"
|
||||||
#include "../../game/player/player_actions.h"
|
|
||||||
#include "../../game/player/player_logic.h"
|
|
||||||
#include "../../interface/widgets/utility/get_text_with_max.h"
|
#include "../../interface/widgets/utility/get_text_with_max.h"
|
||||||
#include "../board/abstract_counter.h"
|
#include "../board/abstract_counter.h"
|
||||||
#include "../board/card_item.h"
|
#include "../board/card_item.h"
|
||||||
|
#include "../dialogs/dlg_annotation.h"
|
||||||
#include "../dialogs/dlg_create_token.h"
|
#include "../dialogs/dlg_create_token.h"
|
||||||
#include "../dialogs/dlg_move_top_cards_until.h"
|
#include "../dialogs/dlg_move_top_cards_until.h"
|
||||||
#include "../dialogs/dlg_roll_dice.h"
|
#include "../dialogs/dlg_roll_dice.h"
|
||||||
|
|
@ -17,6 +16,8 @@
|
||||||
#include <QInputDialog>
|
#include <QInputDialog>
|
||||||
#include <QLineEdit>
|
#include <QLineEdit>
|
||||||
#include <libcockatrice/card/relation/card_relation.h>
|
#include <libcockatrice/card/relation/card_relation.h>
|
||||||
|
#include <libcockatrice/game/player/player_actions.h>
|
||||||
|
#include <libcockatrice/game/player/player_logic.h>
|
||||||
#include <libcockatrice/utility/string_limits.h>
|
#include <libcockatrice/utility/string_limits.h>
|
||||||
|
|
||||||
PlayerDialogs::PlayerDialogs(PlayerGraphicsItem *_player, PlayerActions *_playerActions)
|
PlayerDialogs::PlayerDialogs(PlayerGraphicsItem *_player, PlayerActions *_playerActions)
|
||||||
|
|
@ -34,7 +35,7 @@ PlayerDialogs::PlayerDialogs(PlayerGraphicsItem *_player, PlayerActions *_player
|
||||||
&PlayerDialogs::onShuffleBottomDialogRequested);
|
&PlayerDialogs::onShuffleBottomDialogRequested);
|
||||||
|
|
||||||
connect(playerActions, &PlayerActions::requestMulliganDialog, this, &PlayerDialogs::onMulliganDialogRequested);
|
connect(playerActions, &PlayerActions::requestMulliganDialog, this, &PlayerDialogs::onMulliganDialogRequested);
|
||||||
|
connect(playerActions, &PlayerActions::startingHandSizeChosen, this, &PlayerDialogs::onStartingHandSizeChosen);
|
||||||
connect(playerActions, &PlayerActions::requestDrawCardsDialog, this, &PlayerDialogs::onDrawCardsDialogRequested);
|
connect(playerActions, &PlayerActions::requestDrawCardsDialog, this, &PlayerDialogs::onDrawCardsDialogRequested);
|
||||||
|
|
||||||
connect(playerActions, &PlayerActions::requestMoveTopCardsToDialog, this,
|
connect(playerActions, &PlayerActions::requestMoveTopCardsToDialog, this,
|
||||||
|
|
@ -126,6 +127,11 @@ void PlayerDialogs::onMulliganDialogRequested(int startSize, int handSize, int d
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PlayerDialogs::onStartingHandSizeChosen(int number)
|
||||||
|
{
|
||||||
|
SettingsCache::instance().userInterface().setStartingHandSize(number);
|
||||||
|
}
|
||||||
|
|
||||||
void PlayerDialogs::onDrawCardsDialogRequested(int defaultNumberTopCards, int deckSize)
|
void PlayerDialogs::onDrawCardsDialogRequested(int defaultNumberTopCards, int deckSize)
|
||||||
{
|
{
|
||||||
bool ok;
|
bool ok;
|
||||||
|
|
@ -196,7 +202,7 @@ void PlayerDialogs::onRollDieDialogRequested()
|
||||||
playerActions->actRollDie(dlg.getDieSideCount(), dlg.getDiceToRollCount());
|
playerActions->actRollDie(dlg.getDieSideCount(), dlg.getDiceToRollCount());
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlayerDialogs::onCreateRelatedFromRelationDialogRequested(const CardItem *sourceCard,
|
void PlayerDialogs::onCreateRelatedFromRelationDialogRequested(const CardState *sourceCard,
|
||||||
const CardRelation *cardRelation)
|
const CardRelation *cardRelation)
|
||||||
{
|
{
|
||||||
if (sourceCard == nullptr || cardRelation == nullptr) {
|
if (sourceCard == nullptr || cardRelation == nullptr) {
|
||||||
|
|
@ -251,14 +257,14 @@ void PlayerDialogs::onMoveCardXCardsFromTopDialogRequested(int defaultNumberTopC
|
||||||
number -= 1; // indexes start at 0
|
number -= 1; // indexes start at 0
|
||||||
|
|
||||||
if (ok) {
|
if (ok) {
|
||||||
playerActions->actMoveCardXCardsFromTop(player->getGameScene()->selectedCards(), number);
|
playerActions->actMoveCardXCardsFromTop(player->getGameScene()->selectedCardStates(), number);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlayerDialogs::onSetPTDialogRequested(const QString &oldPT)
|
void PlayerDialogs::onSetPTDialogRequested(const QString &oldPT)
|
||||||
{
|
{
|
||||||
bool ok;
|
bool ok;
|
||||||
auto cards = player->getGameScene()->selectedCards();
|
auto cards = player->getGameScene()->selectedCardStates();
|
||||||
emit requestDialogSemaphore(true);
|
emit requestDialogSemaphore(true);
|
||||||
QString pt = getTextWithMax(dialogParent(), tr("Change power/toughness"), tr("Change stats to:"), QLineEdit::Normal,
|
QString pt = getTextWithMax(dialogParent(), tr("Change power/toughness"), tr("Change stats to:"), QLineEdit::Normal,
|
||||||
oldPT, &ok);
|
oldPT, &ok);
|
||||||
|
|
@ -273,7 +279,7 @@ void PlayerDialogs::onSetPTDialogRequested(const QString &oldPT)
|
||||||
|
|
||||||
void PlayerDialogs::onSetAnnotationDialogRequested(const QString &oldAnnotation)
|
void PlayerDialogs::onSetAnnotationDialogRequested(const QString &oldAnnotation)
|
||||||
{
|
{
|
||||||
auto cards = player->getGameScene()->selectedCards();
|
auto cards = player->getGameScene()->selectedCardStates();
|
||||||
emit requestDialogSemaphore(true);
|
emit requestDialogSemaphore(true);
|
||||||
AnnotationDialog *dialog = new AnnotationDialog(dialogParent());
|
AnnotationDialog *dialog = new AnnotationDialog(dialogParent());
|
||||||
dialog->setOptions(QInputDialog::UsePlainTextEditForTextInput);
|
dialog->setOptions(QInputDialog::UsePlainTextEditForTextInput);
|
||||||
|
|
@ -291,7 +297,7 @@ void PlayerDialogs::onSetAnnotationDialogRequested(const QString &oldAnnotation)
|
||||||
|
|
||||||
void PlayerDialogs::onSetCardCounterDialogRequested(int counterId, const QString &oldValueForDlg)
|
void PlayerDialogs::onSetCardCounterDialogRequested(int counterId, const QString &oldValueForDlg)
|
||||||
{
|
{
|
||||||
auto cards = player->getGameScene()->selectedCards();
|
auto cards = player->getGameScene()->selectedCardStates();
|
||||||
emit requestDialogSemaphore(true);
|
emit requestDialogSemaphore(true);
|
||||||
|
|
||||||
auto &cardCounterSettings = SettingsCache::instance().cardCounters();
|
auto &cardCounterSettings = SettingsCache::instance().cardCounters();
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
class CardItem;
|
class CardItem;
|
||||||
class CardRelation;
|
class CardRelation;
|
||||||
|
class CardState;
|
||||||
class PlayerActions;
|
class PlayerActions;
|
||||||
class QWidget;
|
class QWidget;
|
||||||
struct MoveTopCardsUntilOptions;
|
struct MoveTopCardsUntilOptions;
|
||||||
|
|
@ -52,12 +53,13 @@ public slots:
|
||||||
bool faceDown);
|
bool faceDown);
|
||||||
void onDrawBottomCardsDialogRequested(int defaultNumberBottomCards, int maxCards);
|
void onDrawBottomCardsDialogRequested(int defaultNumberBottomCards, int maxCards);
|
||||||
void onRollDieDialogRequested();
|
void onRollDieDialogRequested();
|
||||||
void onCreateRelatedFromRelationDialogRequested(const CardItem *sourceCard, const CardRelation *cardRelation);
|
void onCreateRelatedFromRelationDialogRequested(const CardState *sourceCard, const CardRelation *cardRelation);
|
||||||
void onCreateTokenDialogRequested(const QStringList &predefinedTokens);
|
void onCreateTokenDialogRequested(const QStringList &predefinedTokens);
|
||||||
void onMoveCardXCardsFromTopDialogRequested(int defaultNumberTopCardsToPlaceBelow, int deckSize);
|
void onMoveCardXCardsFromTopDialogRequested(int defaultNumberTopCardsToPlaceBelow, int deckSize);
|
||||||
void onSetPTDialogRequested(const QString &oldPT);
|
void onSetPTDialogRequested(const QString &oldPT);
|
||||||
void onSetAnnotationDialogRequested(const QString &oldAnnotation);
|
void onSetAnnotationDialogRequested(const QString &oldAnnotation);
|
||||||
void onSetCardCounterDialogRequested(int counterId, const QString &oldValueForDlg);
|
void onSetCardCounterDialogRequested(int counterId, const QString &oldValueForDlg);
|
||||||
|
void onStartingHandSizeChosen(int number);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
PlayerGraphicsItem *player;
|
PlayerGraphicsItem *player;
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,6 @@
|
||||||
#include "player_graphics_item.h"
|
#include "player_graphics_item.h"
|
||||||
|
|
||||||
#include "../../client/settings/cache_settings.h"
|
#include "../../client/settings/cache_settings.h"
|
||||||
#include "../../game/board/counter_state.h"
|
|
||||||
#include "../../game/player/player_actions.h"
|
|
||||||
#include "../../game/player/player_info.h"
|
|
||||||
#include "../../game/player/player_logic.h"
|
|
||||||
#include "../../game/zones/card_zone_logic.h"
|
|
||||||
#include "../../game/zones/hand_zone_logic.h"
|
|
||||||
#include "../../game/zones/pile_zone_logic.h"
|
|
||||||
#include "../../game/zones/stack_zone_logic.h"
|
|
||||||
#include "../../game/zones/table_zone_logic.h"
|
|
||||||
#include "../../interface/card_picture_loader/card_picture_loader.h"
|
#include "../../interface/card_picture_loader/card_picture_loader.h"
|
||||||
#include "../../interface/widgets/cards/art_crop_attribution.h"
|
#include "../../interface/widgets/cards/art_crop_attribution.h"
|
||||||
#include "../../interface/widgets/cards/card_art_utils.h"
|
#include "../../interface/widgets/cards/card_art_utils.h"
|
||||||
|
|
@ -45,6 +36,15 @@
|
||||||
#include <libcockatrice/card/database/card_database_manager.h>
|
#include <libcockatrice/card/database/card_database_manager.h>
|
||||||
#include <libcockatrice/deck_list/deck_list.h>
|
#include <libcockatrice/deck_list/deck_list.h>
|
||||||
#include <libcockatrice/deck_list/playmat_resolver.h>
|
#include <libcockatrice/deck_list/playmat_resolver.h>
|
||||||
|
#include <libcockatrice/game/board/counter_state.h>
|
||||||
|
#include <libcockatrice/game/player/player_actions.h>
|
||||||
|
#include <libcockatrice/game/player/player_info.h>
|
||||||
|
#include <libcockatrice/game/player/player_logic.h>
|
||||||
|
#include <libcockatrice/game/zones/card_zone_logic.h>
|
||||||
|
#include <libcockatrice/game/zones/hand_zone_logic.h>
|
||||||
|
#include <libcockatrice/game/zones/pile_zone_logic.h>
|
||||||
|
#include <libcockatrice/game/zones/stack_zone_logic.h>
|
||||||
|
#include <libcockatrice/game/zones/table_zone_logic.h>
|
||||||
#include <libcockatrice/settings/interface_settings.h>
|
#include <libcockatrice/settings/interface_settings.h>
|
||||||
#include <qnamespace.h>
|
#include <qnamespace.h>
|
||||||
#include <qnumeric.h>
|
#include <qnumeric.h>
|
||||||
|
|
@ -177,6 +177,22 @@ void PlayerGraphicsItem::initializeZones()
|
||||||
|
|
||||||
void PlayerGraphicsItem::onCustomZoneAdded(QString customZoneName)
|
void PlayerGraphicsItem::onCustomZoneAdded(QString customZoneName)
|
||||||
{
|
{
|
||||||
|
// Custom zones have no board view, but their cards still need a graphics owner.
|
||||||
|
// Without one, PileZone::onCardAdded never runs and cards moved from the table
|
||||||
|
// or hand into a custom zone stay drawn at their old spot. Give them a hidden
|
||||||
|
// pile zone so added cards are reparented to (0, 0) and hidden.
|
||||||
|
CardZoneLogic *logic = player->getZone(customZoneName);
|
||||||
|
if (auto *pileLogic = qobject_cast<PileZoneLogic *>(logic)) {
|
||||||
|
auto *hiddenZone = new PileZone(pileLogic, this);
|
||||||
|
hiddenZone->setVisible(false);
|
||||||
|
hiddenZone->setPos(0, 0);
|
||||||
|
zoneGraphicsItems.insert(customZoneName, hiddenZone);
|
||||||
|
// Hide any cards that arrived before the graphics item existed.
|
||||||
|
for (CardState *state : pileLogic->getCards()) {
|
||||||
|
hiddenZone->onCardAdded(state);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
zoneGraphicsItems.insert(customZoneName, nullptr); // Custom zone view goes here, if we ever implement it.
|
zoneGraphicsItems.insert(customZoneName, nullptr); // Custom zone view goes here, if we ever implement it.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
#include "player_target.h"
|
#include "player_target.h"
|
||||||
|
|
||||||
#include "../../client/settings/cache_settings.h"
|
#include "../../client/settings/cache_settings.h"
|
||||||
#include "../../game/player/player_info.h"
|
|
||||||
#include "../../game/player/player_logic.h"
|
|
||||||
#include "../../interface/pixel_map_generator.h"
|
#include "../../interface/pixel_map_generator.h"
|
||||||
|
#include "../../interface/theme_manager.h"
|
||||||
#include "../board/abstract_counter.h"
|
#include "../board/abstract_counter.h"
|
||||||
#include "../board/abstract_graphics_item.h"
|
#include "../board/abstract_graphics_item.h"
|
||||||
#include "../board/arrow_target.h"
|
#include "../board/arrow_target.h"
|
||||||
|
|
@ -30,6 +29,8 @@
|
||||||
#include <QTransform>
|
#include <QTransform>
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
#include <QtMath>
|
#include <QtMath>
|
||||||
|
#include <libcockatrice/game/player/player_info.h>
|
||||||
|
#include <libcockatrice/game/player/player_logic.h>
|
||||||
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
|
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
|
||||||
#include <qnamespace.h>
|
#include <qnamespace.h>
|
||||||
#include <qnumeric.h>
|
#include <qnumeric.h>
|
||||||
|
|
@ -76,7 +77,7 @@ void PlayerCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*
|
||||||
|
|
||||||
QRectF translatedRect = path.controlPointRect();
|
QRectF translatedRect = path.controlPointRect();
|
||||||
QSize translatedSize = translatedRect.size().toSize();
|
QSize translatedSize = translatedRect.size().toSize();
|
||||||
QFont font("Serif");
|
QFont font = themeManager->cardTextFont();
|
||||||
font.setWeight(QFont::Bold);
|
font.setWeight(QFont::Bold);
|
||||||
font.setPixelSize(qMax(qRound(translatedSize.height() / 1.3), 9));
|
font.setPixelSize(qMax(qRound(translatedSize.height() / 1.3), 9));
|
||||||
painter->setFont(font);
|
painter->setFont(font);
|
||||||
|
|
@ -226,7 +227,7 @@ void PlayerTarget::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*o
|
||||||
name = name.mid(0, 10) + "...";
|
name = name.mid(0, 10) + "...";
|
||||||
}
|
}
|
||||||
|
|
||||||
QFont font;
|
QFont font = themeManager->appFont(AppFont::CardText);
|
||||||
font.setPixelSize(qMax(qRound(translatedNameRect.height() / 1.5), 9));
|
font.setPixelSize(qMax(qRound(translatedNameRect.height() / 1.5), 9));
|
||||||
painter->setFont(font);
|
painter->setFont(font);
|
||||||
painter->setPen(Qt::white);
|
painter->setPen(Qt::white);
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ static int sumPowers(const QList<CardItem *> &cards)
|
||||||
// calculate total power;
|
// calculate total power;
|
||||||
int total = 0;
|
int total = 0;
|
||||||
for (auto card : cards) {
|
for (auto card : cards) {
|
||||||
QVariantList parsed = CardItem::parsePT(card->getPT());
|
QVariantList parsed = CardState::parsePT(card->getPT());
|
||||||
if (!parsed.isEmpty()) {
|
if (!parsed.isEmpty()) {
|
||||||
int power = parsed.first().toInt(); // toInt will default to 0 if it's not an int
|
int power = parsed.first().toInt(); // toInt will default to 0 if it's not an int
|
||||||
total += qMax(power, 0);
|
total += qMax(power, 0);
|
||||||
|
|
@ -44,7 +44,7 @@ static int sumToughness(const QList<CardItem *> &cards)
|
||||||
{
|
{
|
||||||
int total = 0;
|
int total = 0;
|
||||||
for (auto card : cards) {
|
for (auto card : cards) {
|
||||||
QVariantList parsed = CardItem::parsePT(card->getPT());
|
QVariantList parsed = CardState::parsePT(card->getPT());
|
||||||
if (parsed.size() == 2) {
|
if (parsed.size() == 2) {
|
||||||
int toughness = parsed.at(1).toInt(); // toInt will default to 0 if it's not an int
|
int toughness = parsed.at(1).toInt(); // toInt will default to 0 if it's not an int
|
||||||
total += qMax(toughness, 0);
|
total += qMax(toughness, 0);
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
#include "card_zone.h"
|
#include "card_zone.h"
|
||||||
|
|
||||||
#include "../../game/board/card_list.h"
|
|
||||||
#include "../../game/zones/card_zone_logic.h"
|
|
||||||
#include "../board/abstract_graphics_item.h"
|
#include "../board/abstract_graphics_item.h"
|
||||||
#include "../board/card_item.h"
|
#include "../board/card_item.h"
|
||||||
|
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
#include <QGraphicsSceneMouseEvent>
|
#include <QGraphicsSceneMouseEvent>
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
|
#include <libcockatrice/game/board/card_list.h>
|
||||||
|
#include <libcockatrice/game/zones/card_zone_logic.h>
|
||||||
#include <qnamespace.h>
|
#include <qnamespace.h>
|
||||||
|
|
||||||
class QGraphicsItem;
|
class QGraphicsItem;
|
||||||
|
|
@ -18,23 +18,60 @@ CardZone::CardZone(CardZoneLogic *_logic, QGraphicsItem *parent)
|
||||||
: AbstractGraphicsItem(parent), menu(nullptr), doubleClickAction(0), logic(_logic)
|
: AbstractGraphicsItem(parent), menu(nullptr), doubleClickAction(0), logic(_logic)
|
||||||
{
|
{
|
||||||
connect(logic, &CardZoneLogic::retranslateUi, this, &CardZone::retranslateUi);
|
connect(logic, &CardZoneLogic::retranslateUi, this, &CardZone::retranslateUi);
|
||||||
|
// Invalidation is connected before any consumer so a handler that reads cardItems()
|
||||||
|
// during the same signal emission always sees a stale flag and rebuilds.
|
||||||
|
connect(logic, &CardZoneLogic::cardAdded, this, &CardZone::invalidateItems);
|
||||||
|
connect(logic, &CardZoneLogic::cardCountChanged, this, &CardZone::invalidateItems);
|
||||||
|
connect(logic, &CardZoneLogic::reorganizeCards, this, &CardZone::invalidateItems);
|
||||||
connect(logic, &CardZoneLogic::cardAdded, this, &CardZone::onCardAdded);
|
connect(logic, &CardZoneLogic::cardAdded, this, &CardZone::onCardAdded);
|
||||||
|
connect(logic, &CardZoneLogic::createCardRequested, this, &CardZone::onCreateCardRequested);
|
||||||
connect(logic, &CardZoneLogic::setGraphicsVisibility, this, [this](bool v) { this->setVisible(v); });
|
connect(logic, &CardZoneLogic::setGraphicsVisibility, this, [this](bool v) { this->setVisible(v); });
|
||||||
connect(logic, &CardZoneLogic::updateGraphics, this, [this]() { update(); });
|
connect(logic, &CardZoneLogic::updateGraphics, this, [this]() { update(); });
|
||||||
connect(logic, &CardZoneLogic::reorganizeCards, this, &CardZone::reorganizeCards);
|
connect(logic, &CardZoneLogic::reorganizeCards, this, &CardZone::reorganizeCards);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CardZone::onCardAdded(CardItem *addedCard)
|
void CardZone::invalidateItems()
|
||||||
{
|
{
|
||||||
addedCard->setParentItem(this);
|
itemsDirty = true;
|
||||||
addedCard->setVisible(true);
|
}
|
||||||
addedCard->update();
|
|
||||||
|
const QList<CardItem *> &CardZone::cardItems() const
|
||||||
|
{
|
||||||
|
if (itemsDirty) {
|
||||||
|
items.clear();
|
||||||
|
for (auto *cardState : getLogic()->getCards()) {
|
||||||
|
if (auto *cardItem = qobject_cast<CardItem *>(cardState->parent())) {
|
||||||
|
items.append(cardItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
itemsDirty = false;
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CardZone::onCardAdded(CardState *addedCard)
|
||||||
|
{
|
||||||
|
auto *addedItem = addedCard == nullptr ? nullptr : qobject_cast<CardItem *>(addedCard->parent());
|
||||||
|
if (addedItem == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addedItem->setParentItem(this);
|
||||||
|
addedItem->setVisible(true);
|
||||||
|
addedItem->update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CardZone::onCreateCardRequested(const ServerInfo_Card &cardInfo, bool reorganize)
|
||||||
|
{
|
||||||
|
CardRef cardRef = {QString::fromStdString(cardInfo.name()), QString::fromStdString(cardInfo.provider_id())};
|
||||||
|
auto *card = new CardItem(getLogic()->getPlayer(), nullptr, cardRef, cardInfo.id());
|
||||||
|
card->processCardInfo(cardInfo);
|
||||||
|
getLogic()->addCard(card->getState(), reorganize, cardInfo.x(), cardInfo.y());
|
||||||
}
|
}
|
||||||
|
|
||||||
void CardZone::retranslateUi()
|
void CardZone::retranslateUi()
|
||||||
{
|
{
|
||||||
for (int i = 0; i < getLogic()->getCards().size(); ++i) {
|
for (auto *card : cardItems()) {
|
||||||
getLogic()->getCards()[i]->retranslateUi();
|
card->retranslateUi();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,17 @@
|
||||||
|
|
||||||
class CardDragItem;
|
class CardDragItem;
|
||||||
class CardItem;
|
class CardItem;
|
||||||
|
class CardState;
|
||||||
class CardZoneLogic;
|
class CardZoneLogic;
|
||||||
class QAction;
|
class QAction;
|
||||||
class QGraphicsItem;
|
class QGraphicsItem;
|
||||||
class QMenu;
|
class QMenu;
|
||||||
class QPoint;
|
class QPoint;
|
||||||
|
class ServerInfo_Card;
|
||||||
template <typename T> class QList;
|
template <typename T> class QList;
|
||||||
|
|
||||||
|
class CardItem;
|
||||||
|
|
||||||
inline Q_LOGGING_CATEGORY(CardZoneLog, "card_zone");
|
inline Q_LOGGING_CATEGORY(CardZoneLog, "card_zone");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -55,7 +59,9 @@ public slots:
|
||||||
* Virtual so subclasses (e.g. SelectZone) can override parenting behavior — the Qt signal
|
* Virtual so subclasses (e.g. SelectZone) can override parenting behavior — the Qt signal
|
||||||
* connection in CardZone's constructor dispatches through the vtable.
|
* connection in CardZone's constructor dispatches through the vtable.
|
||||||
*/
|
*/
|
||||||
virtual void onCardAdded(CardItem *addedCard);
|
virtual void onCardAdded(CardState *addedCard);
|
||||||
|
/** @brief Builds the card item for @p cardInfo and hands it to this zone's logic. */
|
||||||
|
void onCreateCardRequested(const ServerInfo_Card &cardInfo, bool reorganize);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
enum
|
enum
|
||||||
|
|
@ -76,6 +82,15 @@ public:
|
||||||
return logic;
|
return logic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The CardItems backing getLogic()->getCards(), in zone order.
|
||||||
|
*
|
||||||
|
* Game logic talks about cards as states; this is the graphics side's view of the same list.
|
||||||
|
* It is rebuilt on demand after any content change, so views never reach into the logic's
|
||||||
|
* container themselves and never observe a stale one.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] const QList<CardItem *> &cardItems() const;
|
||||||
|
|
||||||
void setMenu(QMenu *_menu, QAction *_doubleClickAction = 0)
|
void setMenu(QMenu *_menu, QAction *_doubleClickAction = 0)
|
||||||
{
|
{
|
||||||
menu = _menu;
|
menu = _menu;
|
||||||
|
|
@ -83,7 +98,12 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
/** @brief Marks the cached card items stale. Connected to every content-changing signal. */
|
||||||
|
void invalidateItems();
|
||||||
|
|
||||||
CardZoneLogic *logic;
|
CardZoneLogic *logic;
|
||||||
|
mutable QList<CardItem *> items;
|
||||||
|
mutable bool itemsDirty = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,6 @@
|
||||||
#include "hand_zone.h"
|
#include "hand_zone.h"
|
||||||
|
|
||||||
#include "../../client/settings/cache_settings.h"
|
#include "../../client/settings/cache_settings.h"
|
||||||
#include "../../game/player/player_actions.h"
|
|
||||||
#include "../../game/player/player_info.h"
|
|
||||||
#include "../../game/player/player_logic.h"
|
|
||||||
#include "../../game/zones/card_zone_logic.h"
|
|
||||||
#include "../../game/zones/hand_zone_logic.h"
|
|
||||||
#include "../../interface/theme_manager.h"
|
#include "../../interface/theme_manager.h"
|
||||||
#include "../board/card_drag_item.h"
|
#include "../board/card_drag_item.h"
|
||||||
#include "../board/card_item.h"
|
#include "../board/card_item.h"
|
||||||
|
|
@ -18,7 +13,11 @@
|
||||||
#include <QPointF>
|
#include <QPointF>
|
||||||
#include <QRectF>
|
#include <QRectF>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <libcockatrice/protocol/pb/command_move_card.pb.h>
|
#include <libcockatrice/game/player/player_actions.h>
|
||||||
|
#include <libcockatrice/game/player/player_info.h>
|
||||||
|
#include <libcockatrice/game/player/player_logic.h>
|
||||||
|
#include <libcockatrice/game/zones/card_zone_logic.h>
|
||||||
|
#include <libcockatrice/game/zones/hand_zone_logic.h>
|
||||||
#include <libcockatrice/settings/interface_settings.h>
|
#include <libcockatrice/settings/interface_settings.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
|
@ -48,8 +47,8 @@ void HandZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
|
||||||
QPoint point = dropPoint + scenePos().toPoint();
|
QPoint point = dropPoint + scenePos().toPoint();
|
||||||
int x = -1;
|
int x = -1;
|
||||||
if (SettingsCache::instance().userInterface().getHorizontalHand()) {
|
if (SettingsCache::instance().userInterface().getHorizontalHand()) {
|
||||||
for (x = 0; x < getLogic()->getCards().size(); x++) {
|
for (x = 0; x < cardItems().size(); x++) {
|
||||||
if (point.x() < static_cast<CardItem *>(getLogic()->getCards().at(x))->scenePos().x()) {
|
if (point.x() < cardItems().at(x)->scenePos().x()) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -58,19 +57,13 @@ void HandZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
|
||||||
x = calcDropIndexFromY(dropPoint.y(), !sameZone);
|
x = calcDropIndexFromY(dropPoint.y(), !sameZone);
|
||||||
}
|
}
|
||||||
|
|
||||||
Command_MoveCard cmd;
|
QList<CardMoveRequest> cards;
|
||||||
cmd.set_start_player_id(startZone->getPlayer()->getPlayerInfo()->getId());
|
cards.reserve(dragItems.size());
|
||||||
cmd.set_start_zone(startZone->getName().toStdString());
|
for (const CardDragItem *item : dragItems) {
|
||||||
cmd.set_target_player_id(getLogic()->getPlayer()->getPlayerInfo()->getId());
|
cards.append(CardMoveRequest{item->getId()});
|
||||||
cmd.set_target_zone(getLogic()->getName().toStdString());
|
|
||||||
cmd.set_x(x);
|
|
||||||
cmd.set_y(-1);
|
|
||||||
|
|
||||||
for (int i = 0; i < dragItems.size(); ++i) {
|
|
||||||
cmd.mutable_cards_to_move()->add_card()->set_card_id(dragItems[i]->getId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getLogic()->getPlayer()->getPlayerActions()->sendGameCommand(cmd);
|
getLogic()->getPlayer()->getPlayerActions()->moveCards(startZone, getLogic(), x, -1, cards);
|
||||||
}
|
}
|
||||||
|
|
||||||
QRectF HandZone::boundingRect() const
|
QRectF HandZone::boundingRect() const
|
||||||
|
|
@ -90,23 +83,23 @@ void HandZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*optio
|
||||||
|
|
||||||
void HandZone::reorganizeCards()
|
void HandZone::reorganizeCards()
|
||||||
{
|
{
|
||||||
if (!getLogic()->getCards().isEmpty()) {
|
if (!cardItems().isEmpty()) {
|
||||||
const int cardCount = getLogic()->getCards().size();
|
const int cardCount = cardItems().size();
|
||||||
if (SettingsCache::instance().userInterface().getHorizontalHand()) {
|
if (SettingsCache::instance().userInterface().getHorizontalHand()) {
|
||||||
bool leftJustified = SettingsCache::instance().userInterface().getLeftJustified();
|
bool leftJustified = SettingsCache::instance().userInterface().getLeftJustified();
|
||||||
qreal cardWidth = getLogic()->getCards().at(0)->boundingRect().width();
|
qreal cardWidth = cardItems().at(0)->boundingRect().width();
|
||||||
const int xPadding = leftJustified ? cardWidth * 1.4 : 5;
|
const int xPadding = leftJustified ? cardWidth * 1.4 : 5;
|
||||||
qreal totalWidth =
|
qreal totalWidth =
|
||||||
leftJustified ? boundingRect().width() - (1 * xPadding) - 5 : boundingRect().width() - 2 * xPadding;
|
leftJustified ? boundingRect().width() - (1 * xPadding) - 5 : boundingRect().width() - 2 * xPadding;
|
||||||
|
|
||||||
if (cardCount == 1) {
|
if (cardCount == 1) {
|
||||||
CardItem *c = getLogic()->getCards().at(0);
|
CardItem *c = cardItems().at(0);
|
||||||
qreal xPosition = leftJustified ? xPadding : xPadding + (totalWidth - cardWidth) / 2;
|
qreal xPosition = leftJustified ? xPadding : xPadding + (totalWidth - cardWidth) / 2;
|
||||||
c->setPos(xPosition, 5);
|
c->setPos(xPosition, 5);
|
||||||
c->setRealZValue(0);
|
c->setRealZValue(0);
|
||||||
} else {
|
} else {
|
||||||
for (int i = 0; i < cardCount; i++) {
|
for (int i = 0; i < cardCount; i++) {
|
||||||
CardItem *c = getLogic()->getCards().at(i);
|
CardItem *c = cardItems().at(i);
|
||||||
// If the total width of the cards is smaller than the available width,
|
// If the total width of the cards is smaller than the available width,
|
||||||
// the cards do not need to overlap and are displayed in the center of the area.
|
// the cards do not need to overlap and are displayed in the center of the area.
|
||||||
if (cardWidth * cardCount > totalWidth) {
|
if (cardWidth * cardCount > totalWidth) {
|
||||||
|
|
@ -131,7 +124,7 @@ void HandZone::reorganizeCards()
|
||||||
|
|
||||||
void HandZone::sortHand(const QList<CardList::SortOption> &options)
|
void HandZone::sortHand(const QList<CardList::SortOption> &options)
|
||||||
{
|
{
|
||||||
if (getLogic()->getCards().isEmpty()) {
|
if (cardItems().isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,10 @@
|
||||||
#ifndef HANDZONE_H
|
#ifndef HANDZONE_H
|
||||||
#define HANDZONE_H
|
#define HANDZONE_H
|
||||||
|
|
||||||
#include "../../game/board/card_list.h"
|
|
||||||
#include "select_zone.h"
|
#include "select_zone.h"
|
||||||
|
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
|
#include <libcockatrice/game/board/card_list.h>
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
|
|
||||||
class HandZoneLogic;
|
class HandZoneLogic;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,6 @@
|
||||||
#include "pile_zone.h"
|
#include "pile_zone.h"
|
||||||
|
|
||||||
#include "../../client/settings/cache_settings.h"
|
#include "../../client/settings/cache_settings.h"
|
||||||
#include "../../game/board/card_list.h"
|
|
||||||
#include "../../game/player/player_actions.h"
|
|
||||||
#include "../../game/player/player_info.h"
|
|
||||||
#include "../../game/player/player_logic.h"
|
|
||||||
#include "../../game/zones/card_zone_logic.h"
|
|
||||||
#include "../../game/zones/pile_zone_logic.h"
|
|
||||||
#include "../board/card_drag_item.h"
|
#include "../board/card_drag_item.h"
|
||||||
#include "../board/card_item.h"
|
#include "../board/card_item.h"
|
||||||
#include "../card_dimensions.h"
|
#include "../card_dimensions.h"
|
||||||
|
|
@ -24,7 +18,12 @@
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QTransform>
|
#include <QTransform>
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
#include <libcockatrice/protocol/pb/command_move_card.pb.h>
|
#include <libcockatrice/game/board/card_list.h>
|
||||||
|
#include <libcockatrice/game/player/player_actions.h>
|
||||||
|
#include <libcockatrice/game/player/player_info.h>
|
||||||
|
#include <libcockatrice/game/player/player_logic.h>
|
||||||
|
#include <libcockatrice/game/zones/card_zone_logic.h>
|
||||||
|
#include <libcockatrice/game/zones/pile_zone_logic.h>
|
||||||
#include <libcockatrice/settings/cards_display_settings.h>
|
#include <libcockatrice/settings/cards_display_settings.h>
|
||||||
#include <qnamespace.h>
|
#include <qnamespace.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
@ -67,36 +66,35 @@ void PileZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*optio
|
||||||
{
|
{
|
||||||
painter->drawPath(shape());
|
painter->drawPath(shape());
|
||||||
|
|
||||||
if (!getLogic()->getCards().isEmpty()) {
|
if (!cardItems().isEmpty()) {
|
||||||
getLogic()->getCards().at(0)->paintPicture(painter, getLogic()->getCards().at(0)->getTranslatedSize(painter),
|
cardItems().at(0)->paintPicture(painter, cardItems().at(0)->getTranslatedSize(painter), 90);
|
||||||
90);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
painter->translate(CardDimensions::WIDTH_HALF_F, CardDimensions::HEIGHT_HALF_F);
|
painter->translate(CardDimensions::WIDTH_HALF_F, CardDimensions::HEIGHT_HALF_F);
|
||||||
painter->rotate(-90);
|
painter->rotate(-90);
|
||||||
painter->translate(-CardDimensions::WIDTH_HALF_F, -CardDimensions::HEIGHT_HALF_F);
|
painter->translate(-CardDimensions::WIDTH_HALF_F, -CardDimensions::HEIGHT_HALF_F);
|
||||||
paintNumberEllipse(getLogic()->getCards().size(), 28, Qt::white, -1, -1, painter);
|
paintNumberEllipse(cardItems().size(), 28, Qt::white, -1, -1, painter);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PileZone::handleDropEvent(const QList<CardDragItem *> &dragItems, CardZoneLogic *startZone, const QPoint &)
|
void PileZone::handleDropEvent(const QList<CardDragItem *> &dragItems, CardZoneLogic *startZone, const QPoint &)
|
||||||
{
|
{
|
||||||
Command_MoveCard cmd;
|
QList<CardMoveRequest> cards;
|
||||||
cmd.set_start_player_id(startZone->getPlayer()->getPlayerInfo()->getId());
|
cards.reserve(dragItems.size());
|
||||||
cmd.set_start_zone(startZone->getName().toStdString());
|
for (const CardDragItem *item : dragItems) {
|
||||||
cmd.set_target_player_id(getLogic()->getPlayer()->getPlayerInfo()->getId());
|
cards.append(CardMoveRequest{item->getId(), item->isForceFaceDown()});
|
||||||
cmd.set_target_zone(getLogic()->getName().toStdString());
|
|
||||||
cmd.set_x(0);
|
|
||||||
cmd.set_y(0);
|
|
||||||
|
|
||||||
for (int i = 0; i < dragItems.size(); ++i) {
|
|
||||||
auto cardToMove = cmd.mutable_cards_to_move()->add_card();
|
|
||||||
cardToMove->set_card_id(dragItems[i]->getId());
|
|
||||||
if (dragItems[i]->isForceFaceDown()) {
|
|
||||||
cardToMove->set_face_down(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getLogic()->getPlayer()->getPlayerActions()->sendGameCommand(cmd);
|
getLogic()->getPlayer()->getPlayerActions()->moveCards(startZone, getLogic(), 0, 0, cards);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PileZone::onCardAdded(CardState *addedCard)
|
||||||
|
{
|
||||||
|
CardZone::onCardAdded(addedCard);
|
||||||
|
auto *addedItem = addedCard == nullptr ? nullptr : qobject_cast<CardItem *>(addedCard->parent());
|
||||||
|
if (addedItem) {
|
||||||
|
addedItem->setPos(0, 0);
|
||||||
|
addedItem->setVisible(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PileZone::reorganizeCards()
|
void PileZone::reorganizeCards()
|
||||||
|
|
@ -126,15 +124,14 @@ void PileZone::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getLogic()->getCards().isEmpty()) {
|
if (cardItems().isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool forceFaceDown = event->modifiers().testFlag(Qt::ShiftModifier);
|
bool forceFaceDown = event->modifiers().testFlag(Qt::ShiftModifier);
|
||||||
bool bottomCard = event->modifiers().testFlag(Qt::ControlModifier);
|
bool bottomCard = event->modifiers().testFlag(Qt::ControlModifier);
|
||||||
CardItem *card = bottomCard ? getLogic()->getCards().last() : getLogic()->getCards().first();
|
CardItem *card = bottomCard ? cardItems().last() : cardItems().first();
|
||||||
const int cardid =
|
const int cardid = getLogic()->contentsKnown() ? card->getId() : (bottomCard ? cardItems().size() - 1 : 0);
|
||||||
getLogic()->contentsKnown() ? card->getId() : (bottomCard ? getLogic()->getCards().size() - 1 : 0);
|
|
||||||
CardDragItem *drag = card->createDragItem(cardid, event->pos(), event->scenePos(), forceFaceDown);
|
CardDragItem *drag = card->createDragItem(cardid, event->pos(), event->scenePos(), forceFaceDown);
|
||||||
drag->grabMouse();
|
drag->grabMouse();
|
||||||
setCursor(Qt::OpenHandCursor);
|
setCursor(Qt::OpenHandCursor);
|
||||||
|
|
@ -147,8 +144,8 @@ void PileZone::mouseReleaseEvent(QGraphicsSceneMouseEvent * /*event*/)
|
||||||
|
|
||||||
void PileZone::hoverEnterEvent(QGraphicsSceneHoverEvent *event)
|
void PileZone::hoverEnterEvent(QGraphicsSceneHoverEvent *event)
|
||||||
{
|
{
|
||||||
if (!getLogic()->getCards().isEmpty()) {
|
if (!cardItems().isEmpty()) {
|
||||||
getLogic()->getCards()[0]->processHoverEvent();
|
cardItems()[0]->processHoverEvent();
|
||||||
}
|
}
|
||||||
QGraphicsItem::hoverEnterEvent(event);
|
QGraphicsItem::hoverEnterEvent(event);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ public:
|
||||||
[[nodiscard]] QPainterPath shape() const override;
|
[[nodiscard]] QPainterPath shape() const override;
|
||||||
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
|
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
|
||||||
void reorganizeCards() override;
|
void reorganizeCards() override;
|
||||||
|
void onCardAdded(CardState *addedCard) override;
|
||||||
void
|
void
|
||||||
handleDropEvent(const QList<CardDragItem *> &dragItems, CardZoneLogic *startZone, const QPoint &dropPoint) override;
|
handleDropEvent(const QList<CardDragItem *> &dragItems, CardZoneLogic *startZone, const QPoint &dropPoint) override;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
#include "select_zone.h"
|
#include "select_zone.h"
|
||||||
|
|
||||||
#include "../../client/settings/cache_settings.h"
|
#include "../../client/settings/cache_settings.h"
|
||||||
#include "../../game/board/card_list.h"
|
|
||||||
#include "../../game/zones/card_zone_logic.h"
|
|
||||||
#include "../board/card_item.h"
|
#include "../board/card_item.h"
|
||||||
#include "../game_scene.h"
|
#include "../game_scene.h"
|
||||||
|
|
||||||
|
|
@ -17,6 +15,8 @@
|
||||||
#include <QTransform>
|
#include <QTransform>
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
#include <QtMath>
|
#include <QtMath>
|
||||||
|
#include <libcockatrice/game/board/card_list.h>
|
||||||
|
#include <libcockatrice/game/zones/card_zone_logic.h>
|
||||||
#include <libcockatrice/settings/cards_display_settings.h>
|
#include <libcockatrice/settings/cards_display_settings.h>
|
||||||
#include <qnamespace.h>
|
#include <qnamespace.h>
|
||||||
#include <qnumeric.h>
|
#include <qnumeric.h>
|
||||||
|
|
@ -85,7 +85,7 @@ SelectZone *SelectZone::findOwningSelectZone(const QGraphicsItem *card)
|
||||||
|
|
||||||
SelectZone::StackLayoutParams SelectZone::buildStackParams(qreal minOffset) const
|
SelectZone::StackLayoutParams SelectZone::buildStackParams(qreal minOffset) const
|
||||||
{
|
{
|
||||||
const auto &cards = getLogic()->getCards();
|
const auto &cards = cardItems();
|
||||||
if (cards.isEmpty()) {
|
if (cards.isEmpty()) {
|
||||||
return {0, boundingRect().height(), 0.0, 0.0, minOffset};
|
return {0, boundingRect().height(), 0.0, 0.0, minOffset};
|
||||||
}
|
}
|
||||||
|
|
@ -97,7 +97,7 @@ SelectZone::StackLayoutParams SelectZone::buildStackParams(qreal minOffset) cons
|
||||||
|
|
||||||
int SelectZone::calcDropIndexFromY(qreal dropY, bool allowCountExpand, qreal minOffset) const
|
int SelectZone::calcDropIndexFromY(qreal dropY, bool allowCountExpand, qreal minOffset) const
|
||||||
{
|
{
|
||||||
const auto &cards = getLogic()->getCards();
|
const auto &cards = cardItems();
|
||||||
if (cards.isEmpty()) {
|
if (cards.isEmpty()) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
@ -115,7 +115,7 @@ void SelectZone::restoreStaleEscapedCards()
|
||||||
if (!cardClipContainer) {
|
if (!cardClipContainer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (auto *card : getLogic()->getCards()) {
|
for (auto *card : cardItems()) {
|
||||||
// A card parented to the zone (instead of the clip container) should
|
// A card parented to the zone (instead of the clip container) should
|
||||||
// only occur while it is actively hovered. If hover cleanup was
|
// only occur while it is actively hovered. If hover cleanup was
|
||||||
// missed, reparent it back so clipping resumes.
|
// missed, reparent it back so clipping resumes.
|
||||||
|
|
@ -127,7 +127,7 @@ void SelectZone::restoreStaleEscapedCards()
|
||||||
|
|
||||||
void SelectZone::layoutCardsVertically(const StackLayoutParams ¶ms)
|
void SelectZone::layoutCardsVertically(const StackLayoutParams ¶ms)
|
||||||
{
|
{
|
||||||
const auto &cards = getLogic()->getCards();
|
const auto &cards = cardItems();
|
||||||
if (cards.isEmpty() || params.cardCount <= 0) {
|
if (cards.isEmpty() || params.cardCount <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -162,9 +162,9 @@ SelectZone::~SelectZone()
|
||||||
if (cardClipContainer) {
|
if (cardClipContainer) {
|
||||||
// Reparent any hover-escaped cards back to the clip container so Qt's
|
// Reparent any hover-escaped cards back to the clip container so Qt's
|
||||||
// parent-child tree is consistent for destruction. setParentItem() does
|
// parent-child tree is consistent for destruction. setParentItem() does
|
||||||
// not invalidate getLogic()->getCards() (it modifies the graphics tree,
|
// not invalidate cardItems() — it only moves the item in the graphics
|
||||||
// not the zone's logical card list).
|
// tree, it never changes the zone's contents.
|
||||||
for (auto *card : getLogic()->getCards()) {
|
for (auto *card : cardItems()) {
|
||||||
if (card && card->parentItem() == this) {
|
if (card && card->parentItem() == this) {
|
||||||
card->setParentItem(cardClipContainer);
|
card->setParentItem(cardClipContainer);
|
||||||
}
|
}
|
||||||
|
|
@ -172,12 +172,13 @@ SelectZone::~SelectZone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void SelectZone::onCardAdded(CardItem *addedCard)
|
void SelectZone::onCardAdded(CardState *addedCard)
|
||||||
{
|
{
|
||||||
if (cardClipContainer && addedCard) {
|
auto *addedItem = addedCard == nullptr ? nullptr : qobject_cast<CardItem *>(addedCard->parent());
|
||||||
addedCard->setParentItem(cardClipContainer);
|
if (cardClipContainer && addedItem) {
|
||||||
addedCard->setVisible(true);
|
addedItem->setParentItem(cardClipContainer);
|
||||||
addedCard->update();
|
addedItem->setVisible(true);
|
||||||
|
addedItem->update();
|
||||||
} else {
|
} else {
|
||||||
CardZone::onCardAdded(addedCard);
|
CardZone::onCardAdded(addedCard);
|
||||||
}
|
}
|
||||||
|
|
@ -247,7 +248,7 @@ void SelectZone::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
|
||||||
}
|
}
|
||||||
|
|
||||||
QRectF selectionRect = QRectF(selectionOrigin, pos).normalized();
|
QRectF selectionRect = QRectF(selectionOrigin, pos).normalized();
|
||||||
for (auto card : getLogic()->getCards()) {
|
for (auto card : cardItems()) {
|
||||||
if (card->getAttachedTo() && card->getAttachedTo()->getZone() != getLogic()) {
|
if (card->getAttachedTo() && card->getAttachedTo()->getZone() != getLogic()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ public:
|
||||||
|
|
||||||
SelectZone(CardZoneLogic *logic, QGraphicsItem *parent = nullptr);
|
SelectZone(CardZoneLogic *logic, QGraphicsItem *parent = nullptr);
|
||||||
~SelectZone() override;
|
~SelectZone() override;
|
||||||
void onCardAdded(CardItem *addedCard) override;
|
void onCardAdded(CardState *addedCard) override;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Temporarily reparents a card from the clip container to this zone so hover scaling is visible beyond clip
|
* @brief Temporarily reparents a card from the clip container to this zone so hover scaling is visible beyond clip
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,5 @@
|
||||||
#include "stack_zone.h"
|
#include "stack_zone.h"
|
||||||
|
|
||||||
#include "../../game/board/card_list.h"
|
|
||||||
#include "../../game/player/player_actions.h"
|
|
||||||
#include "../../game/player/player_info.h"
|
|
||||||
#include "../../game/player/player_logic.h"
|
|
||||||
#include "../../game/zones/card_zone_logic.h"
|
|
||||||
#include "../../game/zones/stack_zone_logic.h"
|
|
||||||
#include "../../interface/theme_manager.h"
|
#include "../../interface/theme_manager.h"
|
||||||
#include "../board/card_drag_item.h"
|
#include "../board/card_drag_item.h"
|
||||||
#include "../board/card_item.h"
|
#include "../board/card_item.h"
|
||||||
|
|
@ -19,7 +13,12 @@
|
||||||
#include <QPoint>
|
#include <QPoint>
|
||||||
#include <QRectF>
|
#include <QRectF>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <libcockatrice/protocol/pb/command_move_card.pb.h>
|
#include <libcockatrice/game/board/card_list.h>
|
||||||
|
#include <libcockatrice/game/player/player_actions.h>
|
||||||
|
#include <libcockatrice/game/player/player_info.h>
|
||||||
|
#include <libcockatrice/game/player/player_logic.h>
|
||||||
|
#include <libcockatrice/game/zones/card_zone_logic.h>
|
||||||
|
#include <libcockatrice/game/zones/stack_zone_logic.h>
|
||||||
#include <qnumeric.h>
|
#include <qnumeric.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
|
@ -73,31 +72,21 @@ void StackZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
|
||||||
int index = calcDropIndexFromY(dropPoint.y(), !sameZone, MIN_CARD_VISIBLE);
|
int index = calcDropIndexFromY(dropPoint.y(), !sameZone, MIN_CARD_VISIBLE);
|
||||||
if (sameZone) {
|
if (sameZone) {
|
||||||
// Same-zone no-op: don't move a card onto itself
|
// Same-zone no-op: don't move a card onto itself
|
||||||
const auto &cards = getLogic()->getCards();
|
const auto &cards = cardItems();
|
||||||
if (!cards.isEmpty() && cards.at(index)->getId() == dragItems.at(0)->getId()) {
|
if (!cards.isEmpty() && cards.at(index)->getId() == dragItems.at(0)->getId()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Command_MoveCard cmd;
|
QList<CardMoveRequest> cards;
|
||||||
cmd.set_start_player_id(startZone->getPlayer()->getPlayerInfo()->getId());
|
cards.reserve(dragItems.size());
|
||||||
cmd.set_start_zone(startZone->getName().toStdString());
|
|
||||||
cmd.set_target_player_id(getLogic()->getPlayer()->getPlayerInfo()->getId());
|
|
||||||
cmd.set_target_zone(getLogic()->getName().toStdString());
|
|
||||||
cmd.set_x(index);
|
|
||||||
cmd.set_y(0);
|
|
||||||
|
|
||||||
for (const CardDragItem *item : dragItems) {
|
for (const CardDragItem *item : dragItems) {
|
||||||
if (item) {
|
if (item) {
|
||||||
auto *cardToMove = cmd.mutable_cards_to_move()->add_card();
|
cards.append(CardMoveRequest{item->getId(), item->isForceFaceDown()});
|
||||||
cardToMove->set_card_id(item->getId());
|
|
||||||
if (item->isForceFaceDown()) {
|
|
||||||
cardToMove->set_face_down(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getLogic()->getPlayer()->getPlayerActions()->sendGameCommand(cmd);
|
getLogic()->getPlayer()->getPlayerActions()->moveCards(startZone, getLogic(), index, 0, cards);
|
||||||
}
|
}
|
||||||
|
|
||||||
void StackZone::setHeight(qreal newHeight)
|
void StackZone::setHeight(qreal newHeight)
|
||||||
|
|
@ -113,7 +102,7 @@ void StackZone::setHeight(qreal newHeight)
|
||||||
|
|
||||||
void StackZone::reorganizeCards()
|
void StackZone::reorganizeCards()
|
||||||
{
|
{
|
||||||
if (!getLogic()->getCards().isEmpty()) {
|
if (!cardItems().isEmpty()) {
|
||||||
const auto params = buildStackParams(MIN_CARD_VISIBLE);
|
const auto params = buildStackParams(MIN_CARD_VISIBLE);
|
||||||
layoutCardsVertically(params);
|
layoutCardsVertically(params);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,6 @@
|
||||||
#include "table_zone.h"
|
#include "table_zone.h"
|
||||||
|
|
||||||
#include "../../client/settings/cache_settings.h"
|
#include "../../client/settings/cache_settings.h"
|
||||||
#include "../../game/board/card_list.h"
|
|
||||||
#include "../../game/player/player_actions.h"
|
|
||||||
#include "../../game/player/player_info.h"
|
|
||||||
#include "../../game/player/player_logic.h"
|
|
||||||
#include "../../game/zones/card_zone_logic.h"
|
|
||||||
#include "../../game/zones/table_zone_logic.h"
|
|
||||||
#include "../../interface/theme_manager.h"
|
#include "../../interface/theme_manager.h"
|
||||||
#include "../board/abstract_card_item.h"
|
#include "../board/abstract_card_item.h"
|
||||||
#include "../board/card_drag_item.h"
|
#include "../board/card_drag_item.h"
|
||||||
|
|
@ -32,8 +26,12 @@
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <iterator>
|
#include <iterator>
|
||||||
#include <libcockatrice/card/card_info.h>
|
#include <libcockatrice/card/card_info.h>
|
||||||
#include <libcockatrice/protocol/pb/command_move_card.pb.h>
|
#include <libcockatrice/game/board/card_list.h>
|
||||||
#include <libcockatrice/protocol/pb/command_set_card_attr.pb.h>
|
#include <libcockatrice/game/player/player_actions.h>
|
||||||
|
#include <libcockatrice/game/player/player_info.h>
|
||||||
|
#include <libcockatrice/game/player/player_logic.h>
|
||||||
|
#include <libcockatrice/game/zones/card_zone_logic.h>
|
||||||
|
#include <libcockatrice/game/zones/table_zone_logic.h>
|
||||||
#include <libcockatrice/settings/interface_settings.h>
|
#include <libcockatrice/settings/interface_settings.h>
|
||||||
#include <libcockatrice/utility/zone_names.h>
|
#include <libcockatrice/utility/zone_names.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
@ -62,7 +60,8 @@ TableZone::TableZone(TableZoneLogic *_logic, bool _mirrored, QGraphicsItem *pare
|
||||||
|
|
||||||
updateBg();
|
updateBg();
|
||||||
|
|
||||||
height = MARGIN_TOP + MARGIN_BOTTOM + TABLEROWS * CardDimensions::HEIGHT + (TABLEROWS - 1) * PADDING_Y;
|
height = MARGIN_TOP + MARGIN_BOTTOM + TableZoneLogic::ROW_COUNT * CardDimensions::HEIGHT +
|
||||||
|
(TableZoneLogic::ROW_COUNT - 1) * PADDING_Y;
|
||||||
width = MIN_WIDTH;
|
width = MIN_WIDTH;
|
||||||
currentMinimumWidth = width;
|
currentMinimumWidth = width;
|
||||||
|
|
||||||
|
|
@ -208,29 +207,20 @@ void TableZone::handleDropEventByGrid(const QList<CardDragItem *> &dragItems,
|
||||||
CardZoneLogic *startZone,
|
CardZoneLogic *startZone,
|
||||||
const QPoint &gridPoint)
|
const QPoint &gridPoint)
|
||||||
{
|
{
|
||||||
Command_MoveCard cmd;
|
QList<CardMoveRequest> cards;
|
||||||
cmd.set_start_player_id(startZone->getPlayer()->getPlayerInfo()->getId());
|
cards.reserve(dragItems.size());
|
||||||
cmd.set_start_zone(startZone->getName().toStdString());
|
for (const CardDragItem *item : dragItems) {
|
||||||
cmd.set_target_player_id(getLogic()->getPlayer()->getPlayerInfo()->getId());
|
CardMoveRequest card{item->getId(), item->isForceFaceDown()};
|
||||||
cmd.set_target_zone(getLogic()->getName().toStdString());
|
|
||||||
cmd.set_x(gridPoint.x());
|
|
||||||
cmd.set_y(gridPoint.y());
|
|
||||||
|
|
||||||
for (const auto &item : dragItems) {
|
|
||||||
CardToMove *ctm = cmd.mutable_cards_to_move()->add_card();
|
|
||||||
ctm->set_card_id(item->getId());
|
|
||||||
if (item->isForceFaceDown()) {
|
|
||||||
ctm->set_face_down(true);
|
|
||||||
}
|
|
||||||
if (startZone->getName() != getLogic()->getName() && !item->isForceFaceDown()) {
|
if (startZone->getName() != getLogic()->getName() && !item->isForceFaceDown()) {
|
||||||
const auto &card = item->getItem()->getCard();
|
const auto &cardInfo = item->getItem()->getCard();
|
||||||
if (card) {
|
if (cardInfo) {
|
||||||
ctm->set_pt(card.getInfo().getPowTough().toStdString());
|
card.pt = cardInfo.getInfo().getPowTough();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
cards.append(card);
|
||||||
}
|
}
|
||||||
|
|
||||||
startZone->getPlayer()->getPlayerActions()->sendGameCommand(cmd);
|
startZone->getPlayer()->getPlayerActions()->moveCards(startZone, getLogic(), gridPoint.x(), gridPoint.y(), cards);
|
||||||
}
|
}
|
||||||
|
|
||||||
void TableZone::reorganizeCards()
|
void TableZone::reorganizeCards()
|
||||||
|
|
@ -238,8 +228,8 @@ void TableZone::reorganizeCards()
|
||||||
// Calculate card stack widths so mapping functions work properly
|
// Calculate card stack widths so mapping functions work properly
|
||||||
computeCardStackWidths();
|
computeCardStackWidths();
|
||||||
|
|
||||||
for (int i = 0; i < getLogic()->getCards().size(); ++i) {
|
for (int i = 0; i < cardItems().size(); ++i) {
|
||||||
QPoint gridPoint = getLogic()->getCards()[i]->getGridPos();
|
QPoint gridPoint = cardItems()[i]->getGridPos();
|
||||||
if (gridPoint.x() == -1) {
|
if (gridPoint.x() == -1) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -248,21 +238,24 @@ void TableZone::reorganizeCards()
|
||||||
qreal x = mapPoint.x();
|
qreal x = mapPoint.x();
|
||||||
qreal y = mapPoint.y();
|
qreal y = mapPoint.y();
|
||||||
|
|
||||||
int numberAttachedCards = getLogic()->getCards()[i]->getAttachedCards().size();
|
int numberAttachedCards = cardItems()[i]->getAttachedCards().size();
|
||||||
qreal actualX = x + numberAttachedCards * STACKED_CARD_OFFSET_X;
|
qreal actualX = x + numberAttachedCards * STACKED_CARD_OFFSET_X;
|
||||||
qreal actualY = y;
|
qreal actualY = y;
|
||||||
if (numberAttachedCards) {
|
if (numberAttachedCards) {
|
||||||
actualY += 15;
|
actualY += 15;
|
||||||
}
|
}
|
||||||
|
|
||||||
getLogic()->getCards()[i]->setPos(actualX, actualY);
|
cardItems()[i]->setPos(actualX, actualY);
|
||||||
getLogic()->getCards()[i]->setRealZValue(ZValues::tableCardZValue(actualX, actualY));
|
cardItems()[i]->setRealZValue(ZValues::tableCardZValue(actualX, actualY));
|
||||||
|
|
||||||
QListIterator<CardItem *> attachedCardIterator(getLogic()->getCards()[i]->getAttachedCards());
|
QListIterator<CardState *> attachedCardIterator(cardItems()[i]->getAttachedCards());
|
||||||
int j = 0;
|
int j = 0;
|
||||||
while (attachedCardIterator.hasNext()) {
|
while (attachedCardIterator.hasNext()) {
|
||||||
++j;
|
++j;
|
||||||
CardItem *attachedCard = attachedCardIterator.next();
|
CardItem *attachedCard = qobject_cast<CardItem *>(attachedCardIterator.next()->parent());
|
||||||
|
if (attachedCard == nullptr) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
qreal childX = actualX - j * STACKED_CARD_OFFSET_X;
|
qreal childX = actualX - j * STACKED_CARD_OFFSET_X;
|
||||||
qreal childY = y + 5;
|
qreal childY = y + 5;
|
||||||
attachedCard->setPos(childX, childY);
|
attachedCard->setPos(childX, childY);
|
||||||
|
|
@ -276,35 +269,18 @@ void TableZone::reorganizeCards()
|
||||||
|
|
||||||
void TableZone::toggleTapped()
|
void TableZone::toggleTapped()
|
||||||
{
|
{
|
||||||
QList<QGraphicsItem *> selectedItemsRaw = scene()->selectedItems();
|
QList<CardState *> selectedCards;
|
||||||
QList<QGraphicsItem *> selectedItems;
|
for (auto *item : scene()->selectedItems()) {
|
||||||
|
auto *card = qgraphicsitem_cast<CardItem *>(item);
|
||||||
auto isCardOnTable = [](const QGraphicsItem *item) {
|
if (card && card->getZone()->getName() == ZoneNames::TABLE) {
|
||||||
if (auto card = qgraphicsitem_cast<const CardItem *>(item)) {
|
selectedCards.append(card->getState());
|
||||||
return card->getZone()->getName() == ZoneNames::TABLE;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
std::copy_if(selectedItemsRaw.begin(), selectedItemsRaw.end(), std::back_inserter(selectedItems), isCardOnTable);
|
|
||||||
|
|
||||||
bool tapAll = std::any_of(selectedItems.begin(), selectedItems.end(), [](const QGraphicsItem *item) {
|
|
||||||
return !qgraphicsitem_cast<const CardItem *>(item)->getTapped();
|
|
||||||
});
|
|
||||||
QList<const ::google::protobuf::Message *> cmdList;
|
|
||||||
for (const auto &selectedItem : selectedItems) {
|
|
||||||
CardItem *temp = qgraphicsitem_cast<CardItem *>(selectedItem);
|
|
||||||
if (temp->getTapped() != tapAll) {
|
|
||||||
Command_SetCardAttr *cmd = new Command_SetCardAttr;
|
|
||||||
cmd->set_zone(getLogic()->getName().toStdString());
|
|
||||||
cmd->set_card_id(temp->getId());
|
|
||||||
cmd->set_attribute(AttrTapped);
|
|
||||||
cmd->set_attr_value(tapAll ? "1" : "0");
|
|
||||||
cmdList.append(cmd);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
getLogic()->getPlayer()->getPlayerActions()->sendGameCommand(
|
|
||||||
getLogic()->getPlayer()->getPlayerActions()->prepareGameCommand(cmdList));
|
const bool tapAll = std::any_of(selectedCards.begin(), selectedCards.end(),
|
||||||
|
[](const CardState *card) { return !card->getTapped(); });
|
||||||
|
|
||||||
|
getLogic()->getPlayer()->getPlayerActions()->actToggleTapped(getLogic()->getName(), selectedCards, tapAll);
|
||||||
}
|
}
|
||||||
|
|
||||||
void TableZone::resizeToContents()
|
void TableZone::resizeToContents()
|
||||||
|
|
@ -312,9 +288,9 @@ void TableZone::resizeToContents()
|
||||||
int xMax = 0;
|
int xMax = 0;
|
||||||
|
|
||||||
// Find rightmost card position, which includes the left margin amount.
|
// Find rightmost card position, which includes the left margin amount.
|
||||||
for (int i = 0; i < getLogic()->getCards().size(); ++i) {
|
for (int i = 0; i < cardItems().size(); ++i) {
|
||||||
if (getLogic()->getCards()[i]->pos().x() > xMax) {
|
if (cardItems()[i]->pos().x() > xMax) {
|
||||||
xMax = (int)getLogic()->getCards()[i]->pos().x();
|
xMax = (int)cardItems()[i]->pos().x();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -335,9 +311,9 @@ void TableZone::resizeToContents()
|
||||||
|
|
||||||
CardItem *TableZone::getCardFromGrid(const QPoint &gridPoint) const
|
CardItem *TableZone::getCardFromGrid(const QPoint &gridPoint) const
|
||||||
{
|
{
|
||||||
for (int i = 0; i < getLogic()->getCards().size(); i++) {
|
for (int i = 0; i < cardItems().size(); i++) {
|
||||||
if (getLogic()->getCards().at(i)->getGridPoint() == gridPoint) {
|
if (cardItems().at(i)->getGridPoint() == gridPoint) {
|
||||||
return getLogic()->getCards().at(i);
|
return cardItems().at(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
|
|
@ -354,8 +330,8 @@ void TableZone::computeCardStackWidths()
|
||||||
// Each card stack is three grid points worth of card locations.
|
// Each card stack is three grid points worth of card locations.
|
||||||
// First pass: compute the number of cards at each card stack.
|
// First pass: compute the number of cards at each card stack.
|
||||||
QMap<int, int> cardStackCount;
|
QMap<int, int> cardStackCount;
|
||||||
for (int i = 0; i < getLogic()->getCards().size(); ++i) {
|
for (int i = 0; i < cardItems().size(); ++i) {
|
||||||
const QPoint &gridPoint = getLogic()->getCards()[i]->getGridPos();
|
const QPoint &gridPoint = cardItems()[i]->getGridPos();
|
||||||
if (gridPoint.x() == -1) {
|
if (gridPoint.x() == -1) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -366,8 +342,8 @@ void TableZone::computeCardStackWidths()
|
||||||
|
|
||||||
// Second pass: compute the width at each card stack.
|
// Second pass: compute the width at each card stack.
|
||||||
cardStackWidth.clear();
|
cardStackWidth.clear();
|
||||||
for (int i = 0; i < getLogic()->getCards().size(); ++i) {
|
for (int i = 0; i < cardItems().size(); ++i) {
|
||||||
const QPoint &gridPoint = getLogic()->getCards()[i]->getGridPos();
|
const QPoint &gridPoint = cardItems()[i]->getGridPos();
|
||||||
if (gridPoint.x() == -1) {
|
if (gridPoint.x() == -1) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -375,8 +351,8 @@ void TableZone::computeCardStackWidths()
|
||||||
const int key = getCardStackMapKey(gridPoint.x() / 3, gridPoint.y());
|
const int key = getCardStackMapKey(gridPoint.x() / 3, gridPoint.y());
|
||||||
const int stackCount = cardStackCount.value(key, 0);
|
const int stackCount = cardStackCount.value(key, 0);
|
||||||
if (stackCount == 1) {
|
if (stackCount == 1) {
|
||||||
cardStackWidth.insert(key, CardDimensions::WIDTH + getLogic()->getCards()[i]->getAttachedCards().size() *
|
cardStackWidth.insert(key, CardDimensions::WIDTH +
|
||||||
STACKED_CARD_OFFSET_X);
|
cardItems()[i]->getAttachedCards().size() * STACKED_CARD_OFFSET_X);
|
||||||
} else {
|
} else {
|
||||||
cardStackWidth.insert(key, CardDimensions::WIDTH + (stackCount - 1) * STACKED_CARD_OFFSET_X);
|
cardStackWidth.insert(key, CardDimensions::WIDTH + (stackCount - 1) * STACKED_CARD_OFFSET_X);
|
||||||
}
|
}
|
||||||
|
|
@ -397,7 +373,7 @@ QPointF TableZone::mapFromGrid(QPoint gridPoint) const
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isInverted()) {
|
if (isInverted()) {
|
||||||
gridPoint.setY(TABLEROWS - 1 - gridPoint.y());
|
gridPoint.setY(TableZoneLogic::ROW_COUNT - 1 - gridPoint.y());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start with margin plus stacked card offset
|
// Start with margin plus stacked card offset
|
||||||
|
|
@ -423,10 +399,10 @@ QPoint TableZone::mapToGrid(const QPointF &mapPoint) const
|
||||||
const int gridPointHeight = CardDimensions::HEIGHT + PADDING_Y;
|
const int gridPointHeight = CardDimensions::HEIGHT + PADDING_Y;
|
||||||
int gridPointY = (y + PADDING_Y / 2) / gridPointHeight;
|
int gridPointY = (y + PADDING_Y / 2) / gridPointHeight;
|
||||||
|
|
||||||
gridPointY = clampValidTableRow(gridPointY);
|
gridPointY = TableZoneLogic::clampValidTableRow(gridPointY);
|
||||||
|
|
||||||
if (isInverted()) {
|
if (isInverted()) {
|
||||||
gridPointY = TABLEROWS - 1 - gridPointY;
|
gridPointY = TableZoneLogic::ROW_COUNT - 1 - gridPointY;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculating the x-coordinate of the grid space requires adding up the
|
// Calculating the x-coordinate of the grid space requires adding up the
|
||||||
|
|
@ -471,22 +447,3 @@ QPointF TableZone::closestGridPoint(const QPointF &point)
|
||||||
}
|
}
|
||||||
return mapFromGrid(gridPoint);
|
return mapFromGrid(gridPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
int TableZone::clampValidTableRow(const int row)
|
|
||||||
{
|
|
||||||
if (row < 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (row >= TABLEROWS) {
|
|
||||||
return TABLEROWS - 1;
|
|
||||||
}
|
|
||||||
return row;
|
|
||||||
}
|
|
||||||
|
|
||||||
int TableZone::tableRowToGridY(int tableRow)
|
|
||||||
{
|
|
||||||
if (tableRow > 2) {
|
|
||||||
tableRow = 1;
|
|
||||||
}
|
|
||||||
return clampValidTableRow(2 - tableRow);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
#include <QPoint>
|
#include <QPoint>
|
||||||
#include <QPointF>
|
#include <QPointF>
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
|
#include <libcockatrice/game/zones/table_zone_logic.h>
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
|
|
||||||
class CardDragItem;
|
class CardDragItem;
|
||||||
|
|
@ -25,7 +26,6 @@ class CardZoneLogic;
|
||||||
class QColor;
|
class QColor;
|
||||||
class QGraphicsItem;
|
class QGraphicsItem;
|
||||||
class QPainter;
|
class QPainter;
|
||||||
class TableZoneLogic;
|
|
||||||
template <typename T> class QList;
|
template <typename T> class QList;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -42,8 +42,6 @@ signals:
|
||||||
void sizeChanged();
|
void sizeChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static const int TABLEROWS = 3;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Margins between table edges and cards, paddings between cards
|
Margins between table edges and cards, paddings between cards
|
||||||
*/
|
*/
|
||||||
|
|
@ -181,15 +179,6 @@ public:
|
||||||
|
|
||||||
QPointF closestGridPoint(const QPointF &point) override;
|
QPointF closestGridPoint(const QPointF &point) override;
|
||||||
|
|
||||||
static int clampValidTableRow(const int row);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a card's logical table row (0=creatures, 1=noncreatures, 2=lands)
|
|
||||||
* to the corresponding grid Y coordinate. Cards with tableRow > 2 (e.g.,
|
|
||||||
* instants/sorceries) default to the noncreatures row.
|
|
||||||
*/
|
|
||||||
static int tableRowToGridY(int tableRow);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Resizes the TableZone in case CardItems are within or
|
Resizes the TableZone in case CardItems are within or
|
||||||
outside of the TableZone constraints.
|
outside of the TableZone constraints.
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue