Compare commits

..

9 commits

Author SHA1 Message Date
Lukas Brübach
1407a2e142 [Fonts] Keep CardText call sites on the app font and fix px sizes
- CardText's unset fallback is qApp->font() again: only the three counter
  painters (paintNumberEllipse, GeneralCounter, PlayerCounter) were Serif
  before the role existed, and they now ask for it through
  ThemeManager::cardTextFont(). Card names, player names, counters, the
  overlay, the Archidekt and EDHREC labels and the completer delegate keep the
  app font they used before.
- Use setPixelSize for the EDHREC card name (20px) and commander name (28px)
  and details (16px) sizes: the stylesheets said px, and setPointSize made
  them about a third larger.
2026-11-01 14:12:35 +01:00
Lukas Brübach
ee2e0a9fa0 [Fonts] Migrate CardText surfaces to theme font role 2026-11-01 14:06:33 +01:00
Lukas Brübach
145f7adf73 [Fonts] Keep ChatView on the app font and share the report log font
- ChatLog's unset fallback is qApp->font() again: ChatView never set a font
  before the role existed, so an unset role must not switch chat and the game
  log to monospace at the system GeneralFont size.
- Move the monospace fallback the report dialogs used before into
  report_utils::reportLogFont(), which returns the theme's ChatLog role when it
  is set and the old monospace otherwise. tab_report, dlg_my_reports and
  dlg_report_user now share it, so they cannot drift and they honour a themed
  size instead of overriding it.
2026-11-01 14:06:05 +01:00
Lukas Brübach
2a3a31c24c [Fonts] Migrate ChatLog surfaces to theme font role 2026-11-01 13:59:37 +01:00
Lukas Brübach
ecc3ecd4ea [Fonts] Make the editor's fallback and Reset preview-independent
- Add ThemeManager::defaultAppFont(), which resolves an unset role against the
  startup font instead of currentFonts, and use it in FontGridWidget::loadFonts
  so the grid never shows the live preview as the theme's default.
- Reload the live theme in PaletteEditorDialog::onReset before re-reading the
  grid, so Reset restores the saved fonts before loadFonts resolves fallbacks.
2026-11-01 13:59:16 +01:00
Lukas Brübach
c0aeaf72e8 [Fonts] Add per-purpose font section to palette editor 2026-11-01 13:53:56 +01:00
Lukas Brübach
c63a359c3d [Fonts] Restore the startup font when a config has no InterfaceDefault
- Capture the untouched application font next to the default palette and use
  it in previewFonts' else branch, so a config without InterfaceDefault
  restores the startup font instead of leaving the last preview applied.
- Drop the unpolish/polish loop from previewFonts: QApplication::setFont
  already notifies every widget, and the loop ran on every editor auto-apply.
2026-11-01 13:53:21 +01:00
Lukas Brübach
58ee72543a [Fonts] Wire FontConfig into ThemeManager with appFont and preview 2026-10-16 09:03:59 +02:00
Lukas Brübach
1385b69bac [Fonts] Add AppFont roles and FontConfig fonts.toml round-trip 2026-10-16 09:00:57 +02:00
579 changed files with 51699 additions and 154696 deletions

View file

@ -3,13 +3,11 @@ FROM debian:12
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
build-essential \
ca-certificates \
ccache \
cmake \
file \
g++ \
git \
libcurl4-openssl-dev \
libgl-dev \
liblzma-dev \
libmariadb-dev-compat \

View file

@ -9,7 +9,6 @@ RUN apt-get update && \
file \
g++ \
git \
libcurl4-openssl-dev \
libgl-dev \
liblzma-dev \
libmariadb-dev-compat \

View file

@ -1,12 +1,10 @@
FROM fedora:43
RUN dnf install -y \
ca-certificates \
ccache \
cmake \
gcc-c++ \
git \
libcurl-devel \
mariadb-devel \
ninja-build \
openssl-devel \

View file

@ -1,12 +1,10 @@
FROM fedora:44
RUN dnf install -y \
ca-certificates \
ccache \
cmake \
gcc-c++ \
git \
libcurl-devel \
mariadb-devel \
ninja-build \
openssl-devel \

View file

@ -3,13 +3,11 @@ FROM ubuntu:24.04
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
build-essential \
ca-certificates \
ccache \
cmake \
file \
g++ \
git \
libcurl4-openssl-dev \
libgl-dev \
liblzma-dev \
libmariadb-dev-compat \

View file

@ -10,7 +10,6 @@ RUN apt-get update && \
g++ \
gcovr \
git \
libcurl4-openssl-dev \
libgl-dev \
liblzma-dev \
libmariadb-dev-compat \

View file

@ -200,20 +200,6 @@ if [[ $USE_VCPKG ]]; then
flags+=("-DUSE_VCPKG=1")
flags+=("-DVCPKG_INSTALL_OPTIONS=--x-abi-tools-use-exact-versions")
fi
if [[ ${WITH_CRASH_REPORTING:-} == "ON" ]]; then
# Release CI opts into the sentry-native SDK (default is OFF so from-source, distro and
# offline package builds never fetch it). Debug builds stay SDK-free so crashes from
# developer builds never reach Sentry.
flags+=("-DWITH_CRASH_REPORTING=ON")
fi
if [[ ${BUILDTYPE:-Release} == "Release" && -n ${COCKATRICE_SENTRY_DSN:-} ]]; then
# Release-only: Debug builds stay DSN-free so crashes from developer builds
# never reach Sentry. Empty (local builds, forks without the secret, and non-tag
# CI builds, which never receive the secret) keeps today's mock fallback via the
# compiled-in default. CI passes the value from the SENTRY_DSN secret on tag
# Release builds only, so pre-release test artifacts never report to Sentry.
flags+=("-DCOCKATRICE_SENTRY_DSN=$COCKATRICE_SENTRY_DSN")
fi
# Add cmake --build flags
buildflags=(--config "$BUILDTYPE")
@ -311,24 +297,16 @@ if [[ $RUNNER_OS == macOS ]]; then
if [[ $MAKE_PACKAGE ]]; then
# Workaround https://github.com/actions/runner-images/issues/7522
# 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.
# have hdiutil repeat the command 10 times in hope of success
hdiutil_script="/tmp/hdiutil.sh"
# shellcheck disable=SC2016
echo '#!/bin/bash
attempts=10
i=0
while true; do
hdiutil "$@" && exit 0
rc=$?
if (( ++i >= attempts )); then
echo "::error::hdiutil $1 failed $i times (last status $rc), giving up" >&2
exit "$rc"
while ! hdiutil "$@"; do
if (( ++i >= 10 )); then
echo "Error: hdiutil failed $i times!" >&2
break
fi
echo "hdiutil $1 attempt $i/$attempts failed (status $rc), retrying" >&2
sleep 1
done' >"$hdiutil_script"
chmod +x "$hdiutil_script"
@ -467,206 +445,6 @@ if [[ $MAKE_COVERAGE ]]; then
echo "::endgroup::"
fi
if [[ $RUNNER_OS == Windows && ! $MAKE_NO_CLIENT ]]; then
echo "::group::Probe the installer's holder lookup"
# The working directory is still the build dir here: the package steps below are what
# leave and re-enter it, and that is what made a $BUILD_DIR-prefixed path name
# build/build. Both paths are relative to the dir the probe was built into.
probe="./installer-holder-probe.exe"
if [[ ! -f $probe ]]; then
echo "::error file=$0::installer-holder-probe.exe was not built"
exit 1
fi
# The probe exists to name a holder that a process-and-module scan cannot see: one
# that has a file open without having loaded it. Holding a temporary DLL with
# FileShare.None is exactly that case, so this asserts the reason the probe was
# added instead of only that the Restart Manager answers at all. It needs no NSIS
# and no packaging, so it runs on every Windows build that gets this far.
# The holder is a process of its own: the probe never names the process that started it,
# which is what keeps an in-place uninstaller from being named as a holder of the
# directory it is emptying. The same DLL held by the script that starts the probe
# therefore has to come back as nothing, and that is the second case below.
probe_check="./holder-probe-check.ps1"
cat >"$probe_check" <<'POWERSHELL'
$ErrorActionPreference = "Stop"
$dir = Join-Path ([IO.Path]::GetTempPath()) "cockatrice-holder-probe-$PID"
Remove-Item -Recurse -Force $dir -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force $dir | Out-Null
$probe = (Resolve-Path -LiteralPath $env:PROBE_EXE).Path
function Invoke-Probe([string] $guardDir) {
$start = New-Object System.Diagnostics.ProcessStartInfo
$start.FileName = $probe
$start.UseShellExecute = $false
$start.RedirectStandardOutput = $true
$start.EnvironmentVariables["COCKATRICE_GUARD_DIR"] = $guardDir
$process = [System.Diagnostics.Process]::Start($start)
$read = $process.StandardOutput.ReadToEndAsync()
if (-not $process.WaitForExit(20000)) {
try { $process.Kill() } catch { }
throw "the holder probe did not finish within 20 s for $guardDir"
}
$output = $read.Result
$code = $process.ExitCode
$process.Dispose()
return @{ Code = $code; Output = $output }
}
function Wait-UntilLocked([string] $path) {
$deadline = (Get-Date).AddSeconds(15)
while ((Get-Date) -lt $deadline) {
try {
$open = [IO.File]::Open($path, "Open", "ReadWrite", "None")
$open.Dispose()
} catch {
return
}
Start-Sleep -Milliseconds 100
}
throw "the holder never locked $path"
}
try {
$file = Join-Path $dir "locked.dll"
[IO.File]::WriteAllText($file, "x")
# The holder is a process of its own, not this script: the probe never names the process
# that started it (the case below asserts that), so a file held here would be left out on
# purpose and the check would pass for the wrong reason.
$holderScript = Join-Path $dir "hold.ps1"
Set-Content -LiteralPath $holderScript -Value @'
param([string] $Path)
$handle = [IO.File]::Open($Path, "Open", "ReadWrite", "None")
Start-Sleep -Seconds 120
'@
$powershell = Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0\powershell.exe"
$holder = Start-Process -FilePath $powershell -PassThru -ArgumentList @(
"-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "`"$holderScript`"", "-Path", "`"$file`"")
try {
Wait-UntilLocked $file
$locked = Invoke-Probe $dir
if ($locked.Code -ne 1) { throw "expected exit 1 for a locked file, got $($locked.Code) : $($locked.Output)" }
if ($locked.Output -notmatch "pid\s+$($holder.Id)\b") {
throw "the lookup did not name the process holding the file: $($locked.Output)"
}
} finally {
try { $holder.Kill() } catch { }
try { $holder.WaitForExit(5000) | Out-Null } catch { }
}
# The probe is started by the installer, or by an uninstaller that runs in place from the
# directory it is emptying, so the asking process holds files there itself. Naming it would
# ask the user to close the program that is asking - a question neither the close nor the
# end offer can answer, so the guard repeated it until the update was cancelled. A holder
# that is only the asking process has to come back as nothing.
$handle = [IO.File]::Open($file, "Open", "ReadWrite", "None")
try {
$self = Invoke-Probe $dir
} finally {
$handle.Dispose()
}
if ($self.Code -ne 0) { throw "expected exit 0 when the asking process is the only holder, got $($self.Code) : $($self.Output)" }
if ($self.Output -match "pid\s+$PID\b") { throw "the lookup named the process that started it: $($self.Output)" }
# A payload executable that refuses to be replaced is running, whether or not this probe
# may name it: the Restart Manager does not report every mapping, and the image path of a
# process under another account cannot be read from here, so both process lookups can
# come back empty while the program is still there. Held here by the asking process, so
# that it cannot be named - the answer has to be "could not name it", never "nothing is
# holding it", which is what sent an update into an uninstall that could only fail
# halfway. Exit 3 is the code the guard answers by asking the user to close it and retry.
$image = Join-Path $dir "oracle.exe"
[IO.File]::WriteAllText($image, "x")
$handle = [IO.File]::Open($image, "Open", "ReadWrite", "None")
try {
$unnameable = Invoke-Probe $dir
} finally {
$handle.Dispose()
}
if ($unnameable.Code -ne 3) {
throw "expected exit 3 for a payload executable that is in use but cannot be named, got $($unnameable.Code) : $($unnameable.Output)"
}
if ($unnameable.Output -notmatch "oracle\.exe is in use") {
throw "the answer did not name the executable it could not replace: $($unnameable.Output)"
}
$empty = Join-Path $dir "empty"
New-Item -ItemType Directory -Force $empty | Out-Null
$nothing = Invoke-Probe $empty
if ($nothing.Code -ne 0) { throw "expected exit 0 for a directory with nothing open, got $($nothing.Code) : $($nothing.Output)" }
# A process running from the directory is a holder even when it has no file open: an
# instance holds its payload as mapped images, not as handles, and that is the state
# the guard used to miss - it passed, the purge failed, and the install ended in a
# warning with no offer to close and try again. The image is a copy of a native
# Windows program, so it starts without its usual directory next to it, and it carries
# one of the payload's own names because that is what the lookup names.
$copy = Join-Path $dir "cockatrice.exe"
Copy-Item -LiteralPath (Join-Path $env:SystemRoot "System32\cmd.exe") -Destination $copy
$running = Start-Process -FilePath $copy -ArgumentList "/c", "ping -n 20 127.0.0.1 >nul" -PassThru
try {
Start-Sleep -Milliseconds 1500
$byImage = Invoke-Probe $dir
} finally {
try { $running.Kill() } catch { }
# Wait for the image to be released, or the cleanup below leaves the copy behind.
try { $running.WaitForExit(5000) | Out-Null } catch { }
}
if ($byImage.Code -ne 1) { throw "expected exit 1 for a process running from the directory, got $($byImage.Code) : $($byImage.Output)" }
if ($byImage.Output -notmatch "cockatrice\.exe \(pid\s+$($running.Id)") {
throw "the lookup did not name the process running from the directory: $($byImage.Output)"
}
# A program in a folder below the directory is not an instance of the installation, even
# when it carries a payload program's name: the install directory can be a folder the
# user keeps other applications in, each in its own subfolder, and naming one of those
# asked for a close the probe never sends and an end it refuses to perform, so the guard
# could only abort the update. A program that really does hold a payload file is still
# named, by the Restart Manager lookup exercised above.
$nested = Join-Path $dir "other"
New-Item -ItemType Directory -Force $nested | Out-Null
$nestedImage = Join-Path $nested "cockatrice.exe"
Copy-Item -LiteralPath (Join-Path $env:SystemRoot "System32\cmd.exe") -Destination $nestedImage
$nestedRunning = Start-Process -FilePath $nestedImage -ArgumentList "/c", "ping -n 20 127.0.0.1 >nul" -PassThru
try {
Start-Sleep -Milliseconds 1500
$inSubfolder = Invoke-Probe $dir
} finally {
try { $nestedRunning.Kill() } catch { }
try { $nestedRunning.WaitForExit(5000) | Out-Null } catch { }
}
if ($inSubfolder.Code -ne 0) {
throw "expected exit 0 for a program in a folder below the directory, got $($inSubfolder.Code) : $($inSubfolder.Output)"
}
# A program the user keeps in the directory itself is not a payload program either: the
# guard ends only what the payload installs, and naming anything else asks for an answer
# that cannot free a single file.
$foreign = Join-Path $dir "tool.exe"
Copy-Item -LiteralPath (Join-Path $env:SystemRoot "System32\cmd.exe") -Destination $foreign
$foreignRunning = Start-Process -FilePath $foreign -ArgumentList "/c", "ping -n 20 127.0.0.1 >nul" -PassThru
try {
Start-Sleep -Milliseconds 1500
$ignored = Invoke-Probe $dir
} finally {
try { $foreignRunning.Kill() } catch { }
try { $foreignRunning.WaitForExit(5000) | Out-Null } catch { }
}
if ($ignored.Code -ne 0) {
throw "expected exit 0 for a program the payload does not install, got $($ignored.Code) : $($ignored.Output)"
}
Write-Host "holder lookup named pid $($holder.Id) for a file another process holds, named nothing for a file only the asking process holds, reported a payload executable it could not name as in use instead of free, named nothing for an empty directory, named pid $($running.Id) for a payload program running from the directory, and named nothing for a payload program in a folder below it or for a program the payload does not install"
} finally {
Remove-Item -Recurse -Force $dir -ErrorAction SilentlyContinue
}
POWERSHELL
if ! PROBE_EXE="$probe" powershell -NoProfile -File "$probe_check"; then
rm -f "$probe_check"
exit 1
fi
rm -f "$probe_check"
echo "::endgroup::"
fi
if [[ $MAKE_INSTALL ]]; then
echo "::group::Install"
cmake --build . --target install --config "$BUILDTYPE"

View file

@ -175,12 +175,6 @@ function RUN ()
args+=(--mount "type=bind,source=$GITHUB_OUTPUT,target=/gh_output")
args+=(--env "GITHUB_OUTPUT=/gh_output")
fi
# The container does not inherit host env vars: without this, WITH_CRASH_REPORTING=ON set in
# the workflow never reaches compile.sh inside the container, so the Linux packages silently
# build with crash reporting OFF while the libcurl packages in the Dockerfiles go unused.
if [[ ${WITH_CRASH_REPORTING:-} ]]; then
args+=(--env "WITH_CRASH_REPORTING=${WITH_CRASH_REPORTING}")
fi
# shellcheck disable=2086
docker run "${args[@]}" $RUN_ARGS "$IMAGE_NAME" bash "$BUILD_SCRIPT" $RUN_OPTS "$@"
return $?

View file

@ -60,10 +60,8 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
ca-certificates \
cmake \
g++ \
libcurl4-openssl-dev \
libgl-dev \
liblzma-dev \
libmariadb-dev-compat \

View file

@ -46,11 +46,6 @@ jobs:
outputs:
tag: ${{ steps.configure.outputs.tag }}
sha: ${{ steps.configure.outputs.sha }}
# MAJ.MIN.PATCH[-beta[.N]] of a tag build; empty otherwise. Mirrors what
# cmake/getversion.cmake derives as PROJECT_VERSION, which the client bakes as its
# Sentry release (cmake/CrashReporting.cmake) — symbol uploads must use the same
# string so stack traces de-symbolicate against this build's binaries.
sentry_release: ${{ steps.configure.outputs.sentry_release }}
steps:
- name: "Configure"
@ -61,9 +56,6 @@ jobs:
run: |
if [[ "$GITHUB_REF_TYPE" == 'tag' ]]; then # release
echo "tag=$GITHUB_REF_NAME" >> "$GITHUB_OUTPUT"
# Tag format is YYYY-MM-DD-{Release|Development}-MAJ.MIN.PATCH[-beta[.N]];
# PROJECT_VERSION is the tail after the type (see cmake/getversion.cmake).
echo "sentry_release=$(sed -E 's/^[0-9]{4}-[0-9]{2}-[0-9]{2}-(Release|Development)-//' <<< "$GITHUB_REF_NAME")" >> "$GITHUB_OUTPUT"
fi
echo "sha=$RESOLVED_SHA" >> "$GITHUB_OUTPUT"
@ -101,36 +93,6 @@ jobs:
--title "$release_name" \
--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:
strategy:
fail-fast: false
@ -263,7 +225,7 @@ jobs:
- name: "Upload coverage artifact"
if: matrix.coverage == 'yes'
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
@ -277,12 +239,6 @@ jobs:
SUFFIX: '-${{ matrix.distro }}${{ matrix.version }}'
package: '${{ matrix.package }}'
server_only: '${{ matrix.server_only }}'
WITH_CRASH_REPORTING: 'ON'
# Tag builds only: artifacts from master pushes and PR branches must not report to
# Sentry. Forks get no secrets on pull_request, so they keep the mock fallback either way.
# configure.outputs.tag is set for ANY tag, so beta (Development) tags get the DSN
# and the symbol upload below just like full releases.
COCKATRICE_SENTRY_DSN: ${{ needs.configure.outputs.tag != null && secrets.SENTRY_DSN || '' }}
run: |
source .ci/docker.sh
args=()
@ -291,58 +247,9 @@ jobs:
args+=(--ccache "$CCACHE_SIZE")
args+=(--cmake-generator "$CMAKE_GENERATOR")
args+=(--suffix "$SUFFIX")
# The container inherits no environment, so the DSN rides in explicitly. -e without
# =value passes it through (possibly empty); compile.sh only consumes it for Release
# configures, and empty keeps the mock fallback.
export RUN_ARGS="${RUN_ARGS:-} -e COCKATRICE_SENTRY_DSN"
RUN --server --release --package "$package" "${args[@]}"
- name: "Install sentry-cli"
if: matrix.package != 'skip' && needs.configure.outputs.tag != null
# Downloads from Sentry's release registry: a Sentry outage or a broken install under
# Git Bash must not take down the release assets below.
continue-on-error: true
shell: bash
env:
SENTRY_CLI_VERSION: '3.8.0'
run: |
export INSTALL_DIR="$GITHUB_WORKSPACE/.sentry-cli"
curl -sL https://sentry.io/get-cli/ | bash
echo "$GITHUB_WORKSPACE/.sentry-cli" >> "$GITHUB_PATH"
- name: "Upload debug symbols to Sentry"
# Like the DSN above, this runs for every tag — beta (Development) tags included.
if: matrix.package != 'skip' && needs.configure.outputs.tag != null
# A Sentry outage or rotated token must not take down the release assets below.
continue-on-error: true
shell: bash
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
run: |
paths=()
# The binaries only, not the directories: a directory makes sentry-cli walk the
# whole build tree, where it finds (and uploads) object files and other fragments
# that can never match a stack frame. The packaged copy is split and stripped by
# cpack, but keeps this binary's GNU build-id, which is how Sentry matches it.
for app in cockatrice oracle servatrice; do
binary="build/$app/$app"
[[ -f $binary ]] && paths+=("$binary")
done
if [[ ${#paths[@]} == 0 ]]; then
# Fail loudly instead of skipping: see the Windows/macOS leg below — this step
# is the only way symbols reach Sentry and `continue-on-error` keeps the job
# green either way, so a silent skip looks exactly like a successful upload.
echo "::error::No debug symbol files found; nothing would be uploaded"
exit 1
fi
# Project slug on Sentry; must match the project the baked DSN points at.
# No --release here: `debug-files upload` has no such option (sentry-cli errors on
# the unknown flag and uploads nothing), and debug information files are matched to
# events by debug ID via the event's debug_meta, not by release. The release grouping
# comes from the client event itself (COCKATRICE_SENTRY_RELEASE).
sentry-cli debug-files upload --org cockatrice --project cockatrice-desktop "${paths[@]}"
# Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342
- name: "Delete remote compiler cache (ccache)"
if: github.ref == 'refs/heads/master' && steps.restore_ccache.outputs.cache-hit
@ -496,7 +403,7 @@ jobs:
vcpkg_upload: 1 # only one leg per vcpkg_scope hands its archives over
name: ${{ matrix.os }} ${{ matrix.target }}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }}
needs: [configure, vcpkg-baseline]
needs: configure
runs-on: ${{ matrix.runner }}
timeout-minutes: 100
env:
@ -614,17 +521,11 @@ jobs:
# job. A scope nobody has written to yet therefore leaves the directory missing
# and fails the job, which is exactly the state every scope starts in. Make the
# directory up front so an empty scope is a cache miss instead of an error.
# always() covers the cache step itself failing: the action's post save still runs
# then, and it should report the real error instead of ENOENT. It cannot rescue a
# failure before the cache step, since a post step only runs for an action whose
# main step did. The fallback keeps this guard from failing when the cache step was
# skipped and left no path output.
- name: "Create vcpkg cache directory"
if: always()
shell: bash
env:
CACHE_DIR: '${{ steps.vcpkg-cache.outputs.path }}'
run: mkdir -p "${CACHE_DIR:-$GITHUB_WORKSPACE/.vcpkg-cache}"
run: mkdir -p "$CACHE_DIR"
# Uses environment variables, see compile.sh for more details
- name: "Build Cockatrice"
@ -634,11 +535,6 @@ jobs:
BUILDTYPE: '${{ matrix.type }}'
CCACHE_EVICTION_AGE: ${{ matrix.ccache_eviction_age }}
CMAKE_GENERATOR: ${{ matrix.cmake_generator }}
# Tag builds only: artifacts from master pushes and PR branches must not report to
# Sentry. Forks get no secrets on pull_request, so they keep the mock fallback either way.
# configure.outputs.tag is set for ANY tag, so beta (Development) tags get the DSN
# and the symbol upload below just like full releases.
COCKATRICE_SENTRY_DSN: ${{ needs.configure.outputs.tag != null && secrets.SENTRY_DSN || '' }}
DEVELOPER_DIR: '/Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer'
GITHUB_TOKEN: ${{ github.token }} # needed for vcpkg dependency graph updates, see VCPKG_FEATURE_FLAGS
MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }}
@ -662,125 +558,8 @@ jobs:
VCPKG_DISABLE_METRICS: 1
VCPKG_FEATURE_FLAGS: dependencygraph
VCPKG_TARGET_MACOS_VERSION: ${{ matrix.vcpkg_deployment_target }}
# Release legs compile the sentry-native SDK; Debug legs stay SDK-free.
WITH_CRASH_REPORTING: ${{ matrix.type == 'Release' && 'ON' || '' }}
run: .ci/compile.sh --server --test --vcpkg
- name: "Install sentry-cli"
if: needs.configure.outputs.tag != null && matrix.make_package == '1'
# Same as the Linux leg above: the install downloads from Sentry's registry, so a
# failure here must not block the release upload below.
continue-on-error: true
shell: bash
env:
SENTRY_CLI_VERSION: '3.8.0'
run: |
export INSTALL_DIR="$GITHUB_WORKSPACE/.sentry-cli"
curl -sL https://sentry.io/get-cli/ | bash
echo "$GITHUB_WORKSPACE/.sentry-cli" >> "$GITHUB_PATH"
- name: "Upload debug symbols to Sentry"
# Like the DSN above, this runs for every tag — beta (Development) tags included.
if: needs.configure.outputs.tag != null && matrix.make_package == '1'
# A Sentry outage or rotated token must not take down the release assets below.
continue-on-error: true
shell: bash
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
run: |
paths=()
if [[ '${{ matrix.os }}' == 'Windows' ]]; then
# Top-level CMakeLists.txt links /DEBUG even in release, so the linker PDBs
# land next to the binaries. Ninja is single-config, so that is
# build/<target>/ and NOT build/<target>/Release/ — the same globs the symbol
# artifact step below uses.
#
# The executables are uploaded as well, in the same invocation. On 64-bit
# Windows the Call Frame Information (.pdata/.xdata) lives in the PE and is
# not copied into the PDB, so a PDB on its own leaves Sentry reporting
# "Stack Unwinding: Missing" for the image. sentry-cli also needs to see the
# PE and its PDB together: only then does it associate the PE's code
# identifier with the PDB's debug identifier.
shopt -s nullglob
for target in cockatrice oracle servatrice; do
paths+=("build/$target"/*.exe "build/$target"/*.pdb)
done
shopt -u nullglob
# Qt's own runtime DLLs, taken from required-runtime.txt — the manifest
# .ci/compile.sh asserts the installer against — so the set uploaded is
# the set an install actually carries. The PE is the only carrier of
# unwind information for a 64-bit image, and sending it in the same
# invocation as a PDB is what lets sentry-cli associate the PE's code
# identifier with the PDB's debug identifier. Qt ships no PDBs with the
# SDK, so Qt frames resolve through the export table: names, no lines.
qt_dir=
qt_core_dir=
# Existence guards matter: the step runs under `bash -eo pipefail`, so a
# redirection-less sed over a missing file would abort the step outright
# and continue-on-error would hide it.
if [[ -f build/CMakeCache.txt ]]; then
qt_core_dir=$(sed -n 's/^Qt6Core_DIR:PATH=//p' build/CMakeCache.txt | head -1)
fi
if [[ -n $qt_core_dir ]]; then
qt_dir=$(dirname "$(dirname "$(dirname "$qt_core_dir")")")
fi
if [[ -z $qt_dir || ! -d $qt_dir ]]; then
# Warnings, not failures: the upload of our own binaries is what this
# step is for, and continue-on-error already means an upload failure
# cannot be told apart from a successful one.
echo "::warning::Qt runtime directory not resolved from build/CMakeCache.txt; Qt images get no unwind information"
elif [[ ! -f build/required-runtime.txt ]]; then
echo "::warning::build/required-runtime.txt is missing; cannot tell which Qt DLLs ship"
else
while IFS= read -r runtime; do
name=${runtime##*/}
if [[ $runtime == */* ]]; then
# A Qt plugin: the install root keeps it as plugins/<type>/<name>.
# `|| true`: an unmatched glob makes ls exit non-zero, and pipefail
# turns that into a step abort.
runtime_path=$(ls "$qt_dir/plugins"/*/"$name" 2>/dev/null | head -1) || true
else
runtime_path="$qt_dir/bin/$name"
fi
if [[ -f ${runtime_path:-} ]]; then
paths+=("$runtime_path")
else
echo "::warning::shipped Qt runtime $runtime not found under $qt_dir"
fi
done < <(grep -E '^(Qt6[^/]*\.dll|Plugins/)' build/required-runtime.txt)
fi
else
# Release builds carry DWARF (-g, kept by CrashReporting.cmake); macOS keeps its
# debug info in dSYM bundles, so materialize them before uploading.
for app in cockatrice oracle servatrice; do
binary="build/$app/$app.app/Contents/MacOS/$app"
if [[ -f $binary ]]; then
if [[ '${{ matrix.os }}' == 'macOS' ]]; then
dsymutil "$binary" -o "$binary.dSYM"
paths+=("$binary.dSYM")
# The dSYM holds the debug information, but the unwind information
# Sentry needs to walk a minidump stack lives in the Mach-O itself.
paths+=("$binary")
else
paths+=("$binary")
fi
fi
done
fi
if [[ ${#paths[@]} == 0 ]]; then
# Fail loudly instead of skipping: this step is the only way symbols reach
# Sentry, and `continue-on-error` keeps the job green either way, so a
# silent skip is indistinguishable from a successful upload. It also hid a
# wrong Windows path (build/<target>/Release, which Ninja never creates).
echo "::error::No debug symbol files found; nothing would be uploaded"
exit 1
fi
# Project slug on Sentry; must match the project the baked DSN points at.
# No --release: see the Linux leg above — the flag does not exist on
# `debug-files upload` and DIFs are matched by debug ID, not release.
sentry-cli debug-files upload --org cockatrice --project cockatrice-desktop "${paths[@]}"
# CPack reports a failed makensis run as "Problem running NSIS command"
# plus a path to NSISOutput.log, and that log lives only on the runner, so
# the actual compile error is otherwise nowhere in the workflow output.
@ -924,6 +703,19 @@ jobs:
if-no-files-found: error
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"
if: needs.configure.outputs.tag != null && matrix.make_package == '1'
id: upload_release
@ -935,33 +727,6 @@ jobs:
tag_name: ${{ needs.configure.outputs.tag }}
run: gh release upload "$tag_name" "$asset_path#$asset_name"
# Ninja is single-config: binaries and their PDBs land directly in the
# target directories instead of a Release subdirectory.
#
# Archived for tag builds only, and ordered after the release upload so
# a missing PDB can never block a release from being published. On a tag
# build this artifact is the only durable copy of the symbols: the Sentry
# upload is continue-on-error, so without it a failed upload would lose
# the ability to symbolicate that release permanently. Other builds upload
# no symbols at all, so archiving them there would only cost storage.
#
# The executables travel with their PDBs: re-uploading the pair together is
# what lets sentry-cli associate the PE's code identifier with the PDB's
# debug identifier, and the PE is what carries the unwind information.
- name: "[Windows] Upload debug symbols (exe + pdb)"
if: matrix.os == 'Windows' && needs.configure.outputs.tag != null
uses: actions/upload-artifact@v7
with:
if-no-files-found: error
name: ${{ steps.build.outputs.name }}-Symbols
path: |
build/cockatrice/*.exe
build/cockatrice/*.pdb
build/oracle/*.exe
build/oracle/*.pdb
build/servatrice/*.exe
build/servatrice/*.pdb
- name: "Attest binary provenance"
if: steps.upload_release.outcome == 'success'
id: attestation

View file

@ -1,16 +0,0 @@
# Integration Docs: https://help.transifex.com/en/articles/6265125-github-installation-and-configuration#h_de29ef5cec
# Config Parameter: https://help.transifex.com/en/articles/6265125-github-installation-and-configuration#h_a734c3e219
# Supported Files: https://help.transifex.com/en/collections/3519116-file-formats
filters:
- filter_type: file
file_format: QT
source_language: en
source_file: cockatrice/cockatrice_en@source.ts
translation_files_expression: cockatrice/translations/cockatrice_<lang>.ts
- filter_type: file
file_format: QT
source_language: en
source_file: oracle/oracle_en@source.ts
translation_files_expression: oracle/translations/oracle_<lang>.ts

View file

@ -90,7 +90,7 @@ endif()
# A project name is needed for CPack
# Version can be overriden by git tags, see cmake/getversion.cmake
project("Cockatrice" VERSION 3.2.0)
project("Cockatrice" VERSION 3.1.2)
# Set release name if not provided via env/cmake var
if(NOT DEFINED GIT_TAG_RELEASENAME)
@ -220,7 +220,6 @@ endif()
# when ENABLE_SANITIZERS or ENABLE_COVERAGE is on.
include(Sanitizers)
include(Coverage)
include(CrashReporting)
# GNU systems need to define the Mersenne exponent for the RNG to compile w/o warning
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
@ -250,22 +249,7 @@ message(STATUS "Update Translations: ${UPDATE_TRANSLATIONS}")
include(FindQtRuntime)
# Reject APIs that were deprecated before the oldest Qt release we support
# (6.4). Qt 6.5 renamed the switch, so use whichever spelling the found
# version understands while still compiling everywhere.
if(Qt6_VERSION VERSION_GREATER_EQUAL 6.5)
add_compile_definitions(QT_DISABLE_DEPRECATED_UP_TO=0x060400)
else()
add_compile_definitions(QT_DISABLE_DEPRECATED_BEFORE=0x060400)
endif()
# Set 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_AUTOUIC TRUE)
set(CMAKE_AUTORCC TRUE)
# Find other needed libraries
find_package(Protobuf CONFIG)
@ -343,7 +327,7 @@ if(UNIX)
endif()
elseif(WIN32)
set(CPACK_GENERATOR NSIS ${CPACK_GENERATOR})
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
if("${CMAKE_GENERATOR_PLATFORM}" MATCHES "(x64)")
set(TRICE_IS_64_BIT 1)
else()
set(TRICE_IS_64_BIT 0)
@ -352,15 +336,6 @@ elseif(WIN32)
# Configure file with custom definitions for NSIS.
configure_file("${COCKATRICE_CMAKE_PATH}/NSIS.definitions.nsh.in" "${PROJECT_BINARY_DIR}/NSIS.definitions.nsh")
# Installer-log sender for the AbortInstall Sentry hook (values parsed out of COCKATRICE_SENTRY_DSN in
# cmake/CrashReporting.cmake). Generated only when a DSN was parsed: cmake/NSIS.template.in embeds it under
# the same CPACK_SENTRY_ENABLED check, so a build without reporting never references or carries the helper.
if(CPACK_SENTRY_ENABLED)
configure_file(
"${COCKATRICE_CMAKE_PATH}/SendInstallerLog.ps1.in" "${PROJECT_BINARY_DIR}/SendInstallerLog.ps1" @ONLY
)
endif()
# Every runtime DLL a Windows install has to contain, as paths relative to the
# install root. .ci/compile.sh reads this and fails the build when the packaged
# installer is missing one of them, or carries a runtime it does not know about.
@ -435,9 +410,6 @@ if(WITH_ORACLE OR WITH_CLIENT)
add_subdirectory(${CMAKE_SOURCE_DIR}/libcockatrice_models ${CMAKE_BINARY_DIR}/libcockatrice_models)
add_subdirectory(${CMAKE_SOURCE_DIR}/libcockatrice_filters ${CMAKE_BINARY_DIR}/libcockatrice_filters)
endif()
if(WITH_CLIENT)
add_subdirectory(${CMAKE_SOURCE_DIR}/libcockatrice_game ${CMAKE_BINARY_DIR}/libcockatrice_game)
endif()
if(WITH_SERVER)
add_subdirectory(servatrice)
@ -449,13 +421,6 @@ if(WITH_CLIENT)
set(CPACK_INSTALL_CMAKE_PROJECTS "Cockatrice;Cockatrice;ALL;/" ${CPACK_INSTALL_CMAKE_PROJECTS})
endif()
if(WIN32 AND WITH_CLIENT)
# Runs from the installer's $PLUGINSDIR when a purge fails, and is never installed
# (see LogProcessesUsingInstallDir in cmake/NSIS.template.in), so it has no install
# rules and is not part of the client's staged tree.
add_subdirectory(${CMAKE_SOURCE_DIR}/tools/installer-holder-probe ${CMAKE_BINARY_DIR}/tools/installer-holder-probe)
endif()
if(WITH_ORACLE)
add_subdirectory(oracle)
set(CPACK_INSTALL_CMAKE_PROJECTS "Oracle;Oracle;ALL;/" ${CPACK_INSTALL_CMAKE_PROJECTS})
@ -468,5 +433,5 @@ endif()
if(Qt6_FOUND AND Qt6_VERSION_MINOR GREATER_EQUAL 3)
# Qt6.3+ requires project finalization to support translations
qt_finalize_project()
qt6_finalize_project()
endif()

View file

@ -15,10 +15,9 @@
---
<br><pre><p align='center'>
<b>To get started with Cockatrice &#8674; [view our webpage](https://cockatrice.github.io/)</b>
<b>To get support, or suggest changes to the app &#8674; [file an issue](https://github.com/Cockatrice/Cockatrice/issues) ([How?](https://github.com/Cockatrice/Cockatrice/wiki/How-to-Create-a-GitHub-Ticket-Regarding-Cockatrice))</b><br>
<b>To learn about the app and its code &#8674; [read our code documentation](https://cockatrice.github.io/docs/)</b>
<b>To help with development &#8674; learn [how to contribute](#contribute)</b>
<b>To get started with Cockatrice &#8674; [view our webpage](https://cockatrice.github.io/)</b><br>
<b>To get support, or suggest changes to the app &#8674; [file an issue](https://github.com/Cockatrice/Cockatrice/issues) ([How?](https://github.com/Cockatrice/Cockatrice/wiki/How-to-Create-a-GitHub-Ticket-Regarding-Cockatrice))</b>
<b>To help with development &#8674; learn [how to contribute](#contribute-)</b>
</pre><p><br>
@ -84,7 +83,7 @@ Join our [Discord community](https://discord.gg/3Z9yzmA) to connect with other p
### Code
To commit code to the project, please review our [Contribution Guidelines](https://github.com/Cockatrice/Cockatrice/blob/master/.github/CONTRIBUTING.md) first.<br>
To contribute code to the project, please review our [guidelines](https://github.com/Cockatrice/Cockatrice/blob/master/.github/CONTRIBUTING.md) first.<br>
We maintain two tags for contributors to easier find issues to potentially work on:
- [![Good first issue](https://img.shields.io/github/issues/cockatrice/cockatrice/Good%20First%20Issue)](https://github.com/Cockatrice/Cockatrice/issues?utf8=%E2%9C%93&q=is%3Aopen%20is%3Aissue%20label%3A%22Good%20first%20issue%22%20)<br>
Issues tagged in this way provide a simple way to get started. They don't require much experience to be worked on.
@ -96,7 +95,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).
### Documentation [![CI Docs](https://github.com/Cockatrice/Cockatrice/actions/workflows/documentation-build.yml/badge.svg?event=push)](https://github.com/Cockatrice/Cockatrice/actions/workflows/documentation-build.yml?query=event%3Arelease+event%3Aworkflow_dispatch)
### Documentation [![CI Docs](https://github.com/Cockatrice/Cockatrice/actions/workflows/documentation-build.yml/badge.svg?event=push)](https://github.com/Cockatrice/Cockatrice/actions/workflows/documentation-build.yml?query=event%3Apush)
There are various places where useful information for different needs are maintained:
- [Official Code Documentation](https://cockatrice.github.io/docs/)
@ -105,8 +104,7 @@ 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 CONTRIBUTING](https://github.com/Cockatrice/Cockatrice/blob/master/.github/CONTRIBUTING.md)
Cockatrice tries to use the [Google Developer Documentation Style Guide](https://developers.google.com/style/) to ensure consistent documentation.
We encourage everybody to improve the documentation by suggesting edits based on this guide.
Cockatrice tries to use the [Google Developer Documentation Style Guide](https://developers.google.com/style/) to ensure consistent documentation. We encourage you to improve the documentation by suggesting edits based on this guide.
### Translation [![Transifex Project](https://img.shields.io/badge/translate-on%20transifex-brightgreen)](https://explore.transifex.com/cockatrice/cockatrice/)

View file

@ -1,212 +0,0 @@
# Crash reporting via sentry-native (Sentry protocol, MIT).
# Included once from the root CMakeLists.txt, right after the sanitizer/coverage helpers.
#
# Inputs:
# WITH_CRASH_REPORTING -- build the client with the sentry-native SDK (default OFF, our release
# CI passes ON; packagers and from-source builds stay SDK-free until this
# ships something real, so offline package builds never fetch the SDK)
# COCKATRICE_SENTRY_DSN -- Sentry DSN baked into the build (default empty = SDK init skipped at runtime)
# COCKATRICE_SENTRY_TAG -- sentry-native release tag to vendor via FetchContent
# ENABLE_SANITIZERS -- forces the sentry backend to "none" (crashpad vs ASan conflict)
# Defines: cockatrice_target_enable_crash_reporting(<target>)
option(WITH_CRASH_REPORTING "Build with crash reporting via sentry-native" OFF)
set(COCKATRICE_SENTRY_DSN
""
CACHE STRING "Sentry DSN baked into the client; empty disables crash-report upload at runtime"
)
# Pinned stable release; tag verified at https://github.com/getsentry/sentry-native/tags.
set(COCKATRICE_SENTRY_TAG
"0.17.3"
CACHE STRING "sentry-native release tag to vendor via FetchContent"
)
# Wire the SDK (or the no-op fallback) into a client target. Always callable: with the
# option OFF the compile definitions below make crash_reporter.cpp compile to no-ops.
function(cockatrice_target_enable_crash_reporting _target)
if(WITH_CRASH_REPORTING AND TARGET sentry::sentry)
target_link_libraries(${_target} PRIVATE sentry::sentry)
target_compile_definitions(${_target} PRIVATE COCKATRICE_WITH_CRASH_REPORTING=1)
else()
target_compile_definitions(${_target} PRIVATE COCKATRICE_WITH_CRASH_REPORTING=0)
endif()
# Always baked in (possibly empty): main() prefers the COCKATRICE_SENTRY_DSN env var at runtime.
target_compile_definitions(${_target} PRIVATE "COCKATRICE_SENTRY_DSN=\"${COCKATRICE_SENTRY_DSN}\"")
# Sentry release for events from this target: PROJECT_VERSION (getversion.cmake ran before this
# file was included). Deliberately not PROJECT_VERSION_FRIENDLY / VERSION_STRING — that carries
# a build date, so every build of the same version would land in a different Sentry release.
# PROJECT_VERSION matches what the installer reports in its own log and what CPack derives the
# package version from, so client and installer events group under one release.
target_compile_definitions(${_target} PRIVATE "COCKATRICE_SENTRY_RELEASE=\"${PROJECT_VERSION}\"")
endfunction()
if(WITH_CRASH_REPORTING AND WITH_CLIENT)
include(FetchContent)
if(ENABLE_SANITIZERS)
# The crash handler cannot run under ASan/UBSan, so there is no backend to capture with.
set(_sentry_backend "none")
else()
# Breakpad on every OS (increment 3 probe, 2026-10-14): the crashpad sources
# do vendor friction-free (FetchContent inits all submodules recursively by
# default, verified populated in a local build tree), but the crashpad backend
# would need its out-of-process crashpad_handler binary installed next to the
# exe, into the macOS bundle, and through NSIS, plus handler-path wiring and
# notarization care. Breakpad is in-process, needs no extra binary and no
# packaging on any OS sentry-native builds, so simplicity wins.
set(_sentry_backend "breakpad")
endif()
# FORCE so toggling ENABLE_SANITIZERS on a reused build dir cannot leave a stale backend behind.
set(SENTRY_BACKEND
"${_sentry_backend}"
CACHE STRING "Crash handler backend for sentry-native" FORCE
)
unset(_sentry_backend)
if(NOT MSVC)
# Symbol upload needs debug info in Release binaries: the top-level flags build GCC
# Release as -s -O2 (stripped, no DWARF) and Clang/Apple Release as plain -O2 (no DWARF),
# both of which upload nothing useful. Keep -g and drop the strip flag while the SDK is
# on; the OFF default leaves local, distro and Debug builds untouched. MSVC already
# links /DEBUG with PDBs in release (see top-level CMakeLists.txt), so it needs nothing.
string(REGEX REPLACE "(^| )-s( |$)" "\\1\\2" CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE}")
if(NOT CMAKE_CXX_FLAGS_RELEASE MATCHES "(^| )-g( |$)")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -g")
endif()
endif()
# No Qt integration: it runs find_package(Qt6) inside the vendored build, which
# cannot see our thin/aqt Qt layouts (macOS CI) under foreign toolchains, and its
# breadcrumbs could carry unredacted log lines into native crash events that bypass
# before_send. Our attached redacted log preview already carries the log context.
set(SENTRY_INTEGRATION_QT
OFF
CACHE BOOL "Qt log-message breadcrumbs for sentry-native (off: discovery + secret surface)" FORCE
)
# The transport stays at the SDK default (curl on Apple/Linux, winhttp on Windows).
set(SENTRY_BUILD_TESTS
OFF
CACHE BOOL "Build sentry-native tests" FORCE
)
set(SENTRY_BUILD_EXAMPLES
OFF
CACHE BOOL "Build sentry-native examples" FORCE
)
fetchcontent_declare(
sentry
GIT_REPOSITORY https://github.com/getsentry/sentry-native.git
GIT_TAG ${COCKATRICE_SENTRY_TAG}
GIT_SHALLOW TRUE
)
fetchcontent_makeavailable(sentry)
# Vendored sentry-native (and its breakpad/crashpad trees) warn under our -Wall -Wextra
# (e.g. breakpad's -Wmissing-field-initializers), and WARNING_AS_ERROR would fail the build
# on code we cannot fix. Those findings are not actionable for us, so opt the whole vendored
# tree out of warnings-as-errors while keeping the warnings themselves visible; our own
# targets keep -Werror. Interface libraries compile nothing, so they are skipped outright:
# an INTERFACE -Wno-error would propagate to our consumers and weaken their build.
function(_cockatrice_sentry_opt_out_of_werror _dir)
get_property(
_sentry_targets
DIRECTORY "${_dir}"
PROPERTY BUILDSYSTEM_TARGETS
)
foreach(_sentry_target IN LISTS _sentry_targets)
get_target_property(_sentry_type ${_sentry_target} TYPE)
if(_sentry_type STREQUAL "INTERFACE_LIBRARY")
continue()
elseif(_sentry_type MATCHES "LIBRARY|EXECUTABLE")
get_target_property(_sentry_imported ${_sentry_target} IMPORTED)
if(NOT _sentry_imported)
target_compile_options(${_sentry_target} PRIVATE -Wno-error)
endif()
endif()
endforeach()
get_property(
_sentry_subdirs
DIRECTORY "${_dir}"
PROPERTY SUBDIRECTORIES
)
foreach(_sentry_subdir IN LISTS _sentry_subdirs)
_cockatrice_sentry_opt_out_of_werror("${_sentry_subdir}")
endforeach()
endfunction()
if(NOT MSVC)
_cockatrice_sentry_opt_out_of_werror("${sentry_SOURCE_DIR}")
endif()
message(STATUS "Crash reporting: sentry-native ${COCKATRICE_SENTRY_TAG} (backend: ${SENTRY_BACKEND})")
else()
message(STATUS "Crash reporting: SDK not vendored "
"(WITH_CRASH_REPORTING=${WITH_CRASH_REPORTING}, WITH_CLIENT=${WITH_CLIENT})"
)
endif()
# Packaged release binaries must not ship the DWARF that the symbol upload needs: it is
# 80-90% of an unstripped binary and nobody downloads a 500 MB installer to get it. The
# build tree keeps -g (CI uploads symbols from there); the *packaged copy* is split at
# install time by a CPack install script, which extracts the debug info next to the staging
# dir and strips the binary. Only ELF (Linux/BSD) needs this: Windows links /DEBUG with
# separate PDBs, and macOS keeps its dSYM extracted from the bundle.
option(COCKATRICE_SPLIT_DEBUG_SYMBOLS "Strip DWARF out of packaged release binaries" ON)
set(COCKATRICE_DEBUG_SYMBOL_DIR
"${CMAKE_BINARY_DIR}/debug-symbols"
CACHE PATH "Where the packaged binaries' split debug information is written (kept out of the package)"
)
if(WITH_CRASH_REPORTING
AND WITH_CLIENT
AND COCKATRICE_SPLIT_DEBUG_SYMBOLS
AND CMAKE_BUILD_TYPE STREQUAL "Release"
AND UNIX
AND NOT APPLE
)
find_program(COCKATRICE_OBJCOPY NAMES objcopy)
find_program(COCKATRICE_STRIP_TOOL NAMES strip)
if(COCKATRICE_OBJCOPY AND COCKATRICE_STRIP_TOOL)
configure_file(
"${CMAKE_CURRENT_LIST_DIR}/SplitDebugSymbols.cmake.in" "${CMAKE_BINARY_DIR}/cmake/SplitDebugSymbols.cmake" @ONLY
)
# PRE_BUILD runs after CPack stages the install tree and before the package is produced.
# CPACK_INSTALL_SCRIPTS would run too early (staging is still empty); the script walks
# CPACK_TEMPORARY_DIRECTORY, never CMAKE_INSTALL_PREFIX, and refuses to run otherwise.
list(APPEND CPACK_PRE_BUILD_SCRIPTS "${CMAKE_BINARY_DIR}/cmake/SplitDebugSymbols.cmake")
message(STATUS "Crash reporting: packaged binaries stripped, debug info split to ${COCKATRICE_DEBUG_SYMBOL_DIR}")
else()
message(WARNING "COCKATRICE_SPLIT_DEBUG_SYMBOLS is ON but objcopy/strip were not found; "
"packaged release binaries will keep their debug information"
)
endif()
endif()
# Installer failure reporting (increment 4): parse COCKATRICE_SENTRY_DSN into the parts the Windows installer
# needs. These are CPACK_* so CPack snapshots them at include(CPack) time and substitutes them into
# cmake/NSIS.template.in at package time; on non-Windows they are inert (only the NSIS template consumes them).
# Set from COCKATRICE_SENTRY_DSN on every configure, so a changed or removed DSN can never leave a stale value
# behind. Plain variables, not CACHE: they only need to be in scope when include(CPack) later snapshots every
# CPACK_* variable into CPackConfig.cmake.
set(CPACK_SENTRY_ENABLED "0")
set(CPACK_SENTRY_KEY "")
set(CPACK_SENTRY_ENVELOPE_URL "")
set(CPACK_SENTRY_DSN_FULL "")
# Deliberately PROJECT_VERSION: that is what the client bakes as its Sentry release and what CI passes to
# `sentry-cli debug-files upload`, so installer and client events group under one release. It is not the
# CPACK_PACKAGE_VERSION the installer logs in its own log line — on Development tags PROJECT_VERSION carries a
# "-<label>" suffix (getversion.cmake) while CPACK_PACKAGE_VERSION is only MAJOR.MINOR.PATCH.
set(CPACK_SENTRY_RELEASE "${PROJECT_VERSION}")
# Canonical <scheme>://<key>@<host>/<project-id>. Deliberately unanchored, so a trailing slash still matches.
# A self-hosted DSN with a path prefix (e.g. GlitchTip behind a subpath) does not match and falls through to the
# disabled-and-warn branch instead of deriving a wrong endpoint from the tail of the path.
if(COCKATRICE_SENTRY_DSN MATCHES "(https?)://([^@]+)@([^/]+)/([0-9]+)")
set(CPACK_SENTRY_ENABLED "1")
set(CPACK_SENTRY_KEY "${CMAKE_MATCH_2}")
# Scheme comes from the DSN, not an assumption: self-hosted (GlitchTip) DSNs may be plain http.
set(CPACK_SENTRY_ENVELOPE_URL "${CMAKE_MATCH_1}://${CMAKE_MATCH_3}/api/${CMAKE_MATCH_4}/envelope/")
set(CPACK_SENTRY_DSN_FULL "${COCKATRICE_SENTRY_DSN}")
elseif(NOT COCKATRICE_SENTRY_DSN STREQUAL "")
message(WARNING "COCKATRICE_SENTRY_DSN is set but malformed "
"(expected <scheme>://<key>@<host>/<project-id>); installer log reporting disabled"
)
endif()

File diff suppressed because it is too large Load diff

View file

@ -1,146 +0,0 @@
# Sends the NSIS installer log to Sentry as an envelope carrying one error
# event plus the log as an attachment.
#
# Generated at configure time from this template by CMake configure_file: the
# CPACK_SENTRY_* placeholders are baked in then, so the real values only ever
# exist in the build directory and the packaged installer, never in the repo.
# Runs on stock Windows PowerShell 5.1 with no extra modules.
param(
[Parameter(Mandatory = $true)][string]$LogPath
)
$ErrorActionPreference = 'Stop'
$EnvelopeUrl = '@CPACK_SENTRY_ENVELOPE_URL@'
$SentryKey = '@CPACK_SENTRY_KEY@'
$Release = '@CPACK_SENTRY_RELEASE@'
$Dsn = '@CPACK_SENTRY_DSN_FULL@'
try {
# The envelope endpoint is TLS-only; stock powershell.exe on older systems
# still negotiates TLS 1.0 otherwise, so opt into 1.2 explicitly.
try {
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
} catch {
}
# --- Log payload, tail-capped at 128 KiB ---------------------------------
$MaxLogBytes = 128 * 1024
$TruncNotice = '[log truncated to last 128 KB]'
if (Test-Path -LiteralPath $LogPath) {
$raw = [System.IO.File]::ReadAllBytes($LogPath)
} else {
$raw = [System.Text.Encoding]::UTF8.GetBytes('(installer log not found: ' + $LogPath + ')')
}
if ($raw.Length -gt $MaxLogBytes) {
$tail = New-Object byte[] $MaxLogBytes
[System.Buffer]::BlockCopy($raw, $raw.Length - $MaxLogBytes, $tail, 0, $MaxLogBytes)
$prefix = [System.Text.Encoding]::UTF8.GetBytes($TruncNotice + "`r`n")
$logBytes = New-Object byte[] ($prefix.Length + $tail.Length)
[System.Buffer]::BlockCopy($prefix, 0, $logBytes, 0, $prefix.Length)
[System.Buffer]::BlockCopy($tail, 0, $logBytes, $prefix.Length, $tail.Length)
} else {
$logBytes = $raw
}
# --- Abort reason, read from the log itself --------------------------------
# AbortInstall logs "ABORT: ..." first, so the reason travels inside the file
# and nothing reason-shaped crosses the command line (no quoting hazards).
# Select-String yields the matching line alone, so the reason is already one
# physical line for the message; anything else stays in the attached log.
# Non-ASCII paths may garble here; the attachment is byte-exact regardless.
$Reason = 'unknown (no ABORT line in log)'
if (Test-Path -LiteralPath $LogPath) {
$abortLine = Select-String -LiteralPath $LogPath -Pattern '^ABORT: ' | Select-Object -Last 1
if ($abortLine) {
$Reason = ($abortLine.Line -replace '^ABORT:\s*', '')
}
}
# --- What was holding the files, read from the log as well -------------------
# The guard and the close attempt each log one line naming every holder and what
# happened to it. Carrying the last of each as event data keeps an abort caused by
# a running instance actionable without opening the attachment. Kept out of the
# message so the issue still groups on the reason alone.
$Holders = ''
$CloseResult = ''
$EndResult = ''
if (Test-Path -LiteralPath $LogPath) {
$holderLine = Select-String -LiteralPath $LogPath -Pattern 'guard: still in use by (.*)$' | Select-Object -Last 1
if ($holderLine) {
$Holders = $holderLine.Matches[0].Groups[1].Value
}
$closeLine = Select-String -LiteralPath $LogPath -Pattern '(close-holders: .*)$' | Select-Object -Last 1
if ($closeLine) {
$CloseResult = $closeLine.Matches[0].Groups[1].Value
}
$endLine = Select-String -LiteralPath $LogPath -Pattern '(end-holders: .*)$' | Select-Object -Last 1
if ($endLine) {
$EndResult = $endLine.Matches[0].Groups[1].Value
}
}
# --- Event ---------------------------------------------------------------
$eventId = [guid]::NewGuid().ToString('N')
$now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
# $Event is an automatic variable (Microsoft.PowerShell.Utility\Register-ObjectEvent),
# so the local is named $sentryEvent to avoid shadowing it.
$sentryEvent = [ordered]@{
event_id = $eventId
timestamp = $now
platform = 'other'
level = 'error'
logger = 'cockatrice-installer'
release = $Release
environment = 'installer'
sdk = @{ name = 'cockatrice.installer'; version = $Release }
message = @{ formatted = 'Installer aborted: ' + $Reason }
extra = @{
reason = $Reason
os = [System.Environment]::OSVersion.VersionString
holders = $Holders
close_result = $CloseResult
end_result = $EndResult
}
}
$eventJson = $sentryEvent | ConvertTo-Json -Compress -Depth 6
$eventBytes = [System.Text.Encoding]::UTF8.GetBytes($eventJson)
# --- Envelope, assembled as bytes (the log is opaque binary) -------------
$sentAt = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
$headerLine = '{"event_id":"' + $eventId + '","sent_at":"' + $sentAt + '","dsn":"' + $Dsn + '"}'
$eventHeader = '{"type":"event","length":' + $eventBytes.Length + '}'
$attachHeader = '{"type":"attachment","filename":"Cockatrice-installer.log","content_type":"text/plain","length":' + $logBytes.Length + '}'
$enc = [System.Text.Encoding]::UTF8
$lf = [byte[]]@(10)
$body = New-Object System.IO.MemoryStream
foreach ($chunk in @($enc.GetBytes($headerLine), $lf, $enc.GetBytes($eventHeader), $lf, $eventBytes, $lf, $enc.GetBytes($attachHeader), $lf, $logBytes)) {
$body.Write($chunk, 0, $chunk.Length)
}
$bodyBytes = $body.ToArray()
$body.Close()
# --- Send ----------------------------------------------------------------
# The key travels in the auth header only; stdout below never contains it.
$auth = 'Sentry sentry_version=7, sentry_client=cockatrice.installer/' + $Release + ', sentry_key=' + $SentryKey
Invoke-RestMethod -Uri $EnvelopeUrl -Method Post -TimeoutSec 45 -ContentType 'application/x-sentry-envelope' -Headers @{ 'X-Sentry-Auth' = $auth } -Body $bodyBytes | Out-Null
Write-Output ('SENT ' + $eventId)
exit 0
} catch {
# Scrub the key/DSN out of the failure text in case a transport error echoes them.
$msg = $_.Exception.Message
if ($msg -and $SentryKey) {
$msg = $msg.Replace($SentryKey, '<redacted>')
}
if ($msg -and $Dsn) {
$msg = $msg.Replace($Dsn, '<redacted>')
}
# Flatten to one line: this text is echoed into the installer log, which is
# itself uploaded as an attachment, so a multi-line transport error must not
# be able to forge extra lines in the artifact we send.
if ($msg) {
$msg = ($msg -replace '\s+', ' ').Trim()
}
Write-Output ('FAILED: ' + $msg)
exit 1
}

View file

@ -1,80 +0,0 @@
# Splits DWARF out of the packaged ELF binaries, then strips them.
#
# This is a CPack pre-build script (CPACK_PRE_BUILD_SCRIPTS, wired in CrashReporting.cmake):
# CPack runs it after staging the install tree and before producing the package, so it only
# ever touches the copy that becomes the release artifact. The build tree is left alone: -g
# stays on, and CI uploads symbols to Sentry straight from build/.
#
# For every ELF file under CPACK_TEMPORARY_DIRECTORY the script extracts the debug information
# to COCKATRICE_DEBUG_SYMBOL_DIR and removes the debug sections from the packaged binary.
# COCKATRICE_DEBUG_SYMBOL_DIR is deliberately outside the staging tree, so sidecars never end
# up in the package; CI can archive/upload them if wanted (it normally uploads straight from
# the build tree). The only thing that must survive stripping is .eh_frame (allocatable, used
# for unwinding), which neither --only-keep-debug nor --strip-debug removes.
#
# Tool paths, the symbol directory and the tree to walk are baked in / provided at configure /
# run time; the guards below refuse to run on anything that is not a CPack staging directory,
# so a missing variable can never turn the glob into a filesystem-wide walk.
set(_objcopy "@COCKATRICE_OBJCOPY@")
set(_strip "@COCKATRICE_STRIP_TOOL@")
set(_symbolDir "@COCKATRICE_DEBUG_SYMBOL_DIR@")
if(_objcopy STREQUAL "" OR _strip STREQUAL "")
message(WARNING "split debug symbols: objcopy/strip not found at configure time, packaging unstripped binaries")
return()
endif()
if(_symbolDir STREQUAL "")
message(FATAL_ERROR "split debug symbols: COCKATRICE_DEBUG_SYMBOL_DIR is empty")
endif()
# CPack sets CPACK_TEMPORARY_DIRECTORY to the staging tree; CMAKE_INSTALL_PREFIX is still the
# configured /usr prefix here, so globbing it would walk system directories. Refuse to run
# unless the path exists and really is a CPack staging tree.
if(NOT CPACK_TEMPORARY_DIRECTORY OR NOT IS_DIRECTORY "${CPACK_TEMPORARY_DIRECTORY}")
message(STATUS "split debug symbols: no CPack staging directory, skipping")
return()
endif()
if(NOT CPACK_TEMPORARY_DIRECTORY MATCHES "_CPack_Packages")
message(WARNING "split debug symbols: ${CPACK_TEMPORARY_DIRECTORY} is not a CPack staging tree, skipping")
return()
endif()
file(MAKE_DIRECTORY "${_symbolDir}")
file(
GLOB_RECURSE _entries
LIST_DIRECTORIES true
"${CPACK_TEMPORARY_DIRECTORY}/*"
)
set(_processed 0)
foreach(_file IN LISTS _entries)
if(IS_DIRECTORY "${_file}" OR IS_SYMLINK "${_file}")
continue()
endif()
# Only ELF: objcopy would fail on the icons, translations and Qt resources next to it.
file(
READ "${_file}" _magic
OFFSET 0
LIMIT 4
HEX
)
if(NOT _magic STREQUAL "7f454c46")
continue()
endif()
get_filename_component(_name "${_file}" NAME)
set(_debug "${_symbolDir}/${_name}.debug")
execute_process(COMMAND "${_objcopy}" --only-keep-debug "${_file}" "${_debug}" RESULT_VARIABLE _rc)
if(NOT _rc EQUAL 0)
message(WARNING "split debug symbols: objcopy --only-keep-debug failed for ${_file}")
continue()
endif()
execute_process(COMMAND "${_strip}" --strip-debug --strip-unneeded "${_file}" RESULT_VARIABLE _rc)
if(NOT _rc EQUAL 0)
message(WARNING "split debug symbols: strip failed for ${_file}")
continue()
endif()
math(EXPR _processed "${_processed} + 1")
endforeach()
message(STATUS "Split debug symbols for ${_processed} packaged ELF file(s); sidecars in ${_symbolDir}")

View file

@ -15,7 +15,6 @@ set(cockatrice_SOURCES
src/client/network/update/client/client_update_checker.cpp
src/client/network/update/client/release_channel.cpp
src/client/network/update/card_spoiler/spoiler_background_updater.cpp
src/client/crash/crash_reporter.cpp
src/client/latency_graph_widget.cpp
src/client/latency_status_widget.cpp
src/client/lag_monitor.cpp
@ -25,11 +24,12 @@ set(cockatrice_SOURCES
src/client/settings/shortcut_treeview.cpp
src/client/settings/shortcuts_settings.cpp
src/interface/deck_loader/card_node_function.cpp
src/interface/deck_loader/deck_file_format.cpp
src/interface/deck_loader/deck_loader.cpp
src/interface/deck_loader/loaded_deck.cpp
src/interface/widgets/dialogs/dlg_connect.cpp
src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.cpp
src/interface/widgets/dialogs/dlg_create_game.cpp
src/interface/widgets/dialogs/dlg_crash_report.cpp
src/interface/widgets/dialogs/dlg_default_tags_editor.cpp
src/interface/widgets/dialogs/dlg_edit_avatar.cpp
src/interface/widgets/dialogs/dlg_edit_password.cpp
@ -60,7 +60,6 @@ set(cockatrice_SOURCES
src/interface/widgets/dialogs/dlg_view_log.cpp
src/interface/widgets/dialogs/override_printing_warning.cpp
src/interface/widgets/dialogs/path_safety_warning.cpp
src/interface/widgets/dialogs/settings_view.cpp
src/interface/widgets/dialogs/tip_of_the_day.cpp
src/interface/widgets/deck_share/deck_share_utils.cpp
src/interface/widgets/deck_share/shared_deck_preview_widget.cpp
@ -69,27 +68,36 @@ set(cockatrice_SOURCES
src/filters/filter_builder.cpp
src/filters/filter_tree_model.cpp
src/filters/syntax_help.cpp
src/game/abstract_game.cpp
src/game/arrow_registry.cpp
src/game_graphics/board/abstract_card_drag_item.cpp
src/game_graphics/board/abstract_card_item.cpp
src/game_graphics/board/abstract_counter.cpp
src/game/board/arrow_data.cpp
src/game_graphics/board/arrow_item.cpp
src/game_graphics/board/arrow_registry.cpp
src/game_graphics/board/arrow_target.cpp
src/game_graphics/board/card_drag_item.cpp
src/game_graphics/board/card_item.cpp
src/game/board/card_list.cpp
src/game/board/card_state.cpp
src/game_graphics/board/counter_general.cpp
src/game/board/counter_state.cpp
src/game_graphics/board/translate_counter_name.cpp
src/game_graphics/deckview/deck_view.cpp
src/game_graphics/deckview/deck_view_container.cpp
src/game_graphics/deckview/tabbed_deck_view_container.cpp
src/game_graphics/dialogs/dlg_annotation.cpp
src/game_graphics/dialogs/dlg_create_token.cpp
src/game_graphics/dialogs/dlg_move_top_cards_until.cpp
src/game_graphics/dialogs/dlg_roll_dice.cpp
src/game/game.cpp
src/game/game_event_handler.cpp
src/game/game_meta_info.cpp
src/game_graphics/game_scene.cpp
src/game/game_state.cpp
src/game_graphics/game_view.cpp
src/game_graphics/hand_counter.cpp
src/game_graphics/log/message_log_widget.cpp
src/game/phase.cpp
src/game_graphics/phases_toolbar.cpp
src/game_graphics/player/menu/card_menu.cpp
src/game_graphics/player/menu/custom_zone_menu.cpp
@ -107,11 +115,23 @@ set(cockatrice_SOURCES
src/game_graphics/tally/stats_tally.cpp
src/game_graphics/tally/subtype_tally.cpp
src/game_graphics/tally/tally.cpp
src/game/player/player_actions.cpp
src/game_graphics/player/player_area.cpp
src/game_graphics/player/player_dialogs.cpp
src/game/player/player_event_handler.cpp
src/game_graphics/player/player_graphics_item.cpp
src/game/player/player_info.cpp
src/game_graphics/player/player_list_widget.cpp
src/game/player/player_logic.cpp
src/game/player/player_manager.cpp
src/game_graphics/player/player_target.cpp
src/game/replay.cpp
src/game/zones/card_zone_logic.cpp
src/game/zones/hand_zone_logic.cpp
src/game/zones/pile_zone_logic.cpp
src/game/zones/stack_zone_logic.cpp
src/game/zones/table_zone_logic.cpp
src/game/zones/view_zone_logic.cpp
src/game_graphics/zones/card_zone.cpp
src/game_graphics/zones/hand_zone.cpp
src/game_graphics/zones/pile_zone.cpp
@ -121,8 +141,6 @@ set(cockatrice_SOURCES
src/game_graphics/zones/view_zone.cpp
src/game_graphics/zones/view_zone_widget.cpp
src/game_graphics/board/abstract_graphics_item.cpp
src/interface/at_risk_data_warning.cpp
src/interface/at_risk_data_warning.h
src/interface/card_database_update/card_database_update_status_bar.cpp
src/interface/card_database_update/card_database_update_status_bar.h
src/interface/card_database_update/card_update_progress.cpp
@ -172,7 +190,6 @@ set(cockatrice_SOURCES
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_text_widget.cpp
src/interface/widgets/cards/card_picture_delegate.cpp
src/interface/widgets/cards/card_size_widget.cpp
src/interface/widgets/cards/deck_card_zone_display_widget.cpp
src/interface/widgets/cards/deck_preview_card_picture_widget.cpp
@ -292,7 +309,6 @@ set(cockatrice_SOURCES
src/interface/widgets/utility/sequence_edit.cpp
src/interface/widgets/utility/visibility_change_listener.cpp
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_filter_save_load_widget.cpp
src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp
@ -325,7 +341,6 @@ set(cockatrice_SOURCES
src/interface/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.cpp
src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp
src/interface/window_main.cpp
src/installer_close_server.cpp
src/main.cpp
src/single_instance_manager.cpp
src/interface/widgets/tabs/abstract_tab_deck_editor.cpp
@ -431,7 +446,6 @@ set(cockatrice_SOURCES
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.h
src/interface/widgets/utility/compact_push_button.cpp
src/interface/widgets/utility/compact_push_button.h
src/installer_close_server.h
src/single_instance_manager.h
src/client/url_scheme_event_filter.h
src/interface/intents/intent_connect_to_server.cpp
@ -493,7 +507,7 @@ if(APPLE)
set(cockatrice_SOURCES ${cockatrice_SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/resources/appicon.icns)
endif(APPLE)
qt_add_resources(cockatrice_RESOURCES_RCC ${cockatrice_RESOURCES})
qt6_add_resources(cockatrice_RESOURCES_RCC ${cockatrice_RESOURCES})
# Declare path variables
set(ICONDIR
@ -514,7 +528,7 @@ set(COCKATRICE_MAC_QM_INSTALL_DIR "cockatrice.app/Contents/Resources/translation
set(COCKATRICE_UNIX_QM_INSTALL_DIR "share/cockatrice/translations")
set(COCKATRICE_WIN32_QM_INSTALL_DIR "translations")
qt_add_executable(
qt6_add_executable(
cockatrice
WIN32
MACOSX_BUNDLE
@ -526,7 +540,7 @@ qt_add_executable(
target_precompile_headers(cockatrice PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtwidgets_pch.h")
qt_add_shaders(
qt6_add_shaders(
cockatrice
"onboarding_shaders"
PREFIX
@ -538,7 +552,7 @@ qt_add_shaders(
src/interface/widgets/onboarding/shaders/brand_plate.frag
)
qt_add_resources(
qt6_add_resources(
cockatrice
"onboarding_qml"
PREFIX
@ -554,7 +568,6 @@ target_link_libraries(
PUBLIC libcockatrice_card
libcockatrice_deck_list
libcockatrice_filters
libcockatrice_game
libcockatrice_utility
libcockatrice_network
libcockatrice_models
@ -563,8 +576,6 @@ target_link_libraries(
${COCKATRICE_QT_MODULES}
)
cockatrice_target_enable_crash_reporting(cockatrice)
if(UNIX)
if(APPLE)
set(MACOSX_BUNDLE_INFO_STRING "${PROJECT_NAME}")
@ -771,7 +782,7 @@ endif()
if(Qt6LinguistTools_FOUND)
#Qt6 Translations happen after the executable is built up
if(UPDATE_TRANSLATIONS)
qt_add_translations(
qt6_add_translations(
cockatrice
TS_FILES
${cockatrice_TS}
@ -781,7 +792,7 @@ if(Qt6LinguistTools_FOUND)
cockatrice_QM
)
else()
qt_add_translations(cockatrice TS_FILES ${cockatrice_TS} QM_FILES_OUTPUT_VARIABLE cockatrice_QM)
qt6_add_translations(cockatrice TS_FILES ${cockatrice_TS} QM_FILES_OUTPUT_VARIABLE cockatrice_QM)
endif()
if(UNIX)
@ -807,4 +818,4 @@ if(Qt6LinguistTools_FOUND)
endforeach()
endif()
qt_finalize_target(cockatrice)
qt6_finalize_target(cockatrice)

View file

@ -1,616 +0,0 @@
#include "crash_reporter.h"
#if COCKATRICE_WITH_CRASH_REPORTING
#include <sentry.h>
#endif
#include <QByteArray>
#include <QCoreApplication>
#include <QDir>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QLockFile>
#include <QLoggingCategory>
#include <QRegularExpression>
#include <QThread>
#include <QUrl>
#include <cstring>
#include <libcockatrice/settings/personal_settings.h>
#include <string>
Q_LOGGING_CATEGORY(CrashReportLog, "crash_report");
#if COCKATRICE_WITH_CRASH_REPORTING
namespace
{
constexpr char CockatriceUrlScheme[] = "cockatrice://";
/**
* @brief Qt-free cockatrice:// secret scrubber for the before_send hook.
*
* before_send does not run in async-signal context on our backends (breakpad invokes it on the
* crashing thread outside the signal handler, and crashpad/native crashes bypass the hook
* entirely), but it still avoids QString/QRegularExpression so it stays safe to call from the
* crash-handler thread. Truncates each URL at the first terminator, keeping scheme://host like
* redactCockatriceUrls does. (std::string allocates, so this is not strictly signal-safe.)
*/
std::string scrubCockatriceUrls(const char *input)
{
std::string out;
const char *pos = input;
const char *scheme = CockatriceUrlScheme;
const size_t schemeLen = std::strlen(scheme);
while (*pos != '\0') {
const char *found = std::strstr(pos, scheme);
if (found == nullptr) {
out.append(pos);
break;
}
out.append(pos, found);
const char *hostEnd = found + schemeLen;
while (*hostEnd != '\0' && *hostEnd != '/' && *hostEnd != '?' && *hostEnd != '#' && *hostEnd != ' ' &&
*hostEnd != '\t' && *hostEnd != '\n' && *hostEnd != '\'' && *hostEnd != '"' && *hostEnd != '<' &&
*hostEnd != '>') {
++hostEnd;
}
out.append(found, hostEnd);
pos = hostEnd;
}
return out;
}
void scrubStringValue(sentry_value_t holder, const char *key)
{
sentry_value_t current = sentry_value_get_by_key(holder, key);
if (sentry_value_get_type(current) != SENTRY_VALUE_TYPE_STRING) {
return;
}
const char *raw = sentry_value_as_string(current);
if (raw == nullptr || std::strstr(raw, CockatriceUrlScheme) == nullptr) {
return;
}
sentry_value_set_by_key(holder, key, sentry_value_new_string(scrubCockatriceUrls(raw).c_str()));
}
/**
* @brief Scrub trivially reachable string values inside a breadcrumb data object.
*/
int scrubDataEntry(const char *key, sentry_value_t value, void *userdata)
{
if (sentry_value_get_type(value) != SENTRY_VALUE_TYPE_STRING) {
return 0;
}
const char *raw = sentry_value_as_string(value);
if (raw == nullptr || std::strstr(raw, CockatriceUrlScheme) == nullptr) {
return 0;
}
auto *holder = static_cast<sentry_value_t *>(userdata);
sentry_value_set_by_key(*holder, key, sentry_value_new_string(scrubCockatriceUrls(raw).c_str()));
return 0;
}
void scrubDataObject(sentry_value_t data)
{
if (sentry_value_get_type(data) != SENTRY_VALUE_TYPE_OBJECT) {
return;
}
sentry_value_foreach_key_value(data, scrubDataEntry, &data);
}
void scrubCrumbList(sentry_value_t crumbs)
{
const size_t count = sentry_value_get_length(crumbs);
for (size_t i = 0; i < count; ++i) {
sentry_value_t crumb = sentry_value_get_by_index(crumbs, i);
if (sentry_value_get_type(crumb) != SENTRY_VALUE_TYPE_OBJECT) {
continue;
}
scrubStringValue(crumb, "message");
scrubDataObject(sentry_value_get_by_key(crumb, "data"));
}
}
void scrubBreadcrumbs(sentry_value_t event)
{
// The SDK merges scope breadcrumbs onto the event as a plain list (sentry__value_merge_breadcrumbs
// in sentry_scope.c); the { "values": [...] } envelope shape only exists on the wire. Accept both
// so the scrubbing runs on the actual merged form.
sentry_value_t breadcrumbs = sentry_value_get_by_key(event, "breadcrumbs");
if (sentry_value_get_type(breadcrumbs) == SENTRY_VALUE_TYPE_LIST) {
scrubCrumbList(breadcrumbs);
return;
}
if (sentry_value_get_type(breadcrumbs) == SENTRY_VALUE_TYPE_OBJECT) {
sentry_value_t values = sentry_value_get_by_key(breadcrumbs, "values");
if (sentry_value_get_type(values) == SENTRY_VALUE_TYPE_LIST) {
scrubCrumbList(values);
}
}
}
/**
* @brief before_send hook: scrub cockatrice:// secrets from the event.
*
* Defense in depth — payloads we build are pre-redacted. No breadcrumbs are recorded today (the Qt
* integration stays off), so this currently covers the top-level message and logentry.formatted,
* plus any breadcrumb messages/data that reach the merged event in the future. Native crash
* minidumps bypass this hook regardless of backend: breakpad captures them in-process without
* invoking before_send, and fast-fail crashes on Windows skip the local handler, so before_send is
* never invoked for those.
*/
sentry_value_t scrubEventBeforeSend(sentry_value_t event, sentry_hint_t *hint, void *userData)
{
Q_UNUSED(hint);
Q_UNUSED(userData);
scrubStringValue(event, "message");
sentry_value_t logentry = sentry_value_get_by_key(event, "logentry");
if (sentry_value_get_type(logentry) == SENTRY_VALUE_TYPE_OBJECT) {
scrubStringValue(logentry, "formatted");
}
scrubBreadcrumbs(event);
return event;
}
} // namespace
#endif
namespace
{
/**
* @brief DSN matcher shared by isDsnWellFormed() and describeDsn().
*
* Kept unanchored on purpose, mirroring the MATCHES in cmake/CrashReporting.cmake (CMake searches
* rather than anchoring), so the client never rejects a DSN the build itself baked in. Compiled once
* because QRegularExpression construction is not free and both helpers are called per pending
* envelope.
*/
const QRegularExpression &dsnPattern()
{
static const QRegularExpression pattern(QStringLiteral("(https?)://([^@]+)@([^/]+)/([0-9]+)"));
return pattern;
}
} // namespace
bool CrashReporter::isDsnWellFormed(const QString &dsn)
{
return dsnPattern().match(dsn).hasMatch();
}
QString CrashReporter::describeDsn(const QString &dsn)
{
const QRegularExpressionMatch match = dsnPattern().match(dsn);
if (!match.hasMatch()) {
return QString();
}
// Host and project id only: the key in group 2 must never reach a log line.
return QStringLiteral("%1 (project %2)").arg(match.captured(3), match.captured(4));
}
QString CrashReporter::describeOriginMismatch(const QString &envelopePath, const QString &currentDsn)
{
const QString destination = describeDsn(currentDsn);
if (destination.isEmpty()) {
return QString();
}
QFile file(envelopePath);
if (!file.open(QIODevice::ReadOnly)) {
return QString();
}
// The envelope header is the first line and is small; the cap only keeps a corrupt file from
// being read in full.
const QByteArray headerLine = file.readLine(4096).trimmed();
if (headerLine.isEmpty() || !headerLine.startsWith('{')) {
return QString();
}
const QString originDsn = QJsonDocument::fromJson(headerLine).object().value(QStringLiteral("dsn")).toString();
const QString origin = describeDsn(originDsn);
// Equal destinations are not a mismatch: a different key for the same host and project still
// uploads where the crash was captured.
if (origin.isEmpty() || origin == destination) {
return QString();
}
return QStringLiteral("was captured for %1 but this session uploads with %2").arg(origin, destination);
}
bool CrashReporter::isReportingEnabled(const QString &dsn, PersonalSettings::CrashReportingMode consentMode)
{
return !dsn.isEmpty() && consentMode != PersonalSettings::CrashReportingMode::NeverSend;
}
void CrashReporter::init(const QString &dsn,
const QString &release,
const QString &databasePath,
PersonalSettings::CrashReportingMode consentMode)
{
shutdown();
sentryDatabasePath = databasePath;
if (!isReportingEnabled(dsn, consentMode)) {
if (!dsn.isEmpty()) {
// A deliberate choice rather than a misconfiguration, so it is not a warning. The
// no-DSN case is reported by the caller, which is the only place that knows whether
// the environment or the build supplied the DSN.
qCInfo(CrashReportLog) << "crash reporting disabled by preference (Never send)";
}
return;
}
// Only created when the SDK will actually run: no-DSN launches stay side-effect free.
#if COCKATRICE_WITH_CRASH_REPORTING
// A crash captured by an earlier run is uploaded by THIS session's transport, which posts every
// envelope with the DSN it was armed with (sentry-native keeps the DSN in the transport state,
// and envelopes restored from disk are raw, so their own "dsn" header is never read). When the
// two disagree the report lands in a project nobody chose, which no other log line reveals.
const QStringList pendingEnvelopes = pendingEnvelopePaths(sentryDatabasePath);
for (const QString &envelopePath : pendingEnvelopes) {
const QString mismatch = describeOriginMismatch(envelopePath, dsn);
if (!mismatch.isEmpty()) {
qCWarning(CrashReportLog) << "pending crash envelope" << envelopePath << mismatch;
}
}
qCInfo(CrashReportLog) << "crash reporting armed for" << describeDsn(dsn);
QDir().mkpath(sentryDatabasePath);
if (consentMode != PersonalSettings::CrashReportingMode::AlwaysSend) {
// sentry-native persists consent to <database>/user-consent and reloads it in sentry_init
// before old runs are processed, so a stale "given" from an earlier AlwaysSend run (or an
// earlier Send in Ask mode) would upload the next crash before the dialog asks. Our
// CrashReportingMode is the source of truth, so clear the persisted state first; asking
// afterwards re-establishes it via giveConsent()/revokeConsent().
QFile::remove(sentryDatabasePath + QStringLiteral("/user-consent"));
}
sentry_options_t *options = sentry_options_new();
sentry_options_set_dsn(options, dsn.toUtf8().constData());
sentry_options_set_release(options, release.toUtf8().constData());
sentry_options_set_database_path(options, sentryDatabasePath.toUtf8().constData());
// Consent-gated uploads: crashes are captured into the local database but
// nothing leaves the machine until giveConsent(). cache_keep holds the previous run's
// crash envelope in <database>/cache instead of dropping it at init, so the Ask dialog
// decides its fate: giveConsent() flushes it via the transport retry, revokeConsent()
// deletes it below. This also covers the first crash after switching to AlwaysSend,
// which giveConsent() (running after init) would otherwise miss. http_retry is required
// for the flush: without it the held envelope is written with a plain filename the retry
// scanner skips, and giveConsent()'s sentry_transport_retry() sends nothing.
sentry_options_set_require_user_consent(options, 1);
sentry_options_set_cache_keep(options, SENTRY_CACHE_KEEP_OFFLINE);
sentry_options_set_http_retry(options, 1);
sentry_options_set_before_send(options, scrubEventBeforeSend, nullptr);
ready = (sentry_init(options) == 0);
#else
Q_UNUSED(release);
// Not a misconfiguration like an empty DSN: this build cannot report whatever the DSN says, so
// a tester forcing a crash needs to know the difference.
qCInfo(CrashReportLog) << "crash reporting unavailable: built without Sentry support";
ready = false;
#endif
}
bool CrashReporter::isReady() const
{
return ready;
}
void CrashReporter::shutdown()
{
#if COCKATRICE_WITH_CRASH_REPORTING
if (ready) {
sentry_close();
}
#endif
ready = false;
}
CrashReporter::~CrashReporter() = default;
CrashReporter::CrashReporter() = default;
bool CrashReporter::beginSession(const QString &dir)
{
// Restartable: releasing first keeps begin->end->begin round-trips clean.
endSession();
QDir().mkpath(dir);
const QDir sessionDir(dir);
const QString selfName = QStringLiteral("session-%1").arg(QCoreApplication::applicationPid());
bool unclean = false;
const QStringList leftovers = sessionDir.entryList({QStringLiteral("session-*")}, QDir::Files);
for (const QString &name : leftovers) {
if (name == selfName) {
continue;
}
const QString path = sessionDir.filePath(name);
QLockFile probe(path);
// Long-lived session lock: stale means "owner dead", regardless of file age, so a
// crash followed by an immediate relaunch (younger than the 30 s default) is still
// detected. tryLock() only deletes the file when it finds it stale, so a live
// concurrent instance's lock is never touched here. (The static
// QLockFile::removeStaleLockFile(path) form does not exist in Qt5/Qt6, and the
// member removeStaleLockFile() force-removes without a staleness check, so neither
// is usable for this scan.)
probe.setStaleLockTime(0);
if (probe.tryLock(0)) {
unclean = true;
probe.unlock(); // deletes the stale file
continue;
}
// tryLock failed: either a live concurrent instance owns the file, or it is
// unreadable (garbage/empty) and therefore has no live owner. Only the latter is
// cleaned up; a live instance is left alone. On Unix QLockFile creates the file
// before writing the pid, so a second instance starting at the same moment can
// briefly observe an empty (or half-written) file for a live owner: re-read once
// after a beat so a live owner's fresh lock is never mistaken for garbage and deleted.
qint64 pid = 0;
QString hostname, appname;
if (!probe.getLockInfo(&pid, &hostname, &appname)) {
QThread::msleep(100);
if (!probe.getLockInfo(&pid, &hostname, &appname)) {
QFile::remove(path);
unclean = true;
}
}
}
sessionLockPath = sessionDir.filePath(selfName);
sessionLock = std::make_unique<QLockFile>(sessionLockPath);
sessionLock->setStaleLockTime(0);
if (!sessionLock->tryLock(0)) {
qCWarning(CrashReportLog) << "Failed to claim session lock at" << sessionLockPath;
}
return unclean;
}
void CrashReporter::endSession()
{
if (sessionLock) {
// unlock() removes the lock file; nothing left to clean up.
sessionLock->unlock();
sessionLock.reset();
} else if (!sessionLockPath.isEmpty()) {
// The lock was never claimed (a live concurrent instance owns its file): only our
// own unclaimed file, if tryLock left one behind, is removed.
QFile::remove(sessionLockPath);
}
sessionLockPath.clear();
}
void CrashReporter::giveConsent()
{
#if COCKATRICE_WITH_CRASH_REPORTING
if (ready) {
sentry_user_consent_give();
}
#endif
}
void CrashReporter::revokeConsent()
{
#if COCKATRICE_WITH_CRASH_REPORTING
// Revoke (not reset): an explicit refusal blocks uploads; reset would
// return to unknown, which blocks uploads too but loses the decision.
if (ready) {
sentry_user_consent_revoke();
// Drop the held crash envelope: without this it would sit in <database>/cache and
// upload on the next give (e.g. after switching to AlwaysSend), after the user
// explicitly refused it. The SDK recreates the directory as needed.
QDir(sentryDatabasePath + QStringLiteral("/cache")).removeRecursively();
}
#endif
}
bool CrashReporter::sendReport(const QString &redactedPreview, const QString &userNote)
{
#if COCKATRICE_WITH_CRASH_REPORTING
if (!ready) {
return false;
}
// Local scope (not the global one): the log preview rides only on this event. A global
// sentry_attach_bytes would linger and ride along on every later event in the session,
// including a real native crash later on — and with the crashpad backend byte attachments
// are written to disk for exactly that. In-memory bytes also mean no report files land in
// the Sentry DB dir, so there is no disk growth and no same-second filename collision.
sentry_scope_t *scope = sentry_scope_new();
if (scope == nullptr) {
qCWarning(CrashReportLog) << "Could not create scope for crash report";
return false;
}
const QByteArray previewBytes = redactedPreview.toUtf8();
const sentry_uuid_t attachmentId = sentry_scope_attach_bytes(
scope, previewBytes.constData(), static_cast<size_t>(previewBytes.size()), "crash-log.txt");
const bool attachOk = sentry_uuid_is_nil(&attachmentId) == 0;
if (!attachOk) {
qCWarning(CrashReportLog) << "Could not attach crash log to crash report";
}
bool noteAttachOk = true;
if (!userNote.isEmpty()) {
const QByteArray noteBytes = userNote.toUtf8();
const sentry_uuid_t noteId = sentry_scope_attach_bytes(scope, noteBytes.constData(),
static_cast<size_t>(noteBytes.size()), "user-note.txt");
noteAttachOk = sentry_uuid_is_nil(&noteId) == 0;
if (!noteAttachOk) {
qCWarning(CrashReportLog) << "Could not attach user note to crash report";
}
}
// Release is attached automatically from the init options. Deliberately no
// sentry_set_user call: the report must carry no PII. The dialog collects
// no contact info, so the message event + attachments are the whole payload
// (sentry_value_new_feedback/sentry_capture_feedback exist but stay unused).
sentry_value_t event = sentry_value_new_message_event(SENTRY_LEVEL_ERROR, "crash-report", "Crash report");
const sentry_uuid_t eventId = sentry_scope_capture_event(scope, event, nullptr);
sentry_scope_free(scope);
if (sentry_uuid_is_nil(&eventId)) {
qCWarning(CrashReportLog) << "Failed to capture crash report event";
return false;
}
char eventIdStr[37] = {};
sentry_uuid_as_string(&eventId, eventIdStr);
// Capture only queues the envelope to the background transport: a network failure stays
// invisible here, so true means "queued", not "delivered". The Save fallback is offered only
// when the local capture itself fails.
qCInfo(CrashReportLog) << "Crash report queued, event id:" << eventIdStr;
// A failed attachment still captures the event above, but reports failure
// so callers offer the local-save fallback instead of silent success.
return attachOk && noteAttachOk;
#else
Q_UNUSED(redactedPreview);
Q_UNUSED(userNote);
return false;
#endif
}
bool CrashReporter::appendAttachmentToEnvelope(const QString &envelopePath,
const QString &filename,
const QByteArray &payload)
{
// QIODevice::ReadWrite creates a missing file; appending to a file that was never
// written by the SDK would fabricate an envelope, so require it to exist.
if (!QFile::exists(envelopePath)) {
return false;
}
QFile file(envelopePath);
if (!file.open(QIODevice::ReadWrite)) {
return false;
}
// Item boundaries are single newlines: make sure the header starts on a fresh line.
if (file.size() > 0) {
if (!file.seek(file.size() - 1)) {
return false;
}
char lastByte = 0;
if (file.read(&lastByte, 1) != 1) {
return false;
}
if (lastByte != '\n' && file.write("\n", 1) != 1) {
return false;
}
}
// Item shape mirrors sentry__envelope_add_attachment in the SDK. `length` is what the
// SDK's envelope re-parser uses to frame the payload (a missing length ends the item at
// the next newline, which would truncate a multi-line log and make the rest of the file
// fail to parse); `attachment_length` is the logical attachment size the SDK also emits.
// The filename is a compile-time ASCII constant from the callers, so no JSON escaping is
// needed.
const QByteArray header = QByteArrayLiteral("{\"type\":\"attachment\",\"length\":") +
QByteArray::number(payload.size()) + QByteArrayLiteral(",\"attachment_length\":") +
QByteArray::number(payload.size()) + QByteArrayLiteral(",\"filename\":\"") +
filename.toUtf8() + QByteArrayLiteral("\",\"content_type\":\"text/plain\"}") + '\n';
if (file.write(header) != header.size()) {
return false;
}
return file.write(payload) == payload.size();
}
QStringList CrashReporter::pendingEnvelopePaths(const QString &databasePath)
{
static const auto isUuid = [](const QString &id) { return id.size() == 36 && id.count(QLatin1Char('-')) == 4; };
static const auto isEnvelopeName = [](const QString &name) {
if (name == QStringLiteral("__sentry-crash.envelope")) {
return false;
}
const QString stem = name.left(name.size() - 9); // strip ".envelope"
if (isUuid(stem)) {
return true;
}
// Retry cache filename: "<ts>-<count>-<uuid>".
const qsizetype firstSep = stem.indexOf(QLatin1Char('-'));
const qsizetype secondSep = firstSep > 0 ? stem.indexOf(QLatin1Char('-'), firstSep + 1) : -1;
return firstSep > 0 && secondSep > 0 && isUuid(stem.mid(secondSep + 1)) && stem.left(firstSep).toLongLong() > 0;
};
QStringList result;
const QDir dbDir(databasePath);
if (!dbDir.exists()) {
return result;
}
// Consent-held envelopes (Ask mode: the SDK cached them at crash time instead
// of sending, see sentry__submit_envelope). Bare "<uuid>.envelope" with
// cache_keep only, "<ts>-<count>-<uuid>.envelope" with http_retry.
const QDir cacheDir(databasePath + QStringLiteral("/cache"));
const QStringList cacheNames = cacheDir.entryList({QStringLiteral("*.envelope")}, QDir::Files, QDir::Name);
for (const QString &name : cacheNames) {
if (isEnvelopeName(name)) {
result.append(cacheDir.filePath(name));
}
}
// In-flight envelopes dumped to the crashed run's own directory (AlwaysSend
// mode: consent was already given, so the transport dump wrote them there and
// sentry_init's process_old_runs replays them at startup — attach before init).
const QStringList runDirs =
dbDir.entryList({QStringLiteral("*.run")}, QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name);
for (const QString &runDir : runDirs) {
const QDir dir(dbDir.filePath(runDir));
const QStringList names = dir.entryList({QStringLiteral("*.envelope")}, QDir::Files, QDir::Name);
for (const QString &name : names) {
if (isEnvelopeName(name)) {
result.append(dir.filePath(name));
}
}
}
return result;
}
int CrashReporter::attachToPendingCrashEnvelopes(const QString &redactedLogText, const QString &userNoteText)
{
return attachToPendingCrashEnvelopes(sentryDatabasePath, redactedLogText, userNoteText);
}
int CrashReporter::attachToPendingCrashEnvelopes(const QString &databasePath,
const QString &redactedLogText,
const QString &userNoteText)
{
if (databasePath.isEmpty()) {
return 0;
}
const QByteArray logBytes = redactedLogText.toUtf8();
const QByteArray noteBytes = userNoteText.toUtf8();
int attached = 0;
const QStringList envelopes = pendingEnvelopePaths(databasePath);
for (const QString &path : envelopes) {
bool ok = appendAttachmentToEnvelope(path, QStringLiteral("crash-log.txt"), logBytes);
if (ok && !noteBytes.isEmpty()) {
ok = appendAttachmentToEnvelope(path, QStringLiteral("user-note.txt"), noteBytes);
}
if (ok) {
++attached;
} else {
qCWarning(CrashReportLog) << "Failed to attach crash log to pending envelope" << path;
}
}
if (attached > 0) {
qCInfo(CrashReportLog) << "Attached crash log and user note to" << attached << "pending crash envelope(s)";
}
return attached;
}
QString CrashReporter::sanitizeLogLine(const QString &line)
{
static const auto colorEscapeCodePattern = QRegularExpression(QStringLiteral("\033\\[[0-9;]*m"));
QString sanitized = line;
sanitized.replace(colorEscapeCodePattern, "");
return sanitized;
}
QString CrashReporter::redactCockatriceUrls(const QString &line)
{
static auto urlPattern = QRegularExpression(QStringLiteral("cockatrice://\\S+"));
// Single pass over the original line: rescanning the replaced text would match a redacted
// secret-free URL again and loop forever.
QString redacted;
redacted.reserve(line.size());
qsizetype pos = 0;
QRegularExpressionMatchIterator it = urlPattern.globalMatch(line);
while (it.hasNext()) {
QRegularExpressionMatch match = it.next();
redacted.append(line.mid(pos, match.capturedStart() - pos));
// The pattern only matches cockatrice:// URLs, so the check is folded into the parse:
// scheme://host, secrets dropped.
const QUrl parsed(match.captured(0));
redacted.append(parsed.scheme() + QStringLiteral("://") + parsed.host());
pos = match.capturedEnd();
}
redacted.append(line.mid(pos));
return redacted;
}

View file

@ -1,177 +0,0 @@
/**
* @file crash_reporter.h
* @ingroup Core
*/
#ifndef CRASH_REPORTER_H
#define CRASH_REPORTER_H
#include <QByteArray>
#include <QLoggingCategory>
#include <QString>
#include <QStringList>
#include <libcockatrice/settings/personal_settings.h>
#include <memory>
#ifndef COCKATRICE_WITH_CRASH_REPORTING
#define COCKATRICE_WITH_CRASH_REPORTING 0
#endif
#ifndef COCKATRICE_SENTRY_DSN
#define COCKATRICE_SENTRY_DSN ""
#endif
#ifndef COCKATRICE_SENTRY_RELEASE
#define COCKATRICE_SENTRY_RELEASE ""
#endif
class QLockFile;
Q_DECLARE_LOGGING_CATEGORY(CrashReportLog);
/**
* @brief Sentry-backed crash-report facade, owned by main().
*
* An empty DSN (or Never consent) skips the SDK entirely, so the mock dialog
* fallback behaves exactly as before. All SDK calls are guarded so OFF builds
* compile to no-ops.
*
* @par Consent gating
* init() arms require-user-consent and before_send scrubbing. Stale persisted
* consent from a previous AlwaysSend run is cleared before init when the mode is
* not AlwaysSend, and cached crash envelopes are held for the dialog instead of
* being dropped. giveConsent()/revokeConsent() gate uploads.
*
* @par Envelope and log helpers
* attachToPendingCrashEnvelopes() folds the redacted log and the optional user
* note into the pending native crash event(s) before consent releases them, so
* dump, stack, log and note arrive as ONE Sentry event. sendReport() is the
* fallback for unclean exits that produced no native crash event, capturing a
* redacted message event with the log preview attached. The log redaction
* helpers are static and SDK-independent.
*
* @par Session tracking
* beginSession()/endSession() are SDK-independent: a QLockFile per process in
* the database dir detects unclean exits cross-platform, replacing the legacy
* Windows-only .dmp scan. The held lock makes the reporter non-copyable and
* non-movable.
*/
class CrashReporter
{
public:
CrashReporter();
CrashReporter(const CrashReporter &) = delete;
CrashReporter &operator=(const CrashReporter &) = delete;
CrashReporter(CrashReporter &&) = delete;
CrashReporter &operator=(CrashReporter &&) = delete;
~CrashReporter();
void init(const QString &dsn,
const QString &release,
const QString &databasePath,
PersonalSettings::CrashReportingMode consentMode);
[[nodiscard]] bool isReady() const;
void shutdown();
void giveConsent();
void revokeConsent();
bool sendReport(const QString &redactedPreview, const QString &userNote = QString());
/**
* @brief Append the redacted log (and optional user note) to every pending
* native crash envelope in the local database, so the crashed event uploads
* with dump, stack, log and note together instead of the log arriving as a
* separate event that has to be correlated by hand. Both storage locations
* are covered: consent-held envelopes in <database>/cache (Ask mode) and
* in-flight envelopes dumped into <database>/<run>.run (AlwaysSend mode).
*
* For the AlwaysSend variant run this BEFORE init() (static overload): the
* startup replay of *.run envelopes happens inside sentry_init. SDK-independent
* plain-file surgery on the Sentry envelope format, so it stays unit-testable
* in OFF builds.
*
* @return the number of envelopes that received the attachments.
*/
int attachToPendingCrashEnvelopes(const QString &redactedLogText, const QString &userNoteText);
static int attachToPendingCrashEnvelopes(const QString &databasePath,
const QString &redactedLogText,
const QString &userNoteText);
/**
* @brief Append one attachment item to a cached Sentry envelope file.
*
* An envelope is a newline-separated stream of item-header-line + payload
* pairs. The retry transport replays cached envelopes byte-for-byte
* (sentry-native 0.17.3 reads them raw and serializes raw envelopes back
* verbatim), so appending well-formed items associates them with the event
* already in the file; the first line (event id header) is never touched.
*
* @param filename ASCII-only item filename, embedded verbatim in the header.
*/
static bool
appendAttachmentToEnvelope(const QString &envelopePath, const QString &filename, const QByteArray &payload);
/**
* @brief Pending crash envelopes in <database>: the consent-held files in
* <database>/cache and the in-flight files in <database>/<run>.run.
*
* Matches the cache filename conventions sentry-native accepts (see
* sentry__parse_cache_filename): bare "<uuid>.envelope" and retry
* "<ts>-<count>-<uuid>.envelope"; run dirs accept plain "<uuid>.envelope"
* minus the SDK's own "__sentry-crash.envelope" marker.
*/
static QStringList pendingEnvelopePaths(const QString &databasePath);
/**
* @brief SDK-independent gating decision, unit-testable in OFF builds.
*/
static bool isReportingEnabled(const QString &dsn, PersonalSettings::CrashReportingMode consentMode);
/**
* @brief Whether a DSN has the shape the build and the SDK require:
* <scheme>://<key>@<host>/<project-id>.
*
* Deliberately the same unanchored pattern cmake/CrashReporting.cmake matches (CMake's MATCHES
* searches rather than anchoring), so a DSN this build accepted is never rejected here. The SDK
* refuses to even build a request for a DSN it considers invalid, so an unchecked typo disables
* uploading with no error anywhere.
*/
static bool isDsnWellFormed(const QString &dsn);
/**
* @brief Human-readable destination of a DSN, e.g. "o0.ingest.sentry.io (project 1234567)".
*
* Safe to log: the public key is never part of the result. Empty for a malformed DSN.
*/
static QString describeDsn(const QString &dsn);
/**
* @brief Describe the captured-for DSN of a pending envelope when it points at a different
* destination than the DSN this session would upload it with; empty when they agree.
*
* sentry-native posts every envelope with the DSN held in the transport state of the session
* doing the sending (envelopes restored from disk are raw and their own "dsn" header is never
* read), so a cached crash envelope captured for one project can silently be uploaded to
* another - the exact way a tester's report lands in the wrong place. Compares destinations
* (host and project id), not raw strings, so a different key for the same project stays quiet.
*/
static QString describeOriginMismatch(const QString &envelopePath, const QString &currentDsn);
/**
* @brief Claim this process's session lock, reporting a previous unclean exit.
* @return true if a leftover session lock from a dead process (or an unreadable
* garbage file) was found and cleaned; live concurrent instances are ignored.
*/
bool beginSession(const QString &dir);
void endSession();
static QString sanitizeLogLine(const QString &line);
static QString redactCockatriceUrls(const QString &line);
private:
bool ready = false;
QString sentryDatabasePath;
QString sessionLockPath;
std::unique_ptr<QLockFile> sessionLock;
};
#endif

View file

@ -14,7 +14,6 @@
#include <QObject>
#include <QPoint>
#include <QVBoxLayout>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
LatencyStatusWidget::LatencyStatusWidget(QWidget *parent) : QWidget(parent)
@ -25,7 +24,9 @@ LatencyStatusWidget::LatencyStatusWidget(QWidget *parent) : QWidget(parent)
latencyGraph = new LatencyGraphWidget(this);
latencyGraph->setFixedSize(90, 14);
auto *layout = QtUtils::makeLayout<QHBoxLayout>(this, 4);
auto *layout = new QHBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(4);
layout->addWidget(latencyGraph);
layout->addWidget(pingLabel);

View file

@ -3,7 +3,6 @@
#include "../../../interface/pixel_map_generator.h"
#include "../../settings/cache_settings.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_request.h"
#include "../interface/widgets/dialogs/dlg_forgot_password_reset.h"
@ -25,7 +24,6 @@
#include <libcockatrice/network/client/remote/remote_client.h>
#include <libcockatrice/protocol/pb/response.pb.h>
#include <libcockatrice/settings/servers_settings.h>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
class QWidget;
@ -97,13 +95,6 @@ void ConnectionController::wireClientSignals()
connect(remoteClient, &RemoteClient::sigPromptForForgotPasswordChallenge, this,
&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()
@ -112,32 +103,16 @@ void ConnectionController::connectToServer()
connect(dlgConnect, &DlgConnect::sigStartForgotPasswordRequest, this, &ConnectionController::forgotPasswordRequest);
if (dlgConnect->exec()) {
pendingSaveName = dlgConnect->getSaveName();
pendingSavePassword = dlgConnect->getSavePassword();
remoteClient->setStoredVerifier(dlgConnect->getStoredVerifier());
remoteClient->connectToServer(dlgConnect->getHost(), static_cast<unsigned int>(dlgConnect->getPort()),
dlgConnect->getPlayerName(), dlgConnect->getPassword());
}
}
void ConnectionController::onPasswordVerifierReady(const QString &verifier)
{
if (pendingSavePassword) {
SettingsCache::instance().servers().setServerPassword(pendingSaveName, verifier);
}
}
void ConnectionController::connectToServerDirect(const QString &host,
unsigned int port,
const QString &playerName,
const QString &password,
const QString &storedVerifier,
const QString &saveName,
bool savePassword)
const QString &password)
{
pendingSaveName = saveName;
pendingSavePassword = savePassword;
remoteClient->setStoredVerifier(storedVerifier);
remoteClient->connectToServer(host, port, playerName, password);
}
@ -249,27 +224,17 @@ void ConnectionController::onLoginError(int r,
{
switch (static_cast<Response::ResponseCode>(r)) {
case Response::RespClientUpdateRequired: {
QString formatted = "Missing Features: ";
for (int i = 0; i < missingFeatures.size(); ++i) {
formatted.append(QString("\n %1").arg(QChar(0x2022)) + " " + missingFeatures.value(i));
}
QMessageBox msgBox(dialogParent);
msgBox.setIcon(QMessageBox::Critical);
msgBox.setWindowTitle(tr("Failed Login"));
if (missingFeatures.isEmpty()) {
// Without a feature list this is either a login the client stopped itself or a
// strict server asking for a password; the reason string already says which and
// what to do. No client update can fix any of those causes, so show it instead of
// the update advice.
msgBox.setText(reasonStr.isEmpty() ? tr("The login did not complete.") : reasonStr);
} else {
QString formatted = tr("Missing Features:");
for (const QString &feature : missingFeatures) {
formatted.append(QString("\n %1 ").arg(QChar(0x2022)) + feature);
}
msgBox.setText(tr("Your client seems to be missing features this server requires for connection.") +
"\n\n" + tr("To update your client, go to 'Help -> Check for Client Updates'."));
msgBox.setDetailedText(formatted);
}
msgBox.setText(tr("Your client seems to be missing features this server requires for connection.") +
"\n\n" + tr("To update your client, go to 'Help -> Check for Client Updates'."));
msgBox.setDetailedText(formatted);
msgBox.exec();
break;
}
@ -316,8 +281,9 @@ void ConnectionController::onLoginError(int r,
}
case Response::RespRegistrationRequired: {
if (QtUtils::confirm(dialogParent, tr("Error"),
tr("This server requires user registration. Do you want to register now?"))) {
if (QMessageBox::question(dialogParent, tr("Error"),
tr("This server requires user registration. Do you want to register now?"),
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
registerToServer();
}
return; // don't re-prompt connect
@ -356,16 +322,10 @@ void ConnectionController::onLoginError(int r,
}
case Response::RespPasswordChangeRequired: {
// The credentials were right, but the account may not be used until a new password
// is chosen. Let the user complete the change here: sending them off to "ask the
// administrator for your temporary password" sends them in circles, since they just
// 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
}
QMessageBox::information(
dialogParent, tr("Password Change Required"),
tr("An administrator has reset your password. Please contact your server administrator to obtain "
"your temporary password, then log in and change it via Account -> Change Password."));
remoteClient->disconnectFromServer();
return;
}
@ -528,43 +488,6 @@ void ConnectionController::onNotifyUserAboutUpdate()
"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()
{
QMessageBox::information(
@ -575,38 +498,14 @@ void ConnectionController::onForgotPasswordSuccess()
SettingsCache::instance().servers().setFPPlayerName("");
}
void ConnectionController::onForgotPasswordError(int responseCode)
void ConnectionController::onForgotPasswordError()
{
// Rate limiting is retryable, so it must not clear the prefill.
bool keepPrefill = false;
QString message;
switch (static_cast<Response::ResponseCode>(responseCode)) {
case Response::RespTooManyRequests:
// Retryable: keep the host/port/user prefill so the user can try again. The window
// is server-configured (an hour by default), so do not promise a shorter wait.
keepPrefill = true;
message = tr("Too many password reset attempts from your address. Please wait before "
"requesting a new reset.");
break;
case Response::RespFunctionNotAllowed:
// Also returned for an unknown user, a banned user, and an expired token or
// challenge, so the message must not claim a single cause.
message = tr("The server did not accept the reset request. Check the user name, or contact "
"the server operator to reset your password.");
break;
default:
message = tr("Failed to reset user account password, please contact the server operator to reset your "
"password.");
break;
}
QMessageBox::warning(dialogParent, tr("Reset Password"), message);
if (!keepPrefill) {
SettingsCache::instance().servers().setFPHostName("");
SettingsCache::instance().servers().setFPPort("");
SettingsCache::instance().servers().setFPPlayerName("");
}
QMessageBox::warning(
dialogParent, tr("Reset Password"),
tr("Failed to reset user account password, please contact the server operator to reset your password."));
SettingsCache::instance().servers().setFPHostName("");
SettingsCache::instance().servers().setFPPort("");
SettingsCache::instance().servers().setFPPlayerName("");
}
void ConnectionController::onPromptForgotPasswordReset()

View file

@ -40,13 +40,8 @@ public:
void registerToServer();
void forgotPasswordRequest();
void connectToServer();
void connectToServerDirect(const QString &host,
unsigned int port,
const QString &playerName,
const QString &password,
const QString &storedVerifier = QString(),
const QString &saveName = QString(),
bool savePassword = false);
void
connectToServerDirect(const QString &host, unsigned int port, const QString &playerName, const QString &password);
void disconnectFromServer();
void refreshWindowTitle()
@ -87,17 +82,10 @@ private slots:
// Forgot-password flow
void onForgotPasswordSuccess();
void onForgotPasswordError(int responseCode);
void onForgotPasswordError();
void onPromptForgotPasswordReset();
void onPromptForgotPasswordChallenge();
// Persists the derived scrypt verifier after a successful challenge-response login
void onPasswordVerifierReady(const QString &verifier);
// Completes the forced password change the server demanded at login
void onPasswordChangeSuccess();
void onPasswordChangeError(int responseCode);
private:
void wireClientSignals();
void updateWindowTitle();
@ -114,10 +102,6 @@ private:
// Kept as a member so the forgot-password signal can be wired to it
DlgConnect *dlgConnect{nullptr};
// Captured from the connect dialog when a connection is initiated
QString pendingSaveName;
bool pendingSavePassword{false};
};
#endif // COCKATRICE_REMOTE_CONNECTION_CONTROLLER_H

View file

@ -20,12 +20,11 @@
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/deck_list/deck_list.h>
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
#include <libcockatrice/network/network_manager.h>
#include <version_string.h>
DeckStatsInterface::DeckStatsInterface(QObject *parent) : QObject(parent)
{
manager = NetworkManagerFactory::create(this);
manager = new QNetworkAccessManager(this);
connect(manager, &QNetworkAccessManager::finished, this, &DeckStatsInterface::queryFinished);
}

View file

@ -26,12 +26,11 @@
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/deck_list/deck_list.h>
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
#include <libcockatrice/network/network_manager.h>
#include <version_string.h>
TappedOutInterface::TappedOutInterface(QObject *parent) : QObject(parent)
{
manager = NetworkManagerFactory::create(this);
manager = new QNetworkAccessManager(this);
connect(manager, &QNetworkAccessManager::finished, this, &TappedOutInterface::queryFinished);
}

View file

@ -27,7 +27,6 @@
#include <functional>
#include <libcockatrice/card/database/card_database.h>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/network/network_manager.h>
#include <libcockatrice/settings/download_settings.h>
#include <libcockatrice/settings/paths_settings.h>
#include <tuple>
@ -57,7 +56,7 @@ void SpoilerBackgroundUpdater::startSpoilerDownloadProcess(QString url, bool sav
void SpoilerBackgroundUpdater::downloadFromURL(QUrl url, bool saveResults)
{
auto *nam = NetworkManagerFactory::create(this);
auto *nam = new QNetworkAccessManager(this);
auto request = QNetworkRequest(url);
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
QNetworkReply *reply = nam->get(request);

View file

@ -20,7 +20,6 @@
#include <QVariant>
#include <QVariantMap>
#include <QtGlobal>
#include <libcockatrice/network/network_manager.h>
#include <optional>
#include <qnamespace.h>
#include <qprocessordetection.h>
@ -63,7 +62,7 @@ std::optional<int> getProductVersionMajor()
} // namespace
ReleaseChannel::ReleaseChannel() : netMan(NetworkManagerFactory::create(this)), response(nullptr), lastRelease(nullptr)
ReleaseChannel::ReleaseChannel() : netMan(new QNetworkAccessManager(this)), response(nullptr), lastRelease(nullptr)
{
}

View file

@ -9,11 +9,10 @@
#include <QNetworkRequest>
#include <QUrl>
#include <QVariant>
#include <libcockatrice/network/network_manager.h>
UpdateDownloader::UpdateDownloader(QObject *parent) : QObject(parent), response(nullptr)
{
netMan = NetworkManagerFactory::create(this);
netMan = new QNetworkAccessManager(this);
}
void UpdateDownloader::beginDownload(QUrl downloadUrl)

View file

@ -19,6 +19,7 @@ inline Q_LOGGING_CATEGORY(SettingsCacheLog, "settings_cache");
class ReleaseChannel;
#define DEFAULT_LANG_NAME "English"
#define CLIENT_INFO_NOT_SET "notset"
#define DEFAULT_FONT_SIZE 12

View file

@ -2,18 +2,13 @@
#define COCKATRICE_SETTINGS_CARD_PREFERENCE_PROVIDER_H
#include "../../client/settings/cache_settings.h"
#include <QObject>
#include <libcockatrice/interfaces/interface_card_preference_provider.h>
#include <libcockatrice/settings/card_override_settings.h>
#include <libcockatrice/settings/cards_display_settings.h>
class SettingsCardPreferenceProvider : public QObject, public ICardPreferenceProvider
class SettingsCardPreferenceProvider : public ICardPreferenceProvider
{
public:
explicit SettingsCardPreferenceProvider(QObject *parent = nullptr) : QObject(parent)
{
}
[[nodiscard]] QString getCardPreferenceOverride(const QString &cardName) const override
{
return SettingsCache::instance().cardOverrides().getCardPreferenceOverride(cardName);

View file

@ -1,5 +1,6 @@
#include "deck_filter_string.h"
#include "../interface/deck_loader/loaded_deck.h"
#include "libcockatrice/card/card_localization.h"
#include "libcockatrice/card/database/card_database_querier.h"
#include "libcockatrice/deck_list/deck_list.h"
@ -14,7 +15,6 @@
#include <algorithm>
#include <any>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/deck_list/loaded_deck.h>
#include <libcockatrice/filters/filter_string.h>
#include <libcockatrice/utility/peglib.h>
#include <mutex>

View file

@ -11,7 +11,6 @@
#include <QString>
#include <functional>
#include <libcockatrice/card/card_localization.h>
#include <libcockatrice/deck_list/loaded_deck.h>
struct CardSearchLanguage;
struct LoadedDeck;

View file

@ -13,7 +13,6 @@
#include <QVariant>
#include <QtGlobal>
#include <libcockatrice/filters/filter_card.h>
#include <libcockatrice/utility/qt_utils.h>
#include <stddef.h>
FilterBuilder::FilterBuilder(QWidget *parent) : QWidget(parent)
@ -40,8 +39,9 @@ FilterBuilder::FilterBuilder(QWidget *parent) : QWidget(parent)
edit->setClearButtonEnabled(true);
edit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
QGridLayout *layout = QtUtils::makeLayout<QGridLayout>();
QGridLayout *layout = new QGridLayout;
layout->setObjectName("layout");
layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(typeCombo, 0, 0, 1, 2);
layout->addWidget(filterCombo, 0, 2, 1, 2);

View file

@ -11,27 +11,6 @@
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)
{
fTree = new FilterTree;
@ -131,31 +110,46 @@ void FilterTreeModel::clearFiltersOfType(CardFilter::Attr filterType)
QList<const CardFilter *> FilterTreeModel::getFiltersOfType(CardFilter::Attr filterType) const
{
QList<const CardFilter *> filters;
forEachFilter(fTree, [&](const FilterItem *filterItem) {
if (filterItem->attr() == filterType) {
filters.append(new CardFilter(filterItem->text(), filterItem->type(), filterItem->attr()));
}
});
return filters;
}
if (!fTree) {
return filters;
}
QStringList FilterTreeModel::getFilterTermsOfType(CardFilter::Attr filterType) const
{
QStringList terms;
forEachFilter(fTree, [&](const FilterItem *filterItem) {
if (filterItem->attr() == filterType) {
terms.append(filterItem->text());
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()));
}
}
});
return terms;
for (int i = 0; i < node->childCount(); ++i) {
traverse(node->nodeAt(i));
}
};
traverse(fTree);
return filters;
}
QList<const CardFilter *> FilterTreeModel::allFilters() const
{
QList<const CardFilter *> filters;
forEachFilter(fTree, [&](const FilterItem *filterItem) {
filters.append(new CardFilter(filterItem->text(), filterItem->type(), filterItem->attr()));
});
if (!fTree) {
return filters;
}
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;
}

View file

@ -28,14 +28,7 @@ public slots:
void addFilter(const CardFilter *f);
void removeFilter(const CardFilter *f);
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;
/// @brief Copies every active filter into new CardFilter objects. The caller owns them.
[[nodiscard]] QList<const CardFilter *> allFilters() const;
private slots:
@ -55,13 +48,6 @@ public:
{
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 columnCount(const QModelIndex & /*parent*/ = QModelIndex()) const override;
[[nodiscard]] QVariant data(const QModelIndex &index, int role) const override;

View file

@ -18,6 +18,8 @@ AbstractGame::AbstractGame(QObject *_parent) : QObject(_parent)
{
gameMetaInfo = new GameMetaInfo(this);
gameEventHandler = new GameEventHandler(this);
activeCard = nullptr;
}
bool AbstractGame::isHost() const
@ -46,7 +48,12 @@ void AbstractGame::loadReplay(const GameReplay *replay)
gameMetaInfo->setSpectatorsOmniscient(true);
}
CardState *AbstractGame::getCard(int playerId, const QString &zoneName, int cardId) const
void AbstractGame::setActiveCard(CardItem *card)
{
activeCard = card;
}
CardItem *AbstractGame::getCard(int playerId, const QString &zoneName, int cardId) const
{
PlayerLogic *player = playerManager->getPlayer(playerId);
if (!player) {

View file

@ -11,14 +11,14 @@
#include <QString>
#include <qtmetamacros.h>
class CardItem;
class AbstractClient;
class CardState;
class GameEventHandler;
class GameMetaInfo;
class GameReplay;
class GameState;
class PlayerManager;
class CardState;
class AbstractGame : public QObject
{
Q_OBJECT
@ -30,6 +30,7 @@ public:
GameState *gameState;
GameEventHandler *gameEventHandler;
PlayerManager *playerManager;
CardItem *activeCard;
GameMetaInfo *getGameMetaInfo()
{
@ -57,7 +58,13 @@ public:
void loadReplay(const GameReplay *replay);
CardState *getCard(int playerId, const QString &zoneName, int cardId) const;
CardItem *getCard(int playerId, const QString &zoneName, int cardId) const;
void setActiveCard(CardItem *card);
CardItem *getActiveCard() const
{
return activeCard;
}
};
#endif // COCKATRICE_ABSTRACT_GAME_H

View file

@ -1,9 +1,9 @@
#include "arrow_registry.h"
#include "arrow_item.h"
#include "../game_graphics/board/arrow_item.h"
#include "board/arrow_data.h"
#include <QSharedPointer>
#include <libcockatrice/game/board/arrow_data.h>
void ArrowRegistry::insert(QSharedPointer<ArrowData> data, ArrowItem *arrow)
{

View file

@ -4,7 +4,6 @@
#include <QList>
#include <QMap>
#include <QSet>
#include <libcockatrice/game/board/arrow_data.h>
class ArrowItem;
struct ArrowData;

View file

@ -1,6 +1,7 @@
#include "card_list.h"
#include "card_state.h"
#include "../../game_graphics/board/card_item.h"
#include "libcockatrice/card/printing/exact_card.h"
#include <QChar>
#include <QDebug>
@ -10,26 +11,26 @@
#include <compare>
#include <libcockatrice/card/card_info.h>
CardList::CardList(bool _contentsKnown) : QList<CardState *>(), contentsKnown(_contentsKnown)
CardList::CardList(bool _contentsKnown) : QList<CardItem *>(), contentsKnown(_contentsKnown)
{
}
/**
* @brief Finds the CardState with the given id in the list.
* @brief Finds the CardItem with the given id in the list.
* If contentsKnown is false, then this just returns the first element of the list.
*
* @param cardId The id of the card to find.
*
* @returns A pointer to the CardState, or a nullptr if not found.
* @returns A pointer to the CardItem, or a nullptr if not found.
*/
CardState *CardList::findCard(const int cardId) const
CardItem *CardList::findCard(const int cardId) const
{
if (!contentsKnown && !empty()) {
return at(0);
} else {
for (auto *cardState : *this) {
if (cardState->getId() == cardId) {
return cardState;
for (auto *cardItem : *this) {
if (cardItem->getId() == cardId) {
return cardItem;
}
}
}
@ -37,7 +38,7 @@ CardState *CardList::findCard(const int cardId) const
}
/**
* @brief sorts the list by using string comparison on properties extracted from the CardState
* @brief sorts the list by using string comparison on properties extracted from the CardItem
* The cards are compared using each property in order.
* If two cards have the same value for a property, then the next property in the list is used.
*
@ -50,7 +51,7 @@ void CardList::sortBy(const QList<SortOption> &option)
return;
}
auto comparator = [&option](CardState *a, CardState *b) {
auto comparator = [&option](CardItem *a, CardItem *b) {
for (auto prop : option) {
auto extractor = getExtractorFor(prop);
QString t1 = extractor(a);
@ -115,26 +116,26 @@ static QString getColorSortString(const CardInfo &c, bool appendAtEnd)
}
/**
* @brief returns the function that extracts the given property from the CardState.
* @brief returns the function that extracts the given property from the CardItem.
*/
std::function<QString(CardState *)> CardList::getExtractorFor(SortOption option)
std::function<QString(CardItem *)> CardList::getExtractorFor(SortOption option)
{
switch (option) {
case NoSort:
return [](CardState *) { return ""; };
return [](CardItem *) { return ""; };
case SortByMainType:
return [](CardState *c) { return c->getCardInfo().getMainCardType(); };
return [](CardItem *c) { return c->getCardInfo().getMainCardType(); };
case SortByManaValue:
// getCmc returns the int as a string. We pad with 0's so that string comp also works on it
return [](CardState *c) { return c->getCard() ? c->getCardInfo().getCmc().rightJustified(4, '0') : ""; };
return [](CardItem *c) { return c->getCard() ? c->getCardInfo().getCmc().rightJustified(4, '0') : ""; };
case SortByColorGrouping:
return [](CardState *c) { return c->getCard() ? getColorSortString(c->getCardInfo(), false) : ""; };
return [](CardItem *c) { return c->getCard() ? getColorSortString(c->getCardInfo(), false) : ""; };
case SortByName:
return [](CardState *c) { return c->getName(); };
return [](CardItem *c) { return c->getName(); };
case SortByType:
return [](CardState *c) { return c->getCardInfo().getCardType(); };
return [](CardItem *c) { return c->getCardInfo().getCardType(); };
case SortByManaCost:
return [](CardState *c) {
return [](CardItem *c) {
if (!c->getCard()) {
return QString();
}
@ -146,18 +147,18 @@ std::function<QString(CardState *)> CardList::getExtractorFor(SortOption option)
return QString("%1%2").arg(info.getCmc(), 4, QChar('0')).arg(info.getManaCost());
};
case SortByColors:
return [](CardState *c) { return c->getCard() ? getColorSortString(c->getCardInfo(), true) : ""; };
return [](CardItem *c) { return c->getCard() ? getColorSortString(c->getCardInfo(), true) : ""; };
case SortByPt:
// do the same padding trick as above
return
[](CardState *c) { return c->getCard() ? c->getCardInfo().getPowTough().rightJustified(10, '0') : ""; };
[](CardItem *c) { return c->getCard() ? c->getCardInfo().getPowTough().rightJustified(10, '0') : ""; };
case SortBySet:
return [](CardState *c) { return c->getCardInfo().getSetsNames(); };
return [](CardItem *c) { return c->getCardInfo().getSetsNames(); };
case SortByPrinting:
return [](CardState *c) { return c->getProviderId(); };
return [](CardItem *c) { return c->getProviderId(); };
}
// this line should never be reached
qCWarning(CardListLog) << "cardlist.cpp: Could not find extractor for SortOption" << option;
return [](CardState *) { return ""; };
return [](CardItem *) { return ""; };
}

View file

@ -14,9 +14,9 @@
inline Q_LOGGING_CATEGORY(CardListLog, "card_list");
class CardState;
class CardItem;
class CardList : public QList<CardState *>
class CardList : public QList<CardItem *>
{
protected:
bool contentsKnown;
@ -43,7 +43,7 @@ public:
SortByPrinting
};
explicit CardList(bool _contentsKnown);
CardState *findCard(const int cardId) const;
CardItem *findCard(const int cardId) const;
bool getContentsKnown() const
{
return contentsKnown;
@ -51,7 +51,7 @@ public:
void sortBy(const QList<SortOption> &options);
static std::function<QString(CardState *)> getExtractorFor(SortOption option);
static std::function<QString(CardItem *)> getExtractorFor(SortOption option);
};
#endif

View file

@ -0,0 +1,113 @@
#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();
}

View file

@ -0,0 +1,106 @@
#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

View file

@ -16,16 +16,13 @@ Game::Game(QObject *_parent,
bool isLocalGame,
QList<AbstractClient *> &_clients,
const Event_GameJoined &event,
const QMap<int, QString> &_roomGameTypes,
IInterfaceSettingsProvider *interfaceSettings,
ICardPreferenceProvider *cardPreferences)
const QMap<int, QString> &_roomGameTypes)
: AbstractGame(_parent)
{
gameMetaInfo->setFromProto(event.game_info());
gameMetaInfo->setRoomGameTypes(_roomGameTypes);
gameState = new GameState(this, 0, event.host_id(), isLocalGame, _clients, false, event.resuming(), -1, false);
connect(gameMetaInfo, &GameMetaInfo::startedChanged, gameState, &GameState::onStartedChanged);
playerManager = new PlayerManager(this, event.player_id(), event.judge(), event.spectator(), interfaceSettings,
cardPreferences);
playerManager = new PlayerManager(this, event.player_id(), event.judge(), event.spectator());
gameMetaInfo->setStarted(false);
}

View file

@ -18,9 +18,6 @@ class QString;
template <class Key, class T> class QMap;
template <typename T> class QList;
class ICardPreferenceProvider;
class IInterfaceSettingsProvider;
class Game : public AbstractGame
{
Q_OBJECT
@ -30,9 +27,7 @@ public:
bool isLocalGame,
QList<AbstractClient *> &_clients,
const Event_GameJoined &event,
const QMap<int, QString> &_roomGameTypes,
IInterfaceSettingsProvider *interfaceSettings = nullptr,
ICardPreferenceProvider *cardPreferences = nullptr);
const QMap<int, QString> &_roomGameTypes);
};
#endif // COCKATRICE_GAME_H

View file

@ -28,20 +28,12 @@
#include <libcockatrice/network/client/abstract/abstract_client.h>
#include <libcockatrice/protocol/get_pb_extension.h>
#include <libcockatrice/protocol/pb/command_concede.pb.h>
#include <libcockatrice/protocol/pb/command_deck_select.pb.h>
#include <libcockatrice/protocol/pb/command_delete_arrow.pb.h>
#include <libcockatrice/protocol/pb/command_draw_cards.pb.h>
#include <libcockatrice/protocol/pb/command_game_say.pb.h>
#include <libcockatrice/protocol/pb/command_leave_game.pb.h>
#include <libcockatrice/protocol/pb/command_next_turn.pb.h>
#include <libcockatrice/protocol/pb/command_ready_start.pb.h>
#include <libcockatrice/protocol/pb/command_reverse_turn.pb.h>
#include <libcockatrice/protocol/pb/command_set_active_phase.pb.h>
#include <libcockatrice/protocol/pb/command_set_card_attr.pb.h>
#include <libcockatrice/protocol/pb/command_set_playmat.pb.h>
#include <libcockatrice/protocol/pb/command_set_sideboard_lock.pb.h>
#include <libcockatrice/protocol/pb/command_set_sideboard_plan.pb.h>
#include <libcockatrice/protocol/pb/context_connection_state_changed.pb.h>
#include <libcockatrice/protocol/pb/context_deck_select.pb.h>
#include <libcockatrice/protocol/pb/event_game_closed.pb.h>
#include <libcockatrice/protocol/pb/event_game_host_changed.pb.h>
@ -56,7 +48,6 @@
#include <libcockatrice/protocol/pb/event_set_active_player.pb.h>
#include <libcockatrice/protocol/pb/game_event_container.pb.h>
#include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/utility/zone_names.h>
#include <string>
#include <utility>
@ -232,89 +223,6 @@ void GameEventHandler::handleActivePhaseChanged(int phase)
sendGameCommand(cmd);
}
void GameEventHandler::handleUntapAll()
{
Command_SetCardAttr cmd;
cmd.set_zone(ZoneNames::TABLE);
cmd.set_attribute(AttrTapped);
cmd.set_attr_value("0");
sendGameCommand(cmd);
}
void GameEventHandler::handleDrawCard()
{
Command_DrawCards cmd;
cmd.set_number(1);
sendGameCommand(cmd);
}
PendingCommand *GameEventHandler::prepareDeckSelect(const QString &deckList)
{
Command_DeckSelect cmd;
cmd.set_deck(deckList.toStdString());
return prepareGameCommand(cmd);
}
PendingCommand *GameEventHandler::prepareDeckSelectId(int deckId)
{
Command_DeckSelect cmd;
cmd.set_deck_id(deckId);
return prepareGameCommand(cmd);
}
PendingCommand *GameEventHandler::prepareSetPlaymat(const PlaymatInfo &playmat)
{
Command_SetPlaymat cmd;
auto *pp = cmd.mutable_playmat_params();
pp->set_card_name(playmat.card.name.toStdString());
pp->set_card_provider_id(playmat.card.providerId.toStdString());
pp->set_margin_pct_l(playmat.params.marginPctL);
pp->set_margin_pct_r(playmat.params.marginPctR);
pp->set_vertical_offset(playmat.params.verticalOffset);
pp->set_zoom(playmat.params.zoom);
return prepareGameCommand(cmd);
}
void GameEventHandler::sendReadyStart(bool ready, int playerId)
{
Command_ReadyStart cmd;
cmd.set_ready(ready);
sendGameCommand(cmd, playerId);
}
void GameEventHandler::sendForceStart(int playerId)
{
Command_ReadyStart cmd;
cmd.set_force_start(true);
cmd.set_ready(true);
sendGameCommand(cmd, playerId);
}
void GameEventHandler::sendSetSideboardLock(bool locked, int playerId)
{
Command_SetSideboardLock cmd;
cmd.set_locked(locked);
sendGameCommand(cmd, playerId);
}
void GameEventHandler::sendSetSideboardPlan(const QList<MoveCard_ToZone> &plan, int playerId)
{
Command_SetSideboardPlan cmd;
for (const auto &zone : plan) {
cmd.add_move_list()->CopyFrom(zone);
}
sendGameCommand(cmd, playerId);
}
void GameEventHandler::handleGameLeft()
{
sendGameCommand(Command_LeaveGame());

View file

@ -29,14 +29,13 @@
// GameEventContext, ServerInfo_Player and ServerInfo_User are passed by value
// to signals and slots, so the moc-generated code needs the complete types.
#include <libcockatrice/protocol/pb/game_event_context.pb.h>
#include <libcockatrice/protocol/pb/move_card_to_zone.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_player.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
#include <libcockatrice/utility/playmat_params.h>
class AbstractClient;
class AbstractGame;
class GameEventContainer;
class GameEventContext;
class PendingCommand;
class PlayerLogic;
class Response;
@ -128,12 +127,6 @@ public:
*/
void handleActivePhaseChanged(int phase);
/** @brief Request untapping every permanent on the table. */
void handleUntapAll();
/** @brief Request drawing a single card. */
void handleDrawCard();
/** @brief Leave the current game session. */
void handleGameLeft();
@ -152,70 +145,6 @@ public:
void handleArrowDeletion(int creatorId, int arrowId);
void handleArrowDeletionFinished(const Response &response, int creatorId, int arrowId);
/**
* @brief Prepare a deck selection from its native deck list text.
*
* The returned command is not sent yet, so the caller can connect to
* PendingCommand::finished before handing it to sendGameCommand().
*
* @param deckList Serialized deck list.
* @return The pending command awaiting the deck download response.
*/
PendingCommand *prepareDeckSelect(const QString &deckList);
/**
* @brief Prepare a deck selection for a deck stored on the server.
*
* The returned command is not sent yet, so the caller can connect to
* PendingCommand::finished before handing it to sendGameCommand().
*
* @param deckId Server-side deck identifier.
* @return The pending command awaiting the deck download response.
*/
PendingCommand *prepareDeckSelectId(int deckId);
/**
* @brief Build the playmat command for a player without sending it.
*
* The returned command is not sent yet, so the caller can connect to
* PendingCommand::finished before handing it to sendGameCommand().
*
* @param playmat Resolved playmat card and positioning parameters.
* @return The pending command awaiting the playmat response.
*/
PendingCommand *prepareSetPlaymat(const PlaymatInfo &playmat);
/**
* @brief Set a player's ready state.
*
* @param ready Whether the player is ready.
* @param playerId Player the state applies to.
*/
void sendReadyStart(bool ready, int playerId);
/**
* @brief Force-start the game, kicking every player who is not ready.
*
* @param playerId Player issuing the force start.
*/
void sendForceStart(int playerId);
/**
* @brief Lock or unlock a player's sideboard.
*
* @param locked Whether the sideboard is locked.
* @param playerId Player the lock applies to.
*/
void sendSetSideboardLock(bool locked, int playerId);
/**
* @brief Send a player's sideboard plan.
*
* @param plan Cards to move between zones when leaving sideboarding.
* @param playerId Player the plan belongs to.
*/
void sendSetSideboardPlan(const QList<MoveCard_ToZone> &plan, int playerId);
/** @} */
/** @name Incoming event processing

View file

@ -1,8 +1,13 @@
#include "player_actions.h"
#include "../../client/settings/cache_settings.h"
#include "../../game_graphics/board/card_item.h"
#include "../../game_graphics/dialogs/dlg_create_token.h"
#include "../../game_graphics/dialogs/dlg_move_top_cards_until.h"
#include "../../game_graphics/player/card_menu_action_type.h"
#include "../../game_graphics/zones/table_zone.h"
#include "../../interface/deck_loader/loaded_deck.h"
#include "../abstract_game.h"
#include "../board/arrow_data.h"
#include "../board/card_state.h"
#include "../board/counter_state.h"
#include "../game_event_handler.h"
#include "../zones/card_zone_logic.h"
@ -26,6 +31,8 @@
#include <QChar>
#include <QColor>
#include <QFlags>
#include <QInputDialog>
#include <QKeyEvent>
#include <QList>
#include <QMap>
#include <QMapIterator>
@ -44,15 +51,10 @@
#include <google/protobuf/stubs/port.h>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/card/relation/card_relation.h>
#include <libcockatrice/deck_list/loaded_deck.h>
#include <libcockatrice/interfaces/interface_card_preference_provider.h>
#include <libcockatrice/interfaces/interface_interface_settings_provider.h>
#include <libcockatrice/protocol/pb/command_attach_card.pb.h>
#include <libcockatrice/protocol/pb/command_change_zone_properties.pb.h>
#include <libcockatrice/protocol/pb/command_create_arrow.pb.h>
#include <libcockatrice/protocol/pb/command_create_token.pb.h>
#include <libcockatrice/protocol/pb/command_draw_cards.pb.h>
#include <libcockatrice/protocol/pb/command_dump_zone.pb.h>
#include <libcockatrice/protocol/pb/command_flip_card.pb.h>
#include <libcockatrice/protocol/pb/command_game_say.pb.h>
#include <libcockatrice/protocol/pb/command_inc_counter.pb.h>
@ -62,12 +64,11 @@
#include <libcockatrice/protocol/pb/command_roll_die.pb.h>
#include <libcockatrice/protocol/pb/command_set_card_attr.pb.h>
#include <libcockatrice/protocol/pb/command_set_card_counter.pb.h>
#include <libcockatrice/protocol/pb/command_set_counter.pb.h>
#include <libcockatrice/protocol/pb/command_shuffle.pb.h>
#include <libcockatrice/protocol/pb/command_undo_draw.pb.h>
#include <libcockatrice/protocol/pb/context_move_card.pb.h>
#include <libcockatrice/settings/card_override_settings.h>
#include <libcockatrice/settings/interface_settings.h>
#include <libcockatrice/utility/clamped_arithmetic.h>
#include <libcockatrice/utility/color.h>
#include <libcockatrice/utility/counter_limits.h>
#include <libcockatrice/utility/expression.h>
#include <libcockatrice/utility/zone_names.h>
@ -79,11 +80,8 @@
// milliseconds in between triggers of the move top cards until action
static constexpr int MOVE_TOP_CARD_UNTIL_INTERVAL = 100;
PlayerActions::PlayerActions(PlayerLogic *_player,
IInterfaceSettingsProvider *_interfaceSettings,
ICardPreferenceProvider *_cardPreferences)
: QObject(_player), player(_player), interfaceSettings(_interfaceSettings), cardPreferences(_cardPreferences),
lastTokenTableRow(0), movingCardsUntil(false)
PlayerActions::PlayerActions(PlayerLogic *_player)
: QObject(_player), player(_player), lastTokenTableRow(0), movingCardsUntil(false)
{
connect(this, &PlayerActions::requestZoneViewToggle, player, &PlayerLogic::onRequestZoneViewToggle);
@ -93,27 +91,7 @@ PlayerActions::PlayerActions(PlayerLogic *_player,
connect(moveTopCardTimer, &QTimer::timeout, [this]() { actMoveTopCardToPlay(); });
}
bool PlayerActions::getPlayToStack() const
{
return interfaceSettings ? interfaceSettings->getPlayToStack() : true;
}
int PlayerActions::getStartingHandSize() const
{
return interfaceSettings ? interfaceSettings->getStartingHandSize() : 7;
}
bool PlayerActions::getAnnotateTokens() const
{
return interfaceSettings ? interfaceSettings->getAnnotateTokens() : false;
}
QString PlayerActions::getCardPreferenceOverride(const QString &cardName) const
{
return cardPreferences ? cardPreferences->getCardPreferenceOverride(cardName) : QString();
}
void PlayerActions::playCard(CardState *card, bool faceDown)
void PlayerActions::playCard(CardItem *card, bool faceDown)
{
if (card == nullptr) {
return;
@ -134,7 +112,7 @@ void PlayerActions::playCard(CardState *card, bool faceDown)
const CardInfo &info = exactCard.getInfo();
int tableRow = info.getUiAttributes().tableRow;
bool playToStack = getPlayToStack();
bool playToStack = SettingsCache::instance().userInterface().getPlayToStack();
QString currentZone = card->getZone()->getName();
if (!faceDown && currentZone == ZoneNames::STACK && tableRow == 3) {
cmd.set_target_zone(ZoneNames::GRAVE);
@ -147,7 +125,7 @@ void PlayerActions::playCard(CardState *card, bool faceDown)
cmd.set_y(0);
} else {
tableRow = faceDown ? 2 : info.getUiAttributes().tableRow;
QPoint gridPoint = QPoint(-1, TableZoneLogic::tableRowToGridY(tableRow));
QPoint gridPoint = QPoint(-1, TableZone::tableRowToGridY(tableRow));
cardToMove->set_face_down(faceDown);
if (!faceDown) {
cardToMove->set_pt(info.getPowTough().toStdString());
@ -166,7 +144,7 @@ void PlayerActions::playCard(CardState *card, bool faceDown)
* Like {@link PlayerActions::playCard}, but forces the card to be played to the table zone.
* Cards with tablerow 3 (the stack) will be played to tablerow 1 (the noncreatures row).
*/
void PlayerActions::playCardToTable(const CardState *card, bool faceDown)
void PlayerActions::playCardToTable(const CardItem *card, bool faceDown)
{
if (card == nullptr) {
return;
@ -187,7 +165,7 @@ void PlayerActions::playCardToTable(const CardState *card, bool faceDown)
const CardInfo &info = exactCard.getInfo();
int tableRow = faceDown ? 2 : info.getUiAttributes().tableRow;
QPoint gridPoint = QPoint(-1, TableZoneLogic::tableRowToGridY(tableRow));
QPoint gridPoint = QPoint(-1, TableZone::tableRowToGridY(tableRow));
cardToMove->set_face_down(faceDown);
if (!faceDown) {
cardToMove->set_pt(info.getPowTough().toStdString());
@ -199,60 +177,6 @@ void PlayerActions::playCardToTable(const CardState *card, bool faceDown)
sendGameCommand(cmd);
}
void PlayerActions::attachCards(CardState *startCard, const CardState *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) {
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());
sendGameCommand(cmd);
}
void PlayerActions::createArrow(CardState *startCard, const ArrowData &arrow, int deleteInPhase)
{
Command_CreateArrow cmd;
cmd.mutable_arrow_color()->CopyFrom(convertQColorToColor(arrow.color));
cmd.set_start_player_id(arrow.startPlayerId);
cmd.set_start_zone(arrow.startZone.toStdString());
cmd.set_start_card_id(arrow.startCardId);
cmd.set_target_player_id(arrow.targetPlayerId);
if (!arrow.isPlayerTargeted()) {
cmd.set_target_zone(arrow.targetZone.toStdString());
cmd.set_target_card_id(arrow.targetCardId);
}
// if the card is in hand then it has just been moved to the stack or table as part of drawing the arrow
if (arrow.startZone == ZoneNames::HAND) {
CardInfoPtr ci = startCard->getCard().getCardPtr();
bool playToStack = 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) {
cmd.set_delete_in_phase(deleteInPhase);
}
sendGameCommand(cmd);
}
void PlayerActions::actViewLibrary()
{
emit requestZoneViewToggle(ZoneNames::DECK, -1);
@ -431,7 +355,7 @@ void PlayerActions::actDrawCard()
void PlayerActions::actRequestMulliganDialog()
{
int startSize = getStartingHandSize();
int startSize = SettingsCache::instance().userInterface().getStartingHandSize();
int handSize = player->getHandZone()->getCards().size();
int deckSize = player->getDeckZone()->getCards().size() + handSize;
@ -447,7 +371,7 @@ void PlayerActions::actMulligan(int number)
}
doMulligan(number);
emit startingHandSizeChosen(number);
SettingsCache::instance().userInterface().setStartingHandSize(number);
}
void PlayerActions::actMulliganSameSize()
@ -625,7 +549,7 @@ void PlayerActions::moveTopCardsUntil(const QString &expr, MoveTopCardsUntilOpti
}
}
void PlayerActions::moveOneCardUntil(CardState *card)
void PlayerActions::moveOneCardUntil(CardItem *card)
{
moveTopCardTimer->stop();
@ -817,6 +741,61 @@ void PlayerActions::actMoveBottomCardToTop()
sendGameCommand(cmd);
}
/**
* Selects all cards in the given zone.
*
* @param zone The zone to select from
* @param filter A predicate to filter which cards are selected. Defaults to always returning true.
*/
static void selectCardsInZone(
const CardZoneLogic *zone,
std::function<bool(const CardItem *)> filter = [](const CardItem *) { return true; })
{
if (!zone) {
return;
}
for (auto &cardItem : zone->getCards()) {
if (cardItem && filter(cardItem)) {
cardItem->setSelected(true);
}
}
}
void PlayerActions::actSelectAll()
{
const CardItem *card = player->getGame()->getActiveCard();
if (!card) {
return;
}
selectCardsInZone(card->getZone());
}
void PlayerActions::actSelectRow()
{
const CardItem *card = player->getGame()->getActiveCard();
if (!card) {
return;
}
auto isSameRow = [card](const CardItem *cardItem) {
return qAbs(card->scenePos().y() - cardItem->scenePos().y()) < 50;
};
selectCardsInZone(card->getZone(), isSameRow);
}
void PlayerActions::actSelectColumn()
{
const CardItem *card = player->getGame()->getActiveCard();
if (!card) {
return;
}
auto isSameColumn = [card](const CardItem *cardItem) { return cardItem->x() == card->x(); };
selectCardsInZone(card->getZone(), isSameColumn);
}
void PlayerActions::actDrawBottomCard()
{
if (player->getDeckZone()->getCards().empty()) {
@ -950,7 +929,7 @@ void PlayerActions::actCreateToken(TokenInfo tokenToCreate)
if (correctedCard) {
lastTokenInfo.name = correctedCard.getName();
int tableRow = lastTokenInfo.faceDown ? 2 : correctedCard.getInfo().getUiAttributes().tableRow;
lastTokenTableRow = TableZoneLogic::tableRowToGridY(tableRow);
lastTokenTableRow = TableZone::tableRowToGridY(tableRow);
if (lastTokenInfo.pt.isEmpty()) {
lastTokenInfo.pt = correctedCard.getInfo().getPowTough();
}
@ -996,14 +975,15 @@ void PlayerActions::setLastTokenInfo(CardInfoPtr cardInfo)
return;
}
lastTokenInfo = {.name = cardInfo->getName(),
.color = cardInfo->getColors().isEmpty() ? QString() : cardInfo->getColors().left(1).toLower(),
.pt = cardInfo->getPowTough(),
.annotation = getAnnotateTokens() ? cardInfo->getText() : "",
.destroy = true,
.providerId = getCardPreferenceOverride(cardInfo->getName())};
lastTokenInfo = {
.name = cardInfo->getName(),
.color = cardInfo->getColors().isEmpty() ? QString() : cardInfo->getColors().left(1).toLower(),
.pt = cardInfo->getPowTough(),
.annotation = SettingsCache::instance().userInterface().getAnnotateTokens() ? cardInfo->getText() : "",
.destroy = true,
.providerId = SettingsCache::instance().cardOverrides().getCardPreferenceOverride(cardInfo->getName())};
lastTokenTableRow = TableZoneLogic::tableRowToGridY(cardInfo->getUiAttributes().tableRow);
lastTokenTableRow = TableZone::tableRowToGridY(cardInfo->getUiAttributes().tableRow);
emit requestEnableAndSetCreateAnotherTokenAction(lastTokenInfo.name);
}
@ -1021,20 +1001,25 @@ void PlayerActions::actCreatePredefinedToken()
actCreateAnotherToken();
}
void PlayerActions::actCreateRelatedCard(const CardState *sourceCard, int index)
void PlayerActions::actCreateRelatedCard()
{
const CardItem *sourceCard = player->getGame()->getActiveCard();
if (!sourceCard) {
return;
}
auto *action = static_cast<QAction *>(sender());
// If there is a better way of passing a CardRelation through a QAction, please add it here.
auto relatedCards = sourceCard->getCardInfo().getAllRelatedCards();
CardRelation *cardRelation = relatedCards.at(index);
CardRelation *cardRelation = relatedCards.at(action->data().toInt());
actRequestCreateRelatedFromRelationDialog(sourceCard, cardRelation);
}
void PlayerActions::actCreateAllRelatedCards(const CardState *sourceCard)
void PlayerActions::actCreateAllRelatedCards()
{
const CardItem *sourceCard = player->getGame()->getActiveCard();
if (!sourceCard) {
return;
}
@ -1120,13 +1105,13 @@ void PlayerActions::actCreateAllRelatedCards(const CardState *sourceCard)
}
}
void PlayerActions::actRequestCreateRelatedFromRelationDialog(const CardState *sourceCard,
void PlayerActions::actRequestCreateRelatedFromRelationDialog(const CardItem *sourceCard,
const CardRelation *cardRelation)
{
emit requestCreateRelatedFromRelationDialog(sourceCard, cardRelation);
}
bool PlayerActions::createRelatedFromRelation(const CardState *sourceCard,
bool PlayerActions::createRelatedFromRelation(const CardItem *sourceCard,
const CardRelation *cardRelation,
int variableCount)
{
@ -1178,7 +1163,7 @@ bool PlayerActions::createRelatedFromRelation(const CardState *sourceCard,
return true;
}
void PlayerActions::onRelatedCardCreated(const CardState *sourceCard, const CardRelation *cardRelation)
void PlayerActions::onRelatedCardCreated(const CardItem *sourceCard, const CardRelation *cardRelation)
{
if (sourceCard == nullptr || cardRelation == nullptr) {
return;
@ -1198,7 +1183,7 @@ void PlayerActions::onRelatedCardCreated(const CardState *sourceCard, const Card
setLastToken(relatedCard.getCardPtr());
}
void PlayerActions::createCard(const CardState *sourceCard,
void PlayerActions::createCard(const CardItem *sourceCard,
const QString &dbCardName,
CardRelationType attachType,
bool persistent,
@ -1210,7 +1195,7 @@ void PlayerActions::createCard(const CardState *sourceCard,
return;
}
QPoint gridPoint = QPoint(-1, TableZoneLogic::tableRowToGridY(cardInfo->getUiAttributes().tableRow));
QPoint gridPoint = QPoint(-1, TableZone::tableRowToGridY(cardInfo->getUiAttributes().tableRow));
// create the token for the related card
Command_CreateToken cmd;
@ -1229,7 +1214,7 @@ void PlayerActions::createCard(const CardState *sourceCard,
}
cmd.set_pt(cardInfo->getPowTough().toStdString());
if (getAnnotateTokens()) {
if (SettingsCache::instance().userInterface().getAnnotateTokens()) {
cmd.set_annotation(cardInfo->getText().toStdString());
} else {
cmd.set_annotation("");
@ -1284,7 +1269,7 @@ void PlayerActions::actRequestMoveCardXCardsFromTopDialog()
emit requestMoveCardXCardsFromTopDialog(defaultNumberTopCardsToPlaceBelow, deckSize);
}
void PlayerActions::actMoveCardXCardsFromTop(QList<CardState *> selectedCards, int number)
void PlayerActions::actMoveCardXCardsFromTop(QList<CardItem *> selectedCards, int number)
{
defaultNumberTopCardsToPlaceBelow = number;
@ -1318,14 +1303,14 @@ void PlayerActions::actMoveCardXCardsFromTop(QList<CardState *> selectedCards, i
}
}
void PlayerActions::actIncPT(QList<CardState *> selectedCards, int deltaP, int deltaT)
void PlayerActions::actIncPT(QList<CardItem *> selectedCards, int deltaP, int deltaT)
{
int playerid = player->getPlayerInfo()->getId();
QList<const ::google::protobuf::Message *> commandList;
for (auto card : selectedCards) {
QString pt = card->getPT();
const auto ptList = CardState::parsePT(pt);
const auto ptList = CardItem::parsePT(pt);
QString newpt;
if (ptList.isEmpty()) {
newpt = QString::number(deltaP) + (deltaT ? "/" + QString::number(deltaT) : "");
@ -1351,7 +1336,7 @@ void PlayerActions::actIncPT(QList<CardState *> selectedCards, int deltaP, int d
player->getGame()->getGameEventHandler()->sendGameCommand(prepareGameCommand(commandList), playerid);
}
void PlayerActions::actResetPT(QList<CardState *> selectedCards)
void PlayerActions::actResetPT(QList<CardItem *> selectedCards)
{
int playerid = player->getPlayerInfo()->getId();
QList<const ::google::protobuf::Message *> commandList;
@ -1384,7 +1369,7 @@ void PlayerActions::actResetPT(QList<CardState *> selectedCards)
}
}
void PlayerActions::actRequestSetPTDialog(QList<CardState *> selectedCards)
void PlayerActions::actRequestSetPTDialog(QList<CardItem *> selectedCards)
{
QString oldPT;
@ -1397,11 +1382,11 @@ void PlayerActions::actRequestSetPTDialog(QList<CardState *> selectedCards)
emit requestSetPTDialog(oldPT);
}
void PlayerActions::actSetPT(QList<CardState *> selectedCards, const QString &pt)
void PlayerActions::actSetPT(QList<CardItem *> selectedCards, const QString &pt)
{
int playerid = player->getPlayerInfo()->getId();
const auto ptList = CardState::parsePT(pt);
const auto ptList = CardItem::parsePT(pt);
bool empty = ptList.isEmpty();
QList<const ::google::protobuf::Message *> commandList;
@ -1409,7 +1394,7 @@ void PlayerActions::actSetPT(QList<CardState *> selectedCards, const QString &pt
auto *cmd = new Command_SetCardAttr;
QString newpt = QString();
if (!empty) {
const auto oldpt = CardState::parsePT(card->getPT());
const auto oldpt = CardItem::parsePT(card->getPT());
int ptIter = 0;
for (const auto &_item : ptList) {
if (_item.typeId() == QMetaType::Type::Int) {
@ -1437,65 +1422,55 @@ void PlayerActions::actSetPT(QList<CardState *> selectedCards, const QString &pt
player->getGame()->getGameEventHandler()->sendGameCommand(prepareGameCommand(commandList), playerid);
}
void PlayerActions::actIncP(QList<CardState *> selectedCards)
void PlayerActions::actDrawArrow()
{
auto *card = player->getGame()->getActiveCard();
if (card) {
card->drawArrow(Qt::red);
}
}
void PlayerActions::actIncP(QList<CardItem *> selectedCards)
{
actIncPT(selectedCards, 1, 0);
}
void PlayerActions::actDecP(QList<CardState *> selectedCards)
void PlayerActions::actDecP(QList<CardItem *> selectedCards)
{
actIncPT(selectedCards, -1, 0);
}
void PlayerActions::actIncT(QList<CardState *> selectedCards)
void PlayerActions::actIncT(QList<CardItem *> selectedCards)
{
actIncPT(selectedCards, 0, 1);
}
void PlayerActions::actDecT(QList<CardState *> selectedCards)
void PlayerActions::actDecT(QList<CardItem *> selectedCards)
{
actIncPT(selectedCards, 0, -1);
}
void PlayerActions::actIncPT(QList<CardState *> selectedCards)
void PlayerActions::actIncPT(QList<CardItem *> selectedCards)
{
actIncPT(selectedCards, 1, 1);
}
void PlayerActions::actDecPT(QList<CardState *> selectedCards)
void PlayerActions::actDecPT(QList<CardItem *> selectedCards)
{
actIncPT(selectedCards, -1, -1);
}
void PlayerActions::actFlowP(QList<CardState *> selectedCards)
void PlayerActions::actFlowP(QList<CardItem *> selectedCards)
{
actIncPT(selectedCards, 1, -1);
}
void PlayerActions::actFlowT(QList<CardState *> selectedCards)
void PlayerActions::actFlowT(QList<CardItem *> selectedCards)
{
actIncPT(selectedCards, -1, 1);
}
void PlayerActions::actIncrementCounter(int counterId, int delta)
{
Command_IncCounter cmd;
cmd.set_counter_id(counterId);
cmd.set_delta(delta);
sendGameCommand(cmd);
}
void PlayerActions::actSetCounter(int counterId, int value)
{
Command_SetCounter cmd;
cmd.set_counter_id(counterId);
cmd.set_value(value);
sendGameCommand(cmd);
}
void PlayerActions::actReduceLifeByPower(QList<CardState *> selectedCards)
void PlayerActions::actReduceLifeByPower(QList<CardItem *> selectedCards)
{
// find life counter
auto lifeCounter = player->getLifeCounter();
@ -1506,7 +1481,7 @@ void PlayerActions::actReduceLifeByPower(QList<CardState *> selectedCards)
// calculate total power;
int total = 0;
for (auto card : selectedCards) {
QVariantList parsed = CardState::parsePT(card->getPT());
QVariantList parsed = CardItem::parsePT(card->getPT());
if (!parsed.isEmpty()) {
int power = parsed.first().toInt(); // toInt will default to 0 if it's not an int
total += qMax(power, 0);
@ -1520,7 +1495,17 @@ void PlayerActions::actReduceLifeByPower(QList<CardState *> selectedCards)
sendGameCommand(prepareGameCommand(cmd));
}
void PlayerActions::actRequestSetAnnotationDialog(QList<CardState *> selectedCards)
void AnnotationDialog::keyPressEvent(QKeyEvent *event)
{
if (event->key() == Qt::Key_Return && event->modifiers() & Qt::ControlModifier) {
event->accept();
accept();
return;
}
QInputDialog::keyPressEvent(event);
}
void PlayerActions::actRequestSetAnnotationDialog(QList<CardItem *> selectedCards)
{
QString oldAnnotation;
for (auto card : selectedCards) {
@ -1532,7 +1517,7 @@ void PlayerActions::actRequestSetAnnotationDialog(QList<CardState *> selectedCar
emit requestSetAnnotationDialog(oldAnnotation);
}
void PlayerActions::actSetAnnotation(QList<CardState *> selectedCards, const QString &annotation)
void PlayerActions::actSetAnnotation(QList<CardItem *> selectedCards, const QString &annotation)
{
QList<const ::google::protobuf::Message *> commandList;
for (auto card : selectedCards) {
@ -1546,7 +1531,17 @@ void PlayerActions::actSetAnnotation(QList<CardState *> selectedCards, const QSt
sendGameCommand(prepareGameCommand(commandList));
}
void PlayerActions::actUnattach(QList<CardState *> selectedCards)
void PlayerActions::actAttach()
{
auto *card = player->getGame()->getActiveCard();
if (!card) {
return;
}
card->drawAttachArrow();
}
void PlayerActions::actUnattach(QList<CardItem *> selectedCards)
{
QList<const ::google::protobuf::Message *> commandList;
for (auto card : selectedCards) {
@ -1562,17 +1557,17 @@ void PlayerActions::actUnattach(QList<CardState *> selectedCards)
sendGameCommand(prepareGameCommand(commandList));
}
void PlayerActions::actAddCardCounter(QList<CardState *> selectedCards, int counterId)
void PlayerActions::actAddCardCounter(QList<CardItem *> selectedCards, int counterId)
{
offsetCardCounter(selectedCards, counterId, 1);
}
void PlayerActions::actRemoveCardCounter(QList<CardState *> selectedCards, int counterId)
void PlayerActions::actRemoveCardCounter(QList<CardItem *> selectedCards, int counterId)
{
offsetCardCounter(selectedCards, counterId, -1);
}
void PlayerActions::offsetCardCounter(QList<CardState *> selectedCards, int counterId, int offset)
void PlayerActions::offsetCardCounter(QList<CardItem *> selectedCards, int counterId, int offset)
{
QList<const ::google::protobuf::Message *> commandList;
for (auto card : selectedCards) {
@ -1598,7 +1593,7 @@ void PlayerActions::offsetCardCounter(QList<CardState *> selectedCards, int coun
sendGameCommand(prepareGameCommand(commandList));
}
void PlayerActions::actRequestSetCardCounterDialog(QList<CardState *> selectedCards, int counterId)
void PlayerActions::actRequestSetCardCounterDialog(QList<CardItem *> selectedCards, int counterId)
{
// If a single card is selected, we show the old value in the dialog. Otherwise, we show "x"
QString oldValueForDlg = "x";
@ -1610,7 +1605,7 @@ void PlayerActions::actRequestSetCardCounterDialog(QList<CardState *> selectedCa
emit requestSetCardCounterDialog(counterId, oldValueForDlg);
}
void PlayerActions::actSetCardCounter(QList<CardState *> selectedCards, int counterId, const QString &counterValue)
void PlayerActions::actSetCardCounter(QList<CardItem *> selectedCards, int counterId, const QString &counterValue)
{
QList<const ::google::protobuf::Message *> commandList;
for (auto card : selectedCards) {
@ -1631,13 +1626,11 @@ void PlayerActions::actSetCardCounter(QList<CardState *> selectedCards, int coun
sendGameCommand(prepareGameCommand(commandList));
}
void PlayerActions::actIncrementAllCardCounters(QList<CardState *> cardsToUpdate)
void PlayerActions::actIncrementAllCardCounters(QList<CardItem *> cardsToUpdate)
{
if (cardsToUpdate.isEmpty()) {
// If no cards selected, update all cards on table
for (auto *state : player->getTableZone()->getCards()) {
cardsToUpdate.append(state);
}
cardsToUpdate = static_cast<QList<CardItem *>>(player->getTableZone()->getCards());
}
QList<const ::google::protobuf::Message *> commandList;
@ -1680,7 +1673,7 @@ static bool isUnwritableRevealZone(CardZoneLogic *zone)
return false;
}
void PlayerActions::playSelectedCards(QList<CardState *> selectedCards, const bool faceDown)
void PlayerActions::playSelectedCards(QList<CardItem *> selectedCards, const bool faceDown)
{
// CardIds will get shuffled downwards when cards leave the deck.
// We need to iterate through the cards in reverse order so cardIds don't get changed out from under us as we play
@ -1695,26 +1688,27 @@ void PlayerActions::playSelectedCards(QList<CardState *> selectedCards, const bo
}
}
void PlayerActions::actPlay(QList<CardState *> selectedCards)
void PlayerActions::actPlay(QList<CardItem *> selectedCards)
{
playSelectedCards(selectedCards, false);
}
void PlayerActions::actPlayFacedown(QList<CardState *> selectedCards)
void PlayerActions::actPlayFacedown(QList<CardItem *> selectedCards)
{
playSelectedCards(selectedCards, true);
}
void PlayerActions::actHide(QList<CardState *> selectedCards)
void PlayerActions::actHide(QList<CardItem *> selectedCards)
{
for (const auto &card : selectedCards) {
if (isUnwritableRevealZone(card->getZone())) {
for (const auto &item : selectedCards) {
auto *card = static_cast<CardItem *>(item);
if (card && isUnwritableRevealZone(card->getZone())) {
card->getZone()->removeCard(card);
}
}
}
void PlayerActions::actReveal(QList<CardState *> selectedCards, QAction *action)
void PlayerActions::actReveal(QList<CardItem *> selectedCards, QAction *action)
{
const int otherPlayerId = action->data().toInt();
@ -1805,9 +1799,9 @@ void PlayerActions::actRevealRandomGraveyardCard(int revealToPlayerId)
sendGameCommand(cmd);
}
void PlayerActions::cardMenuAction(QList<CardState *> selectedCards, CardMenuActionType type)
void PlayerActions::cardMenuAction(QList<CardItem *> selectedCards, CardMenuActionType type)
{
QList<CardState *> cardList = selectedCards;
QList<CardItem *> cardList = selectedCards;
QList<const ::google::protobuf::Message *> commandList;
if (type <= cmClone) {
@ -1997,7 +1991,7 @@ void PlayerActions::cardMenuAction(QList<CardState *> selectedCards, CardMenuAct
ctm->set_tapped(info.getUiAttributes().cipt);
}
cmd->set_y(TableZoneLogic::tableRowToGridY(tableRow));
cmd->set_y(TableZone::tableRowToGridY(tableRow));
commandList.append(cmd);
}
break;
@ -2046,76 +2040,6 @@ PendingCommand *PlayerActions::prepareGameCommand(const QList<const ::google::pr
}
}
void PlayerActions::moveCards(CardZoneLogic *startZone,
CardZoneLogic *targetZone,
int x,
int y,
const QList<CardMoveRequest> &cards)
{
if (!startZone || !targetZone) {
return;
}
Command_MoveCard cmd;
cmd.set_start_player_id(startZone->getPlayer()->getPlayerInfo()->getId());
cmd.set_start_zone(startZone->getName().toStdString());
cmd.set_target_player_id(targetZone->getPlayer()->getPlayerInfo()->getId());
cmd.set_target_zone(targetZone->getName().toStdString());
cmd.set_x(x);
cmd.set_y(y);
if (auto *viewLogic = qobject_cast<ZoneViewZoneLogic *>(targetZone)) {
cmd.set_is_reversed(viewLogic->getIsReversed());
}
for (const CardMoveRequest &card : cards) {
auto *cardToMove = cmd.mutable_cards_to_move()->add_card();
cardToMove->set_card_id(card.cardId);
if (card.faceDown) {
cardToMove->set_face_down(true);
}
if (!card.pt.isEmpty()) {
cardToMove->set_pt(card.pt.toStdString());
}
}
sendGameCommand(cmd);
}
PendingCommand *PlayerActions::prepareZoneDump(CardZoneLogic *zone, int numberCards)
{
if (!zone) {
return nullptr;
}
Command_DumpZone cmd;
cmd.set_player_id(zone->getPlayer()->getPlayerInfo()->getId());
cmd.set_zone_name(zone->getName().toStdString());
cmd.set_number_cards(numberCards);
if (auto *viewLogic = qobject_cast<ZoneViewZoneLogic *>(zone)) {
cmd.set_is_reversed(viewLogic->getIsReversed());
}
return prepareGameCommand(cmd);
}
void PlayerActions::actToggleTapped(const QString &zoneName, const QList<CardState *> &cards, bool tapped)
{
QList<const ::google::protobuf::Message *> cmdList;
for (CardState *card : cards) {
if (card->getTapped() != tapped) {
auto *cmd = new Command_SetCardAttr;
cmd->set_zone(zoneName.toStdString());
cmd->set_card_id(card->getId());
cmd->set_attribute(AttrTapped);
cmd->set_attr_value(tapped ? "1" : "0");
cmdList.append(cmd);
}
}
sendGameCommand(prepareGameCommand(cmdList));
}
void PlayerActions::sendGameCommand(const google::protobuf::Message &command)
{
if (player->getPlayerInfo()->getJudge() && !player->getPlayerInfo()->getLocal()) {

View file

@ -8,13 +8,12 @@
#ifndef COCKATRICE_PLAYER_ACTIONS_H
#define COCKATRICE_PLAYER_ACTIONS_H
#include "../../game_graphics/board/card_item.h" // IWYU pragma: keep - moc instantiates the metatype for QList<CardItem *>, which Qt 6.4 needs complete
#include "../../game_graphics/dialogs/dlg_create_token.h"
#include "../../game_graphics/dialogs/dlg_move_top_cards_until.h"
#include "../../game_graphics/player/card_menu_action_type.h"
#include "../board/card_list.h"
#include "../board/card_state.h"
#include "card_menu_action_type.h"
#include "card_move_request.h"
#include "libcockatrice/card/card_info.h"
#include "move_top_cards_until_options.h"
#include "token_info.h"
#include <QObject>
#include <QString>
@ -23,6 +22,7 @@
#include <libcockatrice/filters/filter_string.h>
#include <qtmetamacros.h>
class CardItem;
class CardRelation;
class QAction;
class QTimer;
@ -36,16 +36,10 @@ class Message;
}
} // namespace google
class CardState;
class CardZoneLogic;
class Command_MoveCard;
class GameEventContext;
class ICardPreferenceProvider;
class IInterfaceSettingsProvider;
class PendingCommand;
class PlayerLogic;
class QAction;
struct ArrowData;
class PlayerActions : public QObject
{
Q_OBJECT
@ -56,9 +50,7 @@ public:
RANDOM_CARD_FROM_ZONE = -2
};
explicit PlayerActions(PlayerLogic *player,
IInterfaceSettingsProvider *interfaceSettings = nullptr,
ICardPreferenceProvider *cardPreferences = nullptr);
explicit PlayerActions(PlayerLogic *player);
void sendGameCommand(PendingCommand *pend);
void sendGameCommand(const google::protobuf::Message &command);
@ -66,52 +58,7 @@ public:
PendingCommand *prepareGameCommand(const ::google::protobuf::Message &cmd);
PendingCommand *prepareGameCommand(const QList<const ::google::protobuf::Message *> &cmdList);
/**
* @brief Moves the requested cards between zones on behalf of a view.
* @param startZone Zone the cards are dragged out of.
* @param targetZone Zone the cards are dropped onto.
* @param x Horizontal target index within the zone.
* @param y Vertical target index within the zone.
* @param cards The cards to move, as chosen by the view.
*/
void
moveCards(CardZoneLogic *startZone, CardZoneLogic *targetZone, int x, int y, const QList<CardMoveRequest> &cards);
/**
* @brief Prepares a dump of the given zone, for the caller to connect to and send.
* @param zone Zone to dump.
* @param numberCards How many cards to dump, -1 for all.
*/
PendingCommand *prepareZoneDump(CardZoneLogic *zone, int numberCards);
/**
* @brief Sets the tapped state of the cards that do not already have it.
* @param zoneName Zone the cards belong to.
* @param cards Cards the view wants toggled.
* @param tapped Desired tapped state.
*/
void actToggleTapped(const QString &zoneName, const QList<CardState *> &cards, bool tapped);
/**
* @brief Attaches one card to another on the table on behalf of a view.
* @param startCard Card being attached.
* @param targetCard Card being attached to.
*/
void attachCards(CardState *startCard, const CardState *targetCard);
/**
* @brief Draws an arrow from a card to another card or a player on behalf of a view.
*
* The view resolves the endpoints before playing the card out of hand, so the
* arrow still records the zone the card started in.
*
* @param startCard Card the arrow starts from.
* @param arrow Arrow endpoints and color, as resolved by the view.
* @param deleteInPhase Phase the arrow expires in, 0 to keep it until removed.
*/
void createArrow(CardState *startCard, const ArrowData &arrow, int deleteInPhase);
void moveOneCardUntil(CardState *card);
void moveOneCardUntil(CardItem *card);
void stopMoveTopCardsUntil();
[[nodiscard]] bool isMovingCardsUntil() const
@ -140,7 +87,7 @@ signals:
void requestDrawBottomCardsDialog(int defaultNumberBottomCards, int maxCards);
void requestRollDieDialog();
void requestCreateTokenDialog(const QStringList &predefinedTokens);
void requestCreateRelatedFromRelationDialog(const CardState *sourceCard, const CardRelation *cardRelation);
void requestCreateRelatedFromRelationDialog(const CardItem *sourceCard, const CardRelation *cardRelation);
void requestMoveCardXCardsFromTopDialog(int defaultNumberTopCardsToPlaceBelow, int deckSize);
void requestSetPTDialog(const QString &oldPT);
void requestSetAnnotationDialog(const QString &oldAnnotation);
@ -149,18 +96,12 @@ signals:
void requestSortHand(const QList<CardList::SortOption> &options);
void requestEnableAndSetCreateAnotherTokenAction(const QString &lastTokenName);
void requestSetLastToken(CardInfoPtr lastToken);
/**
* @brief The player chose a mulligan size to remember as the starting hand size.
*
* Game logic must not write client settings itself, so persistence is left to the application.
*/
void startingHandSizeChosen(int number);
public slots:
void setLastToken(CardInfoPtr cardInfo);
void setLastTokenInfo(CardInfoPtr cardInfo);
void playCard(CardState *c, bool faceDown);
void playCardToTable(const CardState *c, bool faceDown);
void playCard(CardItem *c, bool faceDown);
void playCardToTable(const CardItem *c, bool faceDown);
void actUntapAll();
void actRequestRollDieDialog();
@ -169,9 +110,9 @@ public slots:
void actRequestCreateTokenDialog(const QStringList &predefinedTokens);
void actCreateToken(TokenInfo tokenToCreate);
void actCreateAnotherToken();
void actRequestCreateRelatedFromRelationDialog(const CardState *sourceCard, const CardRelation *cardRelation);
bool createRelatedFromRelation(const CardState *sourceCard, const CardRelation *cardRelation, int variableCount);
void onRelatedCardCreated(const CardState *sourceCard, const CardRelation *cardRelation);
void actRequestCreateRelatedFromRelationDialog(const CardItem *sourceCard, const CardRelation *cardRelation);
bool createRelatedFromRelation(const CardItem *sourceCard, const CardRelation *cardRelation, int variableCount);
void onRelatedCardCreated(const CardItem *sourceCard, const CardRelation *cardRelation);
void setLastRelatedCreationSucceeded(bool succeeded)
{
lastRelatedCreationSucceeded = succeeded;
@ -191,9 +132,9 @@ public slots:
void actMulliganMinusOne();
void doMulligan(int number);
void actPlay(QList<CardState *> selectedCards);
void actPlayFacedown(QList<CardState *> selectedCards);
void actHide(QList<CardState *> selectedCards);
void actPlay(QList<CardItem *> selectedCards);
void actPlayFacedown(QList<CardItem *> selectedCards);
void actHide(QList<CardItem *> selectedCards);
void actMoveTopCardToPlay();
void actMoveTopCardToPlayFaceDown();
@ -223,6 +164,10 @@ public slots:
void actRequestMoveBottomCardsToDialog(const QString &targetZone, const QString &zoneDisplayName, bool faceDown);
void moveBottomCardsTo(int number, const QString &targetZone, bool faceDown);
void actSelectAll();
void actSelectRow();
void actSelectColumn();
void actViewLibrary();
void actViewHand();
void actRequestViewTopCardsDialog();
@ -242,55 +187,47 @@ public slots:
void actOpenDeckInDeckEditor();
void actCreatePredefinedToken();
void actCreateRelatedCard(const CardState *sourceCard, int index);
void actCreateAllRelatedCards(const CardState *sourceCard);
void actCreateRelatedCard();
void actCreateAllRelatedCards();
void actRequestMoveCardXCardsFromTopDialog();
void actMoveCardXCardsFromTop(QList<CardState *> selectedCards, int number);
void actRemoveCardCounter(QList<CardState *> selectedCards, int counterId);
void actAddCardCounter(QList<CardState *> selectedCards, int counterId);
void actRequestSetCardCounterDialog(QList<CardState *> selectedCards, int counterId);
void actSetCardCounter(QList<CardState *> selectedCards, int counterId, const QString &counterValue);
void actIncrementAllCardCounters(QList<CardState *> cardsToUpdate);
void actUnattach(QList<CardState *> selectedCards);
void actIncPT(QList<CardState *> selectedCards, int deltaP, int deltaT);
void actResetPT(QList<CardState *> selectedCards);
void actRequestSetPTDialog(QList<CardState *> selectedCards);
void actSetPT(QList<CardState *> selectedCards, const QString &pt);
void actIncP(QList<CardState *> selectedCards);
void actDecP(QList<CardState *> selectedCards);
void actIncT(QList<CardState *> selectedCards);
void actDecT(QList<CardState *> selectedCards);
void actIncPT(QList<CardState *> selectedCards);
void actDecPT(QList<CardState *> selectedCards);
void actFlowP(QList<CardState *> selectedCards);
void actFlowT(QList<CardState *> selectedCards);
void actMoveCardXCardsFromTop(QList<CardItem *> selectedCards, int number);
void actRemoveCardCounter(QList<CardItem *> selectedCards, int counterId);
void actAddCardCounter(QList<CardItem *> selectedCards, int counterId);
void actRequestSetCardCounterDialog(QList<CardItem *> selectedCards, int counterId);
void actSetCardCounter(QList<CardItem *> selectedCards, int counterId, const QString &counterValue);
void actIncrementAllCardCounters(QList<CardItem *> cardsToUpdate);
void actAttach();
void actUnattach(QList<CardItem *> selectedCards);
void actDrawArrow();
void actIncPT(QList<CardItem *> selectedCards, int deltaP, int deltaT);
void actResetPT(QList<CardItem *> selectedCards);
void actRequestSetPTDialog(QList<CardItem *> selectedCards);
void actSetPT(QList<CardItem *> selectedCards, const QString &pt);
void actIncP(QList<CardItem *> selectedCards);
void actDecP(QList<CardItem *> selectedCards);
void actIncT(QList<CardItem *> selectedCards);
void actDecT(QList<CardItem *> selectedCards);
void actIncPT(QList<CardItem *> selectedCards);
void actDecPT(QList<CardItem *> selectedCards);
void actFlowP(QList<CardItem *> selectedCards);
void actFlowT(QList<CardItem *> selectedCards);
/** @brief Adjusts a player counter by a signed delta. */
void actIncrementCounter(int counterId, int delta);
void actReduceLifeByPower(QList<CardItem *> selectedCards);
/** @brief Sets a player counter to an absolute value. */
void actSetCounter(int counterId, int value);
void actReduceLifeByPower(QList<CardState *> selectedCards);
void actRequestSetAnnotationDialog(QList<CardState *> selectedCards);
void actSetAnnotation(QList<CardState *> selectedCards, const QString &annotation);
void actReveal(QList<CardState *> selectedCards, QAction *action);
void actRequestSetAnnotationDialog(QList<CardItem *> selectedCards);
void actSetAnnotation(QList<CardItem *> selectedCards, const QString &annotation);
void actReveal(QList<CardItem *> selectedCards, QAction *action);
void actRevealHand(int revealToPlayerId);
void actRevealRandomHandCard(int revealToPlayerId);
void actRevealLibrary(int revealToPlayerId);
void actSortHand();
void cardMenuAction(QList<CardState *> selectedCards, CardMenuActionType type);
void cardMenuAction(QList<CardItem *> selectedCards, CardMenuActionType type);
private:
PlayerLogic *player;
// Borrowed settings providers (owned by the application). Null outside the client, in which case the compiled-in
// defaults below apply.
IInterfaceSettingsProvider *interfaceSettings;
ICardPreferenceProvider *cardPreferences;
int defaultNumberTopCards = 1;
int defaultNumberTopCardsToPlaceBelow = 1;
@ -308,23 +245,18 @@ private:
bool lastRelatedCreationSucceeded = false;
[[nodiscard]] bool getPlayToStack() const;
[[nodiscard]] int getStartingHandSize() const;
[[nodiscard]] bool getAnnotateTokens() const;
[[nodiscard]] QString getCardPreferenceOverride(const QString &cardName) const;
void createCard(const CardState *sourceCard,
void createCard(const CardItem *sourceCard,
const QString &dbCardName,
CardRelationType attach = CardRelationType::DoesNotAttach,
bool persistent = false,
bool faceDown = false);
void playSelectedCards(QList<CardState *> selectedCards, bool faceDown = false);
void playSelectedCards(QList<CardItem *> selectedCards, bool faceDown = false);
void cmdSetTopCard(Command_MoveCard &cmd);
void cmdSetBottomCard(Command_MoveCard &cmd);
void offsetCardCounter(QList<CardState *> selectedCards, int counterId, int offset);
void offsetCardCounter(QList<CardItem *> selectedCards, int counterId, int offset);
};
#endif // COCKATRICE_PLAYER_ACTIONS_H

View file

@ -1,14 +1,14 @@
#include "player_event_handler.h"
#include "../../game_graphics/board/card_item.h"
#include "../../game_graphics/zones/view_zone.h"
#include "../abstract_game.h"
#include "../board/arrow_data.h"
#include "../board/card_list.h"
#include "../board/card_state.h"
#include "../board/counter_state.h"
#include "../zones/card_zone_logic.h"
#include "../zones/hand_zone_logic.h"
#include "../zones/pile_zone_logic.h"
#include "../zones/view_zone_logic.h"
#include "libcockatrice/card/card_info.h"
#include "libcockatrice/card/printing/exact_card.h"
#include "libcockatrice/protocol/pb/card_attributes.pb.h"
@ -27,9 +27,6 @@
#include <QSharedPointer>
#include <QtGlobal>
#include <algorithm>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/card/database/card_database_querier.h>
#include <libcockatrice/protocol/pb/command_set_card_attr.pb.h>
#include <libcockatrice/protocol/pb/context_move_card.pb.h>
#include <libcockatrice/protocol/pb/context_undo_draw.pb.h>
#include <libcockatrice/protocol/pb/event_attach_card.pb.h>
@ -52,7 +49,6 @@
#include <libcockatrice/protocol/pb/event_set_card_counter.pb.h>
#include <libcockatrice/protocol/pb/event_set_counter.pb.h>
#include <libcockatrice/protocol/pb/event_shuffle.pb.h>
#include <libcockatrice/utility/color.h>
#include <libcockatrice/utility/zone_names.h>
#include <qlogging.h>
#include <string>
@ -80,12 +76,12 @@ void PlayerEventHandler::eventShuffle(const Event_Shuffle &event)
}
// close all views that contain shuffled cards
for (auto *viewLogic : zone->getViews()) {
if (viewLogic != nullptr) {
int length = viewLogic->getCards().length();
for (ZoneViewZone *view : zone->getViews()) {
if (view != nullptr) {
int length = view->getLogic()->getCards().length();
// we want to close empty views as well
if (length == 0 || length > absStart) { // note this assumes views always start at the top of the library
viewLogic->requestClose();
view->close();
}
} else {
qWarning() << zone->getName() << "of" << player->getPlayerInfo()->getName() << "holds empty zoneview!";
@ -159,29 +155,23 @@ void PlayerEventHandler::eventCreateToken(const Event_CreateToken &event)
}
CardRef cardRef = {QString::fromStdString(event.card_name()), QString::fromStdString(event.card_provider_id())};
CardItem *card = new CardItem(player, nullptr, cardRef, event.card_id());
// use db PT if not provided in event and not face-down
QString pt = QString::fromStdString(event.pt());
if (pt.isEmpty() && !event.face_down()) {
ExactCard dbCard = CardDatabaseManager::query()->getCard(cardRef);
if (!QString::fromStdString(event.pt()).isEmpty()) {
card->setPT(QString::fromStdString(event.pt()));
} else if (!event.face_down()) {
ExactCard dbCard = card->getCard();
if (dbCard) {
pt = dbCard.getInfo().getPowTough();
card->setPT(dbCard.getInfo().getPowTough());
}
}
card->setColor(QString::fromStdString(event.color()));
card->setAnnotation(QString::fromStdString(event.annotation()));
card->setDestroyOnZoneChange(event.destroy_on_zone_change());
card->setFaceDown(event.face_down());
emit logCreateToken(player, cardRef.name, pt, event.face_down());
ServerInfo_Card cardInfo;
cardInfo.set_id(event.card_id());
cardInfo.set_name(event.card_name());
cardInfo.set_provider_id(event.card_provider_id());
cardInfo.set_pt(pt.toStdString());
cardInfo.set_color(event.color());
cardInfo.set_annotation(event.annotation());
cardInfo.set_destroy_on_zone_change(event.destroy_on_zone_change());
cardInfo.set_face_down(event.face_down());
cardInfo.set_x(event.x());
cardInfo.set_y(event.y());
emit zone->createCardRequested(cardInfo, true);
emit logCreateToken(player, card->getName(), card->getPT(), card->getFaceDown());
zone->addCard(card, true, event.x(), event.y());
}
void PlayerEventHandler::eventSetCardAttr(const Event_SetCardAttr &event,
@ -203,7 +193,7 @@ void PlayerEventHandler::eventSetCardAttr(const Event_SetCardAttr &event,
emit logSetTapped(player, nullptr, event.attr_value() == "1");
}
} else {
CardState *card = zone->getCard(event.card_id());
CardItem *card = zone->getCard(event.card_id());
if (!card) {
qWarning() << "PlayerEventHandler::eventSetCardAttr: card id=" << event.card_id() << "not found";
return;
@ -213,7 +203,7 @@ void PlayerEventHandler::eventSetCardAttr(const Event_SetCardAttr &event,
}
void PlayerEventHandler::setCardAttrHelper(const GameEventContext &context,
CardState *card,
CardItem *card,
CardAttribute attribute,
const QString &avalue,
bool allCards,
@ -274,7 +264,7 @@ void PlayerEventHandler::eventSetCardCounter(const Event_SetCardCounter &event)
return;
}
CardState *card = zone->getCard(event.card_id());
CardItem *card = zone->getCard(event.card_id());
if (!card) {
return;
}
@ -351,13 +341,13 @@ void PlayerEventHandler::eventMoveCard(const Event_MoveCard &event, const GameEv
if (x == -1) {
x = 0;
}
CardState *card = startZone->takeCard(position, event.card_id(), startZone != targetZone);
CardItem *card = startZone->takeCard(position, event.card_id(), startZone != targetZone);
if (card == nullptr) {
return;
}
const bool zoneChanged = startZone != targetZone;
const bool ownerChanged = zoneChanged && startZone->getPlayer() != targetZone->getPlayer();
emit cardViewRefreshRequested(card, zoneChanged, ownerChanged ? targetZone->getPlayer() : nullptr);
if (startZone != targetZone) {
card->deleteCardInfoPopup();
}
if (event.has_card_name()) {
QString name = QString::fromStdString(event.card_name());
QString providerId =
@ -366,19 +356,27 @@ void PlayerEventHandler::eventMoveCard(const Event_MoveCard &event, const GameEv
}
if (card->getAttachedTo() && (startZone != targetZone)) {
CardState *parentCard = card->getAttachedTo();
// CardState::setAttachedTo removes the card from the old parent's attachedCards list and emits the zone events.
CardItem *parentCard = card->getAttachedTo();
card->setAttachedTo(nullptr);
parentCard->getZone()->reorganizeCards();
}
card->deleteDragItem();
card->setId(event.new_card_id());
card->setFaceDown(event.face_down());
if (startZone != targetZone) {
const QList<CardState *> &attachedCards = card->getAttachedCards();
card->setBeingPointedAt(false);
card->setHovered(false);
const QList<CardItem *> &attachedCards = card->getAttachedCards();
for (auto attachedCard : attachedCards) {
emit targetZone->cardAdded(attachedCard);
}
if (startZone->getPlayer() != targetZone->getPlayer()) {
card->setOwner(targetZone->getPlayer());
}
}
// The log event has to be sent before the card is added to the target zone
@ -406,7 +404,7 @@ void PlayerEventHandler::eventFlipCard(const Event_FlipCard &event)
if (!zone) {
return;
}
CardState *card = zone->getCard(event.card_id());
CardItem *card = zone->getCard(event.card_id());
if (!card) {
return;
}
@ -429,28 +427,20 @@ void PlayerEventHandler::eventDestroyCard(const Event_DestroyCard &event)
return;
}
CardState *card = zone->getCard(event.card_id());
CardItem *card = zone->getCard(event.card_id());
if (!card) {
return;
}
QList<CardState *> attachedCards = card->getAttachedCards();
QList<CardItem *> attachedCards = card->getAttachedCards();
// This list is always empty except for buggy server implementations.
for (auto &attachedCard : attachedCards) {
card->removeAttachedCard(attachedCard);
attachedCard->setGridPoint(QPoint(-1, attachedCard->getGridPoint().y()));
attachedCard->setAttachedTo(nullptr);
if (attachedCard->getZone() == nullptr) {
attachedCard->deleteView();
} else {
emit attachedCard->getZone()->cardAdded(attachedCard);
attachedCard->getZone()->reorganizeCards();
}
}
emit logDestroyCard(player, card->getName());
zone->takeCard(-1, event.card_id(), true);
card->deleteView();
card->deleteLater();
}
void PlayerEventHandler::eventAttachCard(const Event_AttachCard &event)
@ -458,7 +448,7 @@ void PlayerEventHandler::eventAttachCard(const Event_AttachCard &event)
const QMap<int, PlayerLogic *> &playerList = player->getGame()->getPlayerManager()->getPlayers();
PlayerLogic *targetPlayer = nullptr;
CardZoneLogic *targetZone = nullptr;
CardState *targetCard = nullptr;
CardItem *targetCard = nullptr;
if (event.has_target_player_id()) {
targetPlayer = playerList.value(event.target_player_id(), 0);
if (targetPlayer) {
@ -474,12 +464,12 @@ void PlayerEventHandler::eventAttachCard(const Event_AttachCard &event)
return;
}
CardState *startCard = startZone->getCard(event.card_id());
CardItem *startCard = startZone->getCard(event.card_id());
if (!startCard) {
return;
}
CardState *oldParent = startCard->getAttachedTo();
CardItem *oldParent = startCard->getAttachedTo();
startCard->setAttachedTo(targetCard);
@ -508,7 +498,7 @@ void PlayerEventHandler::eventDrawCards(const Event_DrawCards &event)
if (listSize) {
for (int i = 0; i < listSize; ++i) {
const ServerInfo_Card &cardInfo = event.cards(i);
CardState *card = _deck->takeCard(0, cardInfo.id());
CardItem *card = _deck->takeCard(0, cardInfo.id());
QString cardName = QString::fromStdString(cardInfo.name());
QString providerId = QString::fromStdString(cardInfo.provider_id());
card->setCardRef({cardName, providerId});
@ -556,7 +546,7 @@ void PlayerEventHandler::eventRevealCards(const Event_RevealCards &event, EventP
for (const auto &card : cardList) {
QString cardName = QString::fromStdString(card->name());
QString providerId = QString::fromStdString(card->provider_id());
CardState *cardItem = zone->getCard(card->id());
CardItem *cardItem = zone->getCard(card->id());
if (!cardItem) {
continue;
}

View file

@ -27,7 +27,7 @@
#include <qtmetamacros.h>
#include <sys/types.h>
class CardState;
class CardItem;
class CardZoneLogic;
class PlayerLogic;
class Event_AttachCard;
@ -61,7 +61,7 @@ template <typename T> class QList;
* Design notes:
* - All event handlers assume events are authoritative and already validated
* by the server.
* - Most handlers mutate both logical state (CardState, CardZoneLogic, counters)
* - Most handlers mutate both logical state (CardItem, CardZoneLogic, counters)
* and visual/UI state (views, arrows, menus).
* - Logging signals are emitted *after* or *during* state mutation, depending
* on whether later mutations would invalidate log data.
@ -238,7 +238,7 @@ signals:
void logUndoDraw(PlayerLogic *player, QString cardName);
void logUndoDrawFailed(PlayerLogic *player);
void logMoveCard(PlayerLogic *player,
CardState *card,
CardItem *card,
CardZoneLogic *startZone,
int oldX,
CardZoneLogic *targetZone,
@ -248,11 +248,11 @@ signals:
void logAttachCard(PlayerLogic *player, QString cardName, PlayerLogic *targetPlayer, QString targetCardName);
void logUnattachCard(PlayerLogic *player, QString cardName);
void logSetCardCounter(PlayerLogic *player, QString cardName, int counterId, int value, int oldValue);
void logSetTapped(PlayerLogic *player, CardState *card, bool tapped);
void logSetTapped(PlayerLogic *player, CardItem *card, bool tapped);
void logSetCounter(PlayerLogic *player, QString counterName, int value, int oldValue);
void logSetDoesntUntap(PlayerLogic *player, CardState *card, bool doesntUntap);
void logSetPT(PlayerLogic *player, CardState *card, QString newPT);
void logSetAnnotation(PlayerLogic *player, CardState *card, QString newAnnotation);
void logSetDoesntUntap(PlayerLogic *player, CardItem *card, bool doesntUntap);
void logSetPT(PlayerLogic *player, CardItem *card, QString newPT);
void logSetAnnotation(PlayerLogic *player, CardItem *card, QString newAnnotation);
void logDumpZone(PlayerLogic *player, CardZoneLogic *zone, int numberCards, bool isReversed = false);
void logRevealCards(PlayerLogic *player,
CardZoneLogic *zone,
@ -266,23 +266,15 @@ signals:
void logAlwaysLookAtTopCard(PlayerLogic *player, CardZoneLogic *zone, bool reveal);
/** @} */
void cardZoneChanged(CardState *card, bool sameZone);
void requestCardMenuUpdate(CardState *card);
/**
* @brief A card was just moved by the server and its view needs refreshing.
*
* @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 cardViewRefreshRequested(CardState *card, bool zoneChanged, PlayerLogic *newOwner);
void cardZoneChanged(CardItem *card, bool sameZone);
void requestCardMenuUpdate(const CardItem *card);
private:
/** Owning player instance. */
PlayerLogic *player;
void setCardAttrHelper(const GameEventContext &context,
CardState *card,
CardItem *card,
CardAttribute attribute,
const QString &avalue,
bool allCards,

View file

@ -1,9 +1,8 @@
#include "player_logic.h"
#include "../../game_graphics/board/card_item.h"
#include "../abstract_game.h"
#include "../board/arrow_data.h"
#include "../board/card_list.h"
#include "../board/card_state.h"
#include "../board/counter_state.h"
#include "../game_meta_info.h"
#include "../zones/hand_zone_logic.h"
@ -34,17 +33,10 @@
#include <qalgorithms.h>
#include <string>
PlayerLogic::PlayerLogic(const ServerInfo_User &info,
int _id,
bool _local,
bool _judge,
AbstractGame *_parent,
IInterfaceSettingsProvider *interfaceSettings,
ICardPreferenceProvider *cardPreferences)
PlayerLogic::PlayerLogic(const ServerInfo_User &info, int _id, bool _local, bool _judge, AbstractGame *_parent)
: QObject(_parent), game(_parent), playerInfo(new PlayerInfo(info, _id, _local, _judge)),
playerEventHandler(new PlayerEventHandler(this)),
playerActions(new PlayerActions(this, interfaceSettings, cardPreferences)), active(false), conceded(false),
zoneId(0), dialogSemaphore(false)
playerEventHandler(new PlayerEventHandler(this)), playerActions(new PlayerActions(this)), active(false),
conceded(false), zoneId(0), dialogSemaphore(false)
{
initializeZones();
}
@ -180,20 +172,22 @@ void PlayerLogic::processPlayerInfo(const ServerInfo_Player &info)
const int cardListSize = zoneInfo.card_list_size();
if (!cardListSize) {
for (int j = 0; j < zoneInfo.card_count(); ++j) {
emit zone->createCardRequested(ServerInfo_Card(), false);
zone->addCard(new CardItem(this), false, -1);
}
} else {
for (int j = 0; j < cardListSize; ++j) {
ServerInfo_Card cardInfo = zoneInfo.card_list(j);
const ServerInfo_Card &cardInfo = zoneInfo.card_list(j);
auto *card = new CardItem(this);
card->processCardInfo(cardInfo);
// Zones without coordinates (hand, piles, stack) preserve the order
// they arrive in on the server in the positions of their cards list.
// The x coordinate of such cards is always 0, so inserting at it
// would reverse the list on reconnect. Append instead.
if (!zoneInfo.with_coords()) {
cardInfo.set_x(-1);
cardInfo.set_y(-1);
if (zoneInfo.with_coords()) {
zone->addCard(card, false, cardInfo.x(), cardInfo.y());
} else {
zone->addCard(card, false, -1);
}
emit zone->createCardRequested(cardInfo, false);
}
}
if (zoneInfo.has_always_reveal_top_card()) {
@ -225,8 +219,8 @@ void PlayerLogic::processCardAttachment(const ServerInfo_Player &info)
for (int j = 0; j < cardListSize; ++j) {
const ServerInfo_Card &cardInfo = zoneInfo.card_list(j);
if (cardInfo.has_attach_player_id()) {
CardState *startCard = zone->getCard(cardInfo.id());
CardState *targetCard =
CardItem *startCard = zone->getCard(cardInfo.id());
CardItem *targetCard =
game->getCard(cardInfo.attach_player_id(), QString::fromStdString(cardInfo.attach_zone()),
cardInfo.attach_card_id());
if (!targetCard) {
@ -245,19 +239,19 @@ void PlayerLogic::processCardAttachment(const ServerInfo_Player &info)
}
}
void PlayerLogic::addCard(AbstractCardItem *card)
void PlayerLogic::addCard(CardItem *card)
{
emit newCardAdded(card);
}
void PlayerLogic::deleteCard(CardState *card)
void PlayerLogic::deleteCard(CardItem *card)
{
if (card == nullptr) {
return;
} else if (dialogSemaphore) {
cardsToDelete.append(card);
} else {
card->deleteView();
card->deleteLater();
}
}
@ -338,7 +332,7 @@ bool PlayerLogic::clearCardsToDelete()
for (auto &i : cardsToDelete) {
if (i != nullptr) {
i->deleteView();
i->deleteLater();
}
}
cardsToDelete.clear();

View file

@ -7,8 +7,6 @@
#ifndef PLAYER_H
#define PLAYER_H
#include "../board/arrow_data.h"
#include "../board/counter_state.h"
#include "../zones/card_zone_logic.h"
#include "../zones/hand_zone_logic.h"
#include "../zones/pile_zone_logic.h"
@ -19,16 +17,12 @@
#include "libcockatrice/utility/playmat_params.h"
#include <QColor>
#include <QInputDialog>
#include <QList>
#include <QLoggingCategory>
#include <QMap>
#include <QObject>
#include <QString>
#include <QTimer>
#include <libcockatrice/deck_list/loaded_deck.h>
#include <libcockatrice/filters/filter_string.h>
#include <libcockatrice/protocol/pb/card_attributes.pb.h>
#include <libcockatrice/protocol/pb/game_event.pb.h>
#include <libcockatrice/utility/zone_names.h>
#include <qtmetamacros.h>
@ -37,6 +31,7 @@
// signatures, which the moc-generated code needs to be complete.
#include <libcockatrice/protocol/pb/serverinfo_card.pb.h>
class CardItem;
class CounterState;
class QWidget;
class ServerInfo_PlayerProperties;
@ -47,8 +42,6 @@ template <class T> class QSharedPointer;
inline Q_LOGGING_CATEGORY(PlayerLog, "player");
class AbstractCardItem;
class AbstractGame;
class ICardPreferenceProvider;
class IInterfaceSettingsProvider;
class PlayerInfo;
class PlayerEventHandler;
class PlayerActions;
@ -74,7 +67,7 @@ signals:
/** @brief Emitted when the remote playmat (card/params) is updated from player properties. */
void playmatChanged();
void newCardAdded(AbstractCardItem *card);
void requestCardMenuUpdate(CardState *card);
void requestCardMenuUpdate(const CardItem *card);
void counterAdded(CounterState *state);
void counterRemoved(int counterId);
void rearrangeCounters();
@ -94,13 +87,7 @@ public slots:
void onRequestZoneViewToggle(const QString &zoneName, int numberCards, bool isReversed);
public:
PlayerLogic(const ServerInfo_User &info,
int _id,
bool _local,
bool _judge,
AbstractGame *_parent,
IInterfaceSettingsProvider *interfaceSettings = nullptr,
ICardPreferenceProvider *cardPreferences = nullptr);
PlayerLogic(const ServerInfo_User &info, int _id, bool _local, bool _judge, AbstractGame *_parent);
~PlayerLogic() override;
void initializeZones();
@ -110,8 +97,8 @@ public:
void processPlayerInfo(const ServerInfo_Player &info);
void processCardAttachment(const ServerInfo_Player &info);
void addCard(AbstractCardItem *c);
void deleteCard(CardState *c);
void addCard(CardItem *c);
void deleteCard(CardItem *c);
bool clearCardsToDelete();
@ -263,7 +250,7 @@ private:
QMap<int, CounterState *> counters;
bool dialogSemaphore;
QList<CardState *> cardsToDelete;
QList<CardItem *> cardsToDelete;
// Playmat from player properties (for opponent display)
CardRef remotePlaymatCard;
@ -271,4 +258,15 @@ private:
bool hasRemotePlaymat = false;
};
class AnnotationDialog : public QInputDialog
{
Q_OBJECT
void keyPressEvent(QKeyEvent *e) override;
public:
explicit AnnotationDialog(QWidget *parent = nullptr) : QInputDialog(parent)
{
}
};
#endif

View file

@ -12,12 +12,9 @@
PlayerManager::PlayerManager(AbstractGame *_game,
int _localPlayerId,
bool _localPlayerIsJudge,
bool localPlayerIsSpectator,
IInterfaceSettingsProvider *_interfaceSettings,
ICardPreferenceProvider *_cardPreferences)
bool localPlayerIsSpectator)
: QObject(_game), game(_game), players(QMap<int, PlayerLogic *>()), localPlayerId(_localPlayerId),
localPlayerIsJudge(_localPlayerIsJudge), localPlayerIsSpectator(localPlayerIsSpectator),
interfaceSettings(_interfaceSettings), cardPreferences(_cardPreferences)
localPlayerIsJudge(_localPlayerIsJudge), localPlayerIsSpectator(localPlayerIsSpectator)
{
}
@ -55,7 +52,7 @@ bool PlayerManager::isLocalPlayer(int playerId)
PlayerLogic *PlayerManager::addPlayer(int playerId, const ServerInfo_User &info)
{
auto *newPlayer = new PlayerLogic(info, playerId, isLocalPlayer(playerId) || game->getGameState()->getIsLocalGame(),
isJudge(), getGame(), interfaceSettings, cardPreferences);
isJudge(), getGame());
connect(newPlayer, &PlayerLogic::concededChanged, this, &PlayerManager::onPlayerConceded);
players.insert(playerId, newPlayer);
emit playerAdded(newPlayer);

View file

@ -16,8 +16,6 @@
#include <qtmetamacros.h>
class AbstractGame;
class ICardPreferenceProvider;
class IInterfaceSettingsProvider;
class PlayerLogic;
class PlayerManager : public QObject
@ -25,12 +23,7 @@ class PlayerManager : public QObject
Q_OBJECT
public:
PlayerManager(AbstractGame *_game,
int _localPlayerId,
bool _localPlayerIsJudge,
bool localPlayerIsSpectator,
IInterfaceSettingsProvider *interfaceSettings = nullptr,
ICardPreferenceProvider *cardPreferences = nullptr);
PlayerManager(AbstractGame *_game, int _localPlayerId, bool _localPlayerIsJudge, bool localPlayerIsSpectator);
AbstractGame *game;
QMap<int, PlayerLogic *> players;
@ -38,9 +31,6 @@ public:
bool localPlayerIsJudge;
bool localPlayerIsSpectator;
QMap<int, ServerInfo_User> spectators;
// Borrowed settings providers forwarded to each PlayerLogic (owned by the application).
IInterfaceSettingsProvider *interfaceSettings;
ICardPreferenceProvider *cardPreferences;
[[nodiscard]] bool isSpectator() const
{

View file

@ -9,15 +9,10 @@
class GameReplay;
class QObject;
Replay::Replay(QObject *_parent,
const GameReplay *_replay,
bool isLocalGame,
IInterfaceSettingsProvider *interfaceSettings,
ICardPreferenceProvider *cardPreferences)
: AbstractGame(_parent)
Replay::Replay(QObject *_parent, const GameReplay *_replay, bool isLocalGame) : AbstractGame(_parent)
{
gameState = new GameState(this, 0, -1, isLocalGame, {}, false, false, -1, false);
connect(gameMetaInfo, &GameMetaInfo::startedChanged, gameState, &GameState::onStartedChanged);
playerManager = new PlayerManager(this, -1, false, true, interfaceSettings, cardPreferences);
playerManager = new PlayerManager(this, -1, false, true);
loadReplay(_replay);
}

View file

@ -0,0 +1,26 @@
/**
* @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

View file

@ -5,9 +5,9 @@ namespace CardZoneAlgorithms
{
/**
* Shared insertion logic for zones where cards follow the standard pattern:
* clamp index, insert, clear identity if contents unknown, reset state.
* Visibility and parenting are handled by the graphics zone's onCardAdded().
* Shared insertion logic for zones where cards become visible on add and follow
* the standard pattern: clamp index, insert, clear identity if contents unknown,
* reset state, show card.
*
* Zones with different post-add behavior (signal connections, positional resets,
* hidden cards, or coordinate-based placement) should NOT use this — implement
@ -18,7 +18,8 @@ namespace CardZoneAlgorithms
*
* @tparam CardList Must provide: size() -> int, insert(int, CardType*),
* getContentsKnown() -> bool
* @tparam CardType Must provide: setId(int), setCardRef(CardRefType), resetState(bool)
* @tparam CardType Must provide: setId(int), setCardRef(CardRefType),
* resetState(bool), setVisible(bool)
* @param keepAnnotations Forwarded to card->resetState(). Stack-like zones preserve
* annotations across zone transitions; hand-like zones clear them.
*/
@ -36,6 +37,7 @@ void addCardToList(CardList &cards, CardType *card, int x, bool keepAnnotations)
}
card->resetState(keepAnnotations);
card->setVisible(true);
}
} // namespace CardZoneAlgorithms

View file

@ -1,11 +1,13 @@
#include "card_zone_logic.h"
#include "../../client/translation.h"
#include "../../game_graphics/board/card_item.h"
#include "../../game_graphics/zones/card_zone.h"
#include "../../game_graphics/zones/view_zone.h"
#include "../board/card_list.h"
#include "../board/card_state.h"
#include "../player/player_actions.h"
#include "../player/player_info.h"
#include "../player/player_logic.h"
#include "../translation.h"
#include "libcockatrice/card/database/card_database.h"
#include "view_zone_logic.h"
@ -42,24 +44,19 @@ CardZoneLogic::CardZoneLogic(PlayerLogic *_player,
&CardZoneLogic::refreshCardInfos);
}
void CardZoneLogic::addCard(CardState *card, const bool reorganize, const int x, const int y)
void CardZoneLogic::addCard(CardItem *card, const bool reorganize, const int x, const int y)
{
if (!card) {
qCWarning(CardZoneLogicLog) << "CardZoneLogic::addCard() card is null; this shouldn't normally happen";
qCWarning(CardZoneLog) << "CardZoneLogic::addCard() card is null; this shouldn't normally happen";
return;
}
for (auto *viewLogic : views) {
if (viewLogic->prepareAddCard(x)) {
ServerInfo_Card cardInfo;
const CardRef &cardRef = card->getCardRef();
cardInfo.set_name(cardRef.name.toStdString());
cardInfo.set_provider_id(cardRef.providerId.toStdString());
cardInfo.set_id(card->getId());
cardInfo.set_face_down(card->getFaceDown());
cardInfo.set_x(x);
cardInfo.set_y(y);
emit viewLogic->createCardRequested(cardInfo, reorganize);
for (auto *view : views) {
if (qobject_cast<ZoneViewZoneLogic *>(view->getLogic())->prepareAddCard(x)) {
auto copy = new CardItem(player, nullptr, card->getCardRef(), card->getId());
copy->setFaceDown(card->getFaceDown());
view->getLogic()->addCard(copy, reorganize, x, y);
}
}
@ -74,7 +71,7 @@ void CardZoneLogic::addCard(CardState *card, const bool reorganize, const int x,
emit cardCountChanged();
}
CardState *CardZoneLogic::takeCard(int position, int cardId, bool toNewZone)
CardItem *CardZoneLogic::takeCard(int position, int cardId, bool toNewZone)
{
if (position == -1) {
// position == -1 means either that the zone is indexed by card id
@ -93,11 +90,11 @@ CardState *CardZoneLogic::takeCard(int position, int cardId, bool toNewZone)
return nullptr;
}
for (auto *viewLogic : views) {
viewLogic->removeCard(position, toNewZone);
for (auto *view : views) {
qobject_cast<ZoneViewZoneLogic *>(view->getLogic())->removeCard(position, toNewZone);
}
CardState *c = cards.takeAt(position);
CardItem *c = cards.takeAt(position);
c->setId(cardId);
@ -106,11 +103,11 @@ CardState *CardZoneLogic::takeCard(int position, int cardId, bool toNewZone)
return c;
}
CardState *CardZoneLogic::getCard(int cardId)
CardItem *CardZoneLogic::getCard(int cardId)
{
CardState *c = cards.findCard(cardId);
CardItem *c = cards.findCard(cardId);
if (!c) {
qCWarning(CardZoneLogicLog) << "CardZoneLogic::getCard: card id=" << cardId << "not found";
qCWarning(CardZoneLog) << "CardZoneLogic::getCard: card id=" << cardId << "not found";
return nullptr;
}
// If the card's id is -1, this zone is invisible,
@ -122,10 +119,10 @@ CardState *CardZoneLogic::getCard(int cardId)
return c;
}
void CardZoneLogic::removeCard(CardState *card)
void CardZoneLogic::removeCard(CardItem *card)
{
if (!card) {
qCWarning(CardZoneLogicLog) << "CardZoneLogic::removeCard: card is null, this shouldn't normally happen";
qCWarning(CardZoneLog) << "CardZoneLogic::removeCard: card is null, this shouldn't normally happen";
return;
}
@ -138,8 +135,8 @@ void CardZoneLogic::removeCard(CardState *card)
void CardZoneLogic::refreshCardInfos()
{
for (const auto &cardState : cards) {
cardState->refreshCardInfo();
for (const auto &cardItem : cards) {
cardItem->refreshCardInfo();
}
}
@ -171,17 +168,17 @@ void CardZoneLogic::clearContents()
const CardList toClear = cards;
// Detach and notify attached cards and zones *before* deleting anything.
for (CardState *card : toClear) {
for (CardItem *card : toClear) {
// If an incorrectly implemented server doesn't return attached cards to whom they belong before dropping a
// player, we have to return them to avoid a crash.
const QList<CardState *> &attachedCards = card->getAttachedCards();
for (CardState *attachedCard : attachedCards) {
const QList<CardItem *> &attachedCards = card->getAttachedCards();
for (CardItem *attachedCard : attachedCards) {
emit attachedCard->getZone()->cardAdded(attachedCard);
}
}
// Now request deletions after all manipulations are done.
for (CardState *card : toClear) {
for (CardItem *card : toClear) {
player->deleteCard(card);
}

View file

@ -7,30 +7,28 @@
#ifndef COCKATRICE_CARD_ZONE_LOGIC_H
#define COCKATRICE_CARD_ZONE_LOGIC_H
#include "../../client/translation.h"
#include "../board/card_list.h"
#include "../translation.h"
#include <QList>
#include <QLoggingCategory>
#include <QObject>
#include <QString>
#include <libcockatrice/protocol/pb/serverinfo_card.pb.h>
#include <qtmetamacros.h>
class CardItem;
inline Q_LOGGING_CATEGORY(CardZoneLogicLog, "card_zone_logic");
class PlayerLogic;
class ZoneViewZoneLogic;
class CardDragItem;
class ZoneViewZone;
class CardZoneLogic : public QObject
{
Q_OBJECT
signals:
void cardAdded(CardState *addedCard);
/** @brief Asks the graphics side to build the card item for @p cardInfo and add it to this zone. */
void createCardRequested(const ServerInfo_Card &cardInfo, bool reorganize);
void cardAdded(CardItem *addedCard);
void cardCountChanged();
void reorganizeCards();
void updateGraphics();
@ -45,17 +43,16 @@ public:
bool _contentsKnown,
QObject *parent = nullptr);
void addCard(CardState *card, bool reorganize, int x, int y = -1);
void addCard(CardItem *card, bool reorganize, int x, int y = -1);
// getCard() finds a card by id.
CardState *getCard(int cardId);
void removeCard(CardState *card);
CardItem *getCard(int cardId);
void removeCard(CardItem *card);
// takeCard() finds a card by position and removes it from the zone and from all of its views.
virtual CardState *takeCard(int position, int cardId, bool canResize = true);
virtual CardItem *takeCard(int position, int cardId, bool canResize = true);
void rawInsertCard(CardState *card, int index)
void rawInsertCard(CardItem *card, int index)
{
cards.insert(index, card);
emit cardCountChanged();
}
[[nodiscard]] const CardList &getCards() const
@ -66,7 +63,6 @@ public:
void sortCards(const QList<CardList::SortOption> &options)
{
cards.sortBy(options);
emit cardCountChanged();
}
[[nodiscard]] QString getName() const
{
@ -81,7 +77,7 @@ public:
{
return cards.getContentsKnown();
}
QList<ZoneViewZoneLogic *> &getViews()
QList<ZoneViewZone *> &getViews()
{
return views;
}
@ -113,12 +109,12 @@ protected:
PlayerLogic *player;
QString name;
CardList cards;
QList<ZoneViewZoneLogic *> views;
QList<ZoneViewZone *> views;
bool hasCardAttr;
bool isShufflable;
bool alwaysRevealTopCard;
virtual void addCardImpl(CardState *card, int x, int y) = 0;
virtual void addCardImpl(CardItem *card, int x, int y) = 0;
};
#endif // COCKATRICE_CARD_ZONE_LOGIC_H

View file

@ -1,6 +1,6 @@
#include "hand_zone_logic.h"
#include "../board/card_state.h"
#include "../../game_graphics/board/card_item.h"
#include "card_zone_algorithms.h"
class PlayerLogic;
@ -16,7 +16,7 @@ HandZoneLogic::HandZoneLogic(PlayerLogic *_player,
{
}
void HandZoneLogic::addCardImpl(CardState *card, int x, int /*y*/)
void HandZoneLogic::addCardImpl(CardItem *card, int x, int /*y*/)
{
CardZoneAlgorithms::addCardToList(cards, card, x, false);
}

View file

@ -26,7 +26,7 @@ public:
QObject *parent = nullptr);
protected:
void addCardImpl(CardState *card, int x, int y) override;
void addCardImpl(CardItem *card, int x, int y) override;
};
#endif // COCKATRICE_HAND_ZONE_LOGIC_H

View file

@ -1,6 +1,10 @@
#include "pile_zone_logic.h"
#include "../board/card_state.h"
#include "../../game_graphics/board/card_item.h"
#include "../board/card_list.h"
class PlayerLogic;
class QObject;
PileZoneLogic::PileZoneLogic(PlayerLogic *_player,
const QString &_name,
@ -12,14 +16,15 @@ PileZoneLogic::PileZoneLogic(PlayerLogic *_player,
{
}
void PileZoneLogic::addCardImpl(CardState *card, int x, int /*y*/)
void PileZoneLogic::addCardImpl(CardItem *card, int x, int /*y*/)
{
connect(card, &CardState::cardPixmapUpdated, this, &PileZoneLogic::callUpdate);
connect(card, &CardItem::sigPixmapUpdated, this, &PileZoneLogic::callUpdate);
// if x is negative set it to add at end
if (x < 0 || x >= cards.size()) {
x = cards.size();
}
cards.insert(x, card);
card->setPos(0, 0);
if (!contentsKnown()) {
card->setCardRef({});
card->setId(-1);
@ -28,5 +33,6 @@ void PileZoneLogic::addCardImpl(CardState *card, int x, int /*y*/)
cards.at(x + 1)->setCardRef({});
}
}
card->resetState(false);
card->setVisible(false);
card->resetState();
}

View file

@ -31,7 +31,7 @@ public:
QObject *parent = nullptr);
protected:
void addCardImpl(CardState *card, int x, int y) override;
void addCardImpl(CardItem *card, int x, int y) override;
};
#endif // COCKATRICE_PILE_ZONE_LOGIC_H

View file

@ -1,6 +1,6 @@
#include "stack_zone_logic.h"
#include "../board/card_state.h"
#include "../../game_graphics/board/card_item.h"
#include "card_zone_algorithms.h"
class PlayerLogic;
@ -16,7 +16,7 @@ StackZoneLogic::StackZoneLogic(PlayerLogic *_player,
{
}
void StackZoneLogic::addCardImpl(CardState *card, int x, int /*y*/)
void StackZoneLogic::addCardImpl(CardItem *card, int x, int /*y*/)
{
CardZoneAlgorithms::addCardToList(cards, card, x, true);
}

View file

@ -26,7 +26,7 @@ public:
QObject *parent = nullptr);
protected:
void addCardImpl(CardState *card, int x, int y) override;
void addCardImpl(CardItem *card, int x, int y) override;
};
#endif // COCKATRICE_STACK_ZONE_LOGIC_H

View file

@ -1,9 +1,14 @@
#include "table_zone_logic.h"
#include "../board/card_state.h"
#include "../../game_graphics/board/card_item.h"
#include "../board/card_list.h"
#include "libcockatrice/card/card_info.h"
#include <QPoint>
class PlayerLogic;
class QObject;
TableZoneLogic::TableZoneLogic(PlayerLogic *_player,
const QString &_name,
bool _hasCardAttr,
@ -14,7 +19,7 @@ TableZoneLogic::TableZoneLogic(PlayerLogic *_player,
{
}
void TableZoneLogic::addCardImpl(CardState *card, int _x, int _y)
void TableZoneLogic::addCardImpl(CardItem *card, int _x, int _y)
{
cards.append(card);
if (!card->getFaceDown() && card->getPT().isEmpty()) {
@ -24,33 +29,15 @@ void TableZoneLogic::addCardImpl(CardState *card, int _x, int _y)
card->setDoesntUntap(true);
}
card->setGridPoint(QPoint(_x, _y));
card->setVisible(true);
}
CardState *TableZoneLogic::takeCard(int position, int cardId, bool toNewZone)
CardItem *TableZoneLogic::takeCard(int position, int cardId, bool toNewZone)
{
CardState *result = CardZoneLogic::takeCard(position, cardId);
CardItem *result = CardZoneLogic::takeCard(position, cardId);
if (toNewZone) {
emit contentSizeChanged();
}
return result;
}
int TableZoneLogic::clampValidTableRow(const int row)
{
if (row < 0) {
return 0;
}
if (row >= ROW_COUNT) {
return ROW_COUNT - 1;
}
return row;
}
int TableZoneLogic::tableRowToGridY(int tableRow)
{
if (tableRow > 2) {
tableRow = 1;
}
return clampValidTableRow(2 - tableRow);
}
}

View file

@ -29,23 +29,8 @@ public:
bool _contentsKnown,
QObject *parent = nullptr);
/** @brief Number of rows in the table zone grid (0=creatures, 1=noncreatures, 2=lands). */
static const int ROW_COUNT = 3;
/**
* Clamps a grid row index into [0, ROW_COUNT - 1].
*/
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);
protected:
void addCardImpl(CardState *card, int x, int y) override;
void addCardImpl(CardItem *card, int x, int y) override;
/**
* @brief Removes a card from view.
@ -53,9 +38,9 @@ protected:
* @param position card position
* @param cardId id of card to take
* @param toNewZone Whether the destination of the card is not the same as the starting zone. Defaults to true
* @return CardState that has been removed
* @return CardItem that has been removed
*/
CardState *takeCard(int position, int cardId, bool toNewZone = true) override;
CardItem *takeCard(int position, int cardId, bool toNewZone = true) override;
};
#endif // COCKATRICE_TABLE_ZONE_LOGIC_H

View file

@ -1,11 +1,10 @@
#include "view_zone_logic.h"
#include "../board/card_state.h"
#include "../player/player_actions.h"
#include "../player/player_logic.h"
#include "../../client/settings/cache_settings.h"
#include "../../game_graphics/board/card_item.h"
#include "../board/card_list.h"
#include <QtGlobal>
#include <libcockatrice/interfaces/interface_interface_settings_provider.h>
#include <libcockatrice/settings/interface_settings.h>
class PlayerLogic;
@ -23,19 +22,13 @@ ZoneViewZoneLogic::ZoneViewZoneLogic(PlayerLogic *_player,
bool _revealZone,
bool _writeableRevealZone,
bool _isReversed,
IInterfaceSettingsProvider *_interfaceSettings,
QObject *parent)
: CardZoneLogic(_player, _origZone->getName(), false, false, true, parent), origZone(_origZone),
numberCards(_numberCards), revealZone(_revealZone), writeableRevealZone(_writeableRevealZone),
isReversed(_isReversed), interfaceSettings(_interfaceSettings)
isReversed(_isReversed)
{
}
bool ZoneViewZoneLogic::getCloseEmptyCardView() const
{
return interfaceSettings ? interfaceSettings->getCloseEmptyCardView() : true;
}
/**
* Checks if inserting a card at the given position requires an actual new card to be created and added to the view.
* Also does any cardId updates that would be required if a card is inserted in that position.
@ -43,7 +36,7 @@ bool ZoneViewZoneLogic::getCloseEmptyCardView() const
* Note that this method can end up modifying the cardIds despite returning false.
* (for example, if the card is inserted into a hidden portion of the deck while the view is reversed)
*
* Make sure to call this method once before calling addCard(), so that you skip creating the card and calling
* Make sure to call this method once before calling addCard(), so that you skip creating a new CardItem and calling
* addCard() if it's not required.
*
* @param x The position to insert the card at.
@ -71,7 +64,7 @@ bool ZoneViewZoneLogic::prepareAddCard(int x)
// autoclose check is done both here and in removeCard
if (cards.isEmpty() && !doInsert && getCloseEmptyCardView()) {
if (cards.isEmpty() && !doInsert && SettingsCache::instance().userInterface().getCloseEmptyCardView()) {
emit closeView();
}
@ -82,7 +75,7 @@ bool ZoneViewZoneLogic::prepareAddCard(int x)
* Make sure prepareAddCard() was called before calling addCard().
* This method assumes we already checked that the card is being inserted into the visible portion
*/
void ZoneViewZoneLogic::addCardImpl(CardState *card, int x, int /*y*/)
void ZoneViewZoneLogic::addCardImpl(CardItem *card, int x, int /*y*/)
{
if (!isReversed) {
// if x is negative set it to add at end
@ -151,14 +144,14 @@ void ZoneViewZoneLogic::removeCard(int position, bool toNewZone)
return;
}
CardState *card = cards.takeAt(position);
card->deleteView();
CardItem *card = cards.takeAt(position);
card->deleteLater();
// The toNewZone check is to prevent the view from auto-closing if the view contains only a single card and that
// card gets dragged within the view.
// Another autoclose check is done in prepareAddCard so that the view autocloses if the last card was moved to an
// unrevealed portion of the same zone.
if (cards.isEmpty() && getCloseEmptyCardView() && toNewZone) {
if (cards.isEmpty() && SettingsCache::instance().userInterface().getCloseEmptyCardView() && toNewZone) {
emit closeView();
return;
}
@ -167,18 +160,12 @@ void ZoneViewZoneLogic::removeCard(int position, bool toNewZone)
reorganizeCards();
}
void ZoneViewZoneLogic::requestClose()
{
emit closeView();
}
void ZoneViewZoneLogic::clearCards()
{
for (auto card : cards) {
card->deleteView();
card->deleteLater();
}
cards.clear();
emit cardCountChanged();
}
void ZoneViewZoneLogic::setWriteableRevealZone(bool _writeableRevealZone)
@ -190,16 +177,4 @@ void ZoneViewZoneLogic::setWriteableRevealZone(bool _writeableRevealZone)
emit removeFromViews();
}
writeableRevealZone = _writeableRevealZone;
}
PendingCommand *ZoneViewZoneLogic::prepareZoneDump()
{
return getPlayer()->getPlayerActions()->prepareZoneDump(this, numberCards);
}
void ZoneViewZoneLogic::sendZoneDump(PendingCommand *pendingCommand)
{
if (pendingCommand != nullptr) {
getPlayer()->getPlayerActions()->sendGameCommand(pendingCommand);
}
}
}

View file

@ -12,8 +12,6 @@
class PlayerLogic;
class QObject;
class PendingCommand;
class IInterfaceSettingsProvider;
class ZoneViewZoneLogic : public CardZoneLogic
{
@ -28,8 +26,6 @@ private:
int numberCards;
bool revealZone, writeableRevealZone;
bool isReversed;
// Borrowed settings provider (owned by the application). Null outside the client, in which case views stay open.
IInterfaceSettingsProvider *interfaceSettings;
public:
enum CardAction
@ -45,25 +41,15 @@ public:
bool _revealZone,
bool _writeableRevealZone,
bool _isReversed,
IInterfaceSettingsProvider *interfaceSettings = nullptr,
QObject *parent = nullptr);
bool prepareAddCard(int x);
void removeCard(int position, bool toNewZone);
void updateCardIds(CardAction action);
/**
* @brief Asks the graphics view showing this zone to close itself.
*
* Logic code has no handle on the view, so it goes through the logic object the view was built from.
*/
void requestClose();
/** @brief Removes all cards from the view, without touching anything else. Used in replay rewind */
void clearCards();
[[nodiscard]] bool getCloseEmptyCardView() const;
int getNumberCards() const
{
return numberCards;
@ -87,25 +73,8 @@ public:
return origZone;
}
/**
* @brief Builds the zone-dump command for this view without sending it.
* @return The pending command, or nullptr when there is no zone to dump.
*
* Keeps command construction in game/ so graphics views don't reach
* through the player into PlayerActions to set it up. The response can
* arrive synchronously (local games), so callers must connect to
* PendingCommand::finished before calling sendZoneDump().
*/
PendingCommand *prepareZoneDump();
/**
* @brief Sends a command returned by prepareZoneDump().
* @param pendingCommand The prepared command; nullptr is ignored.
*/
void sendZoneDump(PendingCommand *pendingCommand);
protected:
void addCardImpl(CardState *card, int x, int y) override;
void addCardImpl(CardItem *card, int x, int y) override;
};
#endif // COCKATRICE_VIEW_ZONE_LOGIC_H

View file

@ -7,7 +7,6 @@
#include <QBrush>
#include <QColor>
#include <QCursor>
#include <QGraphicsScene>
#include <QGraphicsSceneMouseEvent>
#include <QPainter>
#include <QPainterPath>
@ -52,20 +51,7 @@ AbstractCardDragItem::AbstractCardDragItem(AbstractCardItem *_item,
update();
});
// 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();
connect(item, &QObject::destroyed, this, &AbstractCardDragItem::deleteLater);
}
QPainterPath AbstractCardDragItem::shape() const
@ -79,10 +65,6 @@ QPainterPath AbstractCardDragItem::shape() const
void AbstractCardDragItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
if (item.isNull()) {
return;
}
item->paint(painter, option, widget);
// adds a mask to the card so it looks like the card hasnt been placed yet

View file

@ -14,7 +14,6 @@
#include <QList>
#include <QObject>
#include <QPointF>
#include <QPointer>
#include <QRectF>
#include <qtmetamacros.h>
@ -25,8 +24,7 @@ class AbstractCardDragItem : public QObject, public QGraphicsItem
Q_OBJECT
Q_INTERFACES(QGraphicsItem)
protected:
/// Guarded pointer: the source card may be destroyed while this drag item still exists.
QPointer<AbstractCardItem> item;
AbstractCardItem *item;
QPointF hotSpot;
QList<AbstractCardDragItem *> childDrags;
@ -46,8 +44,10 @@ public:
}
[[nodiscard]] QPainterPath shape() const override;
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
/// @return the source card, or nullptr if it has already been destroyed.
[[nodiscard]] AbstractCardItem *getItem() const;
[[nodiscard]] AbstractCardItem *getItem() const
{
return item;
}
[[nodiscard]] QPointF getHotSpot() const
{
return hotSpot;

View file

@ -35,6 +35,7 @@
#include <QVariant>
#include <QtGlobal>
#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/debug_settings.h>
@ -43,23 +44,13 @@
class QSizeF;
AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef, PlayerLogic *_owner, int _id)
: ArrowTarget(_owner, parent), state(new CardState(this, nullptr, cardRef, _id)), tapAngle(0),
bgColor(Qt::transparent), isHovered(false), realZValue(0)
: ArrowTarget(_owner, parent), id(_id), cardRef(cardRef), tapped(false), facedown(false), tapAngle(0),
bgColor(Qt::transparent), isHovered(false), realZValue(0), pendingSelectionCollapse(false)
{
setCursor(Qt::OpenHandCursor);
setFlag(ItemIsSelectable);
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,
[this] { update(); });
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this,
@ -77,7 +68,7 @@ AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef
AbstractCardItem::~AbstractCardItem()
{
emit deleteCardInfoPopup(state->getCardRef().name);
emit deleteCardInfoPopup(cardRef.name);
}
QRectF AbstractCardItem::boundingRect() const
@ -100,20 +91,21 @@ void AbstractCardItem::pixmapUpdated()
emit sigPixmapUpdated();
}
void AbstractCardItem::onCardInfoChanged()
{
cacheBgColor();
update();
}
void AbstractCardItem::onCardRefChanged(const CardRef &oldCardRef, const CardRef & /*newCardRef*/)
{
emit deleteCardInfoPopup(oldCardRef.name);
}
void AbstractCardItem::refreshCardInfo()
{
state->refreshCardInfo();
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();
update();
}
/**
@ -122,7 +114,7 @@ void AbstractCardItem::refreshCardInfo()
*/
const CardInfo &AbstractCardItem::getCardInfo() const
{
return state->getCardInfo();
return exactCard.getInfo();
}
void AbstractCardItem::setRealZValue(qreal _zValue)
@ -167,13 +159,13 @@ void AbstractCardItem::paintPicture(QPainter *painter, const QSizeF &translatedS
QPixmap translatedPixmap;
bool paintImage = true;
if (state->getFaceDown() || state->getCardRef().name.isEmpty()) {
if (facedown || cardRef.name.isEmpty()) {
// never reveal card color, always paint the card back
CardPictureLoader::getCardBackPixmap(translatedPixmap, translatedSize.toSize());
} else {
// don't even spend time trying to load the picture if our size is too small
if (translatedSize.width() > 10) {
CardPictureLoader::getPixmap(translatedPixmap, state->getCard(), translatedSize.toSize());
CardPictureLoader::getPixmap(translatedPixmap, exactCard, translatedSize.toSize());
if (translatedPixmap.isNull()) {
paintImage = false;
}
@ -194,20 +186,19 @@ void AbstractCardItem::paintPicture(QPainter *painter, const QSizeF &translatedS
painter->drawPath(shape());
}
if (translatedPixmap.isNull() || SettingsCache::instance().cardsDisplay().getDisplayCardNames() ||
state->getFaceDown()) {
if (translatedPixmap.isNull() || SettingsCache::instance().cardsDisplay().getDisplayCardNames() || facedown) {
painter->save();
transformPainter(painter, translatedSize, angle);
painter->setPen(Qt::white);
painter->setBackground(Qt::black);
painter->setBackgroundMode(Qt::OpaqueMode);
QString nameStr;
if (state->getFaceDown()) {
nameStr = "# " + QString::number(state->getId());
if (facedown) {
nameStr = "# " + QString::number(id);
} else {
QString prefix = "";
if (SettingsCache::instance().debug().getShowCardId()) {
prefix = "#" + QString::number(state->getId()) + " ";
prefix = "#" + QString::number(id) + " ";
}
nameStr = prefix + CardLocalization::displayName(getCardInfo());
}
@ -245,6 +236,21 @@ void AbstractCardItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *
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)
{
if (isHovered == _hovered) {
@ -270,17 +276,18 @@ void AbstractCardItem::setHovered(bool _hovered)
void AbstractCardItem::setColor(const QString &_color)
{
state->setColor(_color);
color = _color;
cacheBgColor();
update();
}
void AbstractCardItem::cacheBgColor()
{
QChar colorChar;
const QString cardColor = state->getColor();
if (cardColor.isEmpty()) {
colorChar = state->getCardInfo().getColorChar();
if (color.isEmpty()) {
colorChar = exactCard.getInfo().getColorChar();
} else {
colorChar = cardColor.at(0);
colorChar = color.at(0);
}
switch (colorChar.toLower().toLatin1()) {
@ -308,12 +315,17 @@ void AbstractCardItem::cacheBgColor()
}
}
void AbstractCardItem::onTappedChanged(bool newTapped, bool canAnimate)
void AbstractCardItem::setTapped(bool _tapped, bool canAnimate)
{
if (tapped == _tapped) {
return;
}
tapped = _tapped;
if (SettingsCache::instance().cardsDisplay().getTapAnimation() && canAnimate) {
static_cast<GameScene *>(scene())->registerAnimationItem(this);
} else {
tapAngle = newTapped ? 90 : 0;
tapAngle = tapped ? 90 : 0;
setTransform(QTransform()
.translate(CardDimensions::WIDTH_HALF_F, CardDimensions::HEIGHT_HALF_F)
.rotate(tapAngle)
@ -327,6 +339,12 @@ bool AbstractCardItem::animationEvent()
return false;
}
void AbstractCardItem::setFaceDown(bool _facedown)
{
facedown = _facedown;
update();
}
void AbstractCardItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
cancelPendingSelectionCollapse();
@ -340,7 +358,7 @@ void AbstractCardItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
}
}
if ((event->modifiers() & Qt::AltModifier) && event->button() == Qt::LeftButton) {
emit cardShiftClicked(state->getCardRef().name);
emit cardShiftClicked(cardRef.name);
} else if ((event->modifiers() & Qt::ControlModifier)) {
setSelected(!isSelected());
} else if (!isSelected()) {
@ -354,7 +372,7 @@ void AbstractCardItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
if (event->button() == Qt::LeftButton) {
setCursor(Qt::ClosedHandCursor);
} else if (event->button() == Qt::MiddleButton) {
emit showCardInfoPopup(event->screenPos(), state->getCardRef());
emit showCardInfoPopup(event->screenPos(), cardRef);
}
event->accept();
}
@ -388,7 +406,7 @@ void AbstractCardItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
}
if (event->button() == Qt::MiddleButton) {
emit deleteCardInfoPopup(state->getCardRef().name);
emit deleteCardInfoPopup(cardRef.name);
}
// This function ensures the parent function doesn't mess around with our selection.

View file

@ -16,7 +16,8 @@
#include <QSizeF>
#include <QString>
#include <QtGlobal>
#include <libcockatrice/game/board/card_state.h>
#include <libcockatrice/card/printing/exact_card.h>
#include <libcockatrice/utility/card_ref.h>
#include <qtmetamacros.h>
class PlayerLogic;
@ -28,8 +29,13 @@ class AbstractCardItem : public ArrowTarget, public IAnimatedItem
{
Q_OBJECT
protected:
CardState *state;
ExactCard exactCard;
int id;
CardRef cardRef;
bool tapped;
bool facedown;
int tapAngle;
QString color;
QColor bgColor;
private:
@ -39,9 +45,6 @@ private:
int selectionCollapseGeneration = 0; ///< Guards the deferred collapse timer against stale firings
private slots:
void pixmapUpdated();
void onCardInfoChanged();
void onCardRefChanged(const CardRef &oldCardRef, const CardRef &newCardRef);
void onTappedChanged(bool newTapped, bool canAnimate);
public slots:
void refreshCardInfo();
@ -77,38 +80,31 @@ public:
QSizeF getTranslatedSize(QPainter *painter) const;
void paintPicture(QPainter *painter, const QSizeF &translatedSize, int angle);
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
[[nodiscard]] CardState *getState() const
{
return state;
}
ExactCard getCard() const
{
return state->getCard();
return exactCard;
}
const CardInfo &getCardInfo() const;
int getId() const
{
return state->getId();
return id;
}
void setId(int _id)
{
state->setId(_id);
id = _id;
}
QString getName() const
{
return state->getCardRef().name;
return cardRef.name;
}
QString getProviderId() const
{
return state->getCardRef().providerId;
}
void setCardRef(const CardRef &_cardRef)
{
state->setCardRef(_cardRef);
return cardRef.providerId;
}
void setCardRef(const CardRef &_cardRef);
CardRef getCardRef() const
{
return state->getCardRef();
return cardRef;
}
qreal getRealZValue() const
{
@ -120,33 +116,27 @@ public:
{
return isHovered;
}
[[nodiscard]] QString getColor() const
QString getColor() const
{
return state->getColor();
return color;
}
void setColor(const QString &_color);
bool getTapped() const
{
return state->getTapped();
}
void setTapped(bool _tapped, bool canAnimate = false)
{
state->setTapped(_tapped, canAnimate);
return tapped;
}
void setTapped(bool _tapped, bool canAnimate = false);
bool getFaceDown() const
{
return state->getFaceDown();
}
void setFaceDown(bool _facedown)
{
state->setFaceDown(_facedown);
return facedown;
}
void setFaceDown(bool _facedown);
void processHoverEvent();
/** @brief Drop a pending collapse and disarm any deferred collapse timer. */
void cancelPendingSelectionCollapse();
void deleteCardInfoPopup()
{
emit deleteCardInfoPopup(state->getCardRef().name);
emit deleteCardInfoPopup(cardRef.name);
}
/** @brief Default: no per-tick animation. Subclasses override to animate. */

View file

@ -2,6 +2,10 @@
#include "../../client/settings/cache_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 "../../interface/widgets/menus/tearoff_menu.h"
@ -19,10 +23,8 @@
#include <QString>
#include <QVariant>
#include <QtGlobal>
#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/protocol/pb/command_inc_counter.pb.h>
#include <libcockatrice/protocol/pb/command_set_counter.pb.h>
#include <libcockatrice/utility/expression.h>
#include <qnamespace.h>
@ -149,7 +151,10 @@ void AbstractCounter::mousePressEvent(QGraphicsSceneMouseEvent *event)
menu->exec(event->screenPos());
}
} else {
player->getPlayerActions()->actIncrementCounter(id, event->button() == Qt::LeftButton ? 1 : -1);
Command_IncCounter cmd;
cmd.set_counter_id(id);
cmd.set_delta(event->button() == Qt::LeftButton ? 1 : -1);
player->getPlayerActions()->sendGameCommand(cmd);
}
event->accept();
}
@ -167,7 +172,10 @@ void AbstractCounter::hoverLeaveEvent(QGraphicsSceneHoverEvent *)
void AbstractCounter::incrementCounter()
{
player->getPlayerActions()->actIncrementCounter(id, static_cast<QAction *>(sender())->data().toInt());
Command_IncCounter cmd;
cmd.set_counter_id(id);
cmd.set_delta(static_cast<QAction *>(sender())->data().toInt());
player->getPlayerActions()->sendGameCommand(cmd);
}
void AbstractCounter::setCounter()
@ -191,7 +199,10 @@ void AbstractCounter::setCounter()
}
Expression exp(value);
player->getPlayerActions()->actSetCounter(id, static_cast<int>(exp.parse(dlg.textValue())));
Command_SetCounter cmd;
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)

View file

@ -2,6 +2,11 @@
#include "arrow_item.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 "../player/player_target.h"
#include "../z_values.h"
@ -22,12 +27,12 @@
#include <QTransform>
#include <QtGlobal>
#include <QtMath>
#include <libcockatrice/game/board/arrow_data.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/card/card_info.h>
#include <libcockatrice/protocol/pb/command_attach_card.pb.h>
#include <libcockatrice/protocol/pb/command_create_arrow.pb.h>
#include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/settings/interface_settings.h>
#include <libcockatrice/utility/color.h>
#include <libcockatrice/utility/zone_names.h>
#include <math.h>
#include <qnamespace.h>
@ -421,19 +426,19 @@ void ArrowDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
CardZoneLogic *startZone = startCard->getZone();
ArrowData arrow;
arrow.color = data->color;
arrow.startPlayerId = startZone->getPlayer()->getPlayerInfo()->getId();
arrow.startZone = startZone->getName();
arrow.startCardId = startCard->getId();
Command_CreateArrow cmd;
cmd.mutable_arrow_color()->CopyFrom(convertQColorToColor(data->color));
cmd.set_start_player_id(startZone->getPlayer()->getPlayerInfo()->getId());
cmd.set_start_zone(startZone->getName().toStdString());
cmd.set_start_card_id(startCard->getId());
if (auto *targetCard = qgraphicsitem_cast<CardItem *>(targetItem)) {
CardZoneLogic *targetZone = targetCard->getZone();
arrow.targetPlayerId = targetZone->getPlayer()->getPlayerInfo()->getId();
arrow.targetZone = targetZone->getName();
arrow.targetCardId = targetCard->getId();
cmd.set_target_player_id(targetZone->getPlayer()->getPlayerInfo()->getId());
cmd.set_target_zone(targetZone->getName().toStdString());
cmd.set_target_card_id(targetCard->getId());
} else if (auto *targetPlayer = qgraphicsitem_cast<PlayerTarget *>(targetItem)) {
arrow.targetPlayerId = targetPlayer->getOwner()->getPlayerInfo()->getId();
cmd.set_target_player_id(targetPlayer->getOwner()->getPlayerInfo()->getId());
} else {
delArrow();
return;
@ -442,9 +447,22 @@ 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 (startZone->getName() == ZoneNames::HAND) {
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);
}
}
player->getPlayerActions()->createArrow(startCard->getState(), arrow, deleteInPhase);
if (deleteInPhase != 0) {
cmd.set_delete_in_phase(deleteInPhase);
}
player->getPlayerActions()->sendGameCommand(cmd);
}
delArrow();
@ -536,7 +554,7 @@ void ArrowAttachItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
auto *startCard = qgraphicsitem_cast<CardItem *>(startItem);
auto *targetCard = qgraphicsitem_cast<CardItem *>(targetItem);
if (startCard && targetCard) {
player->getPlayerActions()->attachCards(startCard->getState(), targetCard->getState());
attachCards(startCard, targetCard);
}
}
@ -545,3 +563,23 @@ void ArrowAttachItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *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);
}

View file

@ -1,6 +1,7 @@
#ifndef ARROWITEM_H
#define ARROWITEM_H
#include "../../game/board/arrow_data.h"
#include "../animated_item.h"
#include "arrow_target.h"
#include "graphics_item_type.h"
@ -17,7 +18,6 @@
#include <QRectF>
#include <QSharedPointer>
#include <QtGlobal>
#include <libcockatrice/game/board/arrow_data.h>
#include <qtmetamacros.h>
class CardItem;
@ -131,6 +131,7 @@ private:
PlayerLogic *player;
QList<ArrowAttachItem *> childArrows;
QMetaObject::Connection positionConnection;
void attachCards(CardItem *startCard, const CardItem *targetCard);
public:
explicit ArrowAttachItem(ArrowTarget *_startItem);

View file

@ -130,9 +130,8 @@ void CardDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
sc->removeItem(this);
QList<CardDragItem *> dragItemList;
CardZoneLogic *startZone = static_cast<CardItem *>(item.data())->getZone();
if (currentZone &&
!(static_cast<CardItem *>(item.data())->getAttachedTo() && (startZone == currentZone->getLogic()))) {
CardZoneLogic *startZone = static_cast<CardItem *>(item)->getZone();
if (currentZone && !(static_cast<CardItem *>(item)->getAttachedTo() && (startZone == currentZone->getLogic()))) {
if (!occupied) {
dragItemList.append(this);
}
@ -140,7 +139,7 @@ void CardDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
for (int i = 0; i < childDrags.size(); i++) {
CardDragItem *c = static_cast<CardDragItem *>(childDrags[i]);
if (!occupied &&
!(static_cast<CardItem *>(c->item.data())->getAttachedTo() && (startZone == currentZone->getLogic())) &&
!(static_cast<CardItem *>(c->item)->getAttachedTo() && (startZone == currentZone->getLogic())) &&
!c->occupied) {
dragItemList.append(c);
}

View file

@ -1,6 +1,17 @@
#include "card_item.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 "../game_scene.h"
#include "arrow_item.h"
@ -30,17 +41,6 @@
#include <QVariant>
#include <QtGlobal>
#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/settings/interface_settings.h>
#include <libcockatrice/utility/card_ref.h>
@ -53,9 +53,8 @@ CardItem::CardItem(PlayerLogic *_owner,
const CardRef &cardRef,
int _cardid,
CardZoneLogic *_zone)
: AbstractCardItem(parent, cardRef, _owner, _cardid), dragItem(nullptr)
: AbstractCardItem(parent, cardRef, _owner, _cardid), state(new CardState(this, _zone)), dragItem(nullptr)
{
state->setZone(_zone);
owner->addCard(this);
connect(&SettingsCache::instance().cardCounters(), &CardCounterSettings::colorChanged, this, [this](int counterId) {
@ -63,44 +62,25 @@ CardItem::CardItem(PlayerLogic *_owner,
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()
{
if (owner != nullptr) {
auto *gameScene = qobject_cast<GameScene *>(scene());
if (gameScene && gameScene->getActiveCard() == this) {
if (owner->getGame()->getActiveCard() == this) {
emit owner->requestCardMenuUpdate(nullptr);
gameScene->setActiveCard(nullptr);
owner->getGame()->setActiveCard(nullptr);
}
owner = nullptr;
}
const QList<CardState *> &attachedStates = state->getAttachedCards();
while (!attachedStates.isEmpty()) {
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);
while (!attachedCards.isEmpty()) {
attachedCards.first()->setZone(nullptr); // so that it won't try to call reorganizeCards()
attachedCards.first()->setAttachedTo(nullptr);
}
if (state->getAttachedTo() != nullptr) {
state->getAttachedTo()->removeAttachedCard(state);
state->getAttachedTo()->removeAttachedCard(this);
state->setAttachedTo(nullptr);
}
}
@ -147,7 +127,7 @@ void CardItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option,
painter->save();
transformPainter(painter, translatedSize, tapAngle);
if (!getFaceDown() && state->getPT() == state->getCardInfo().getPowTough()) {
if (!getFaceDown() && state->getPT() == exactCard.getInfo().getPowTough()) {
painter->setPen(Qt::white);
} else {
painter->setPen(QColor(255, 150, 0)); // dark orange
@ -229,13 +209,36 @@ void CardItem::setPT(const QString &_pt)
void CardItem::setAttachedTo(CardItem *_attachedTo)
{
CardState *newParent = _attachedTo == nullptr ? nullptr : _attachedTo->getState();
state->setAttachedTo(newParent);
if (state->getAttachedTo() != nullptr) {
state->getAttachedTo()->removeAttachedCard(this);
}
// 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 (newParent != nullptr ? newParent->getZone() == nullptr : state->getZone() == nullptr) {
deleteLater();
gridPoint.setX(-1);
state->setAttachedTo(_attachedTo);
if (state->getAttachedTo() != nullptr) {
// 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->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();
}
}
@ -245,6 +248,13 @@ void CardItem::setAttachedTo(CardItem *_attachedTo)
void CardItem::resetState(bool 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)
@ -380,7 +390,7 @@ void CardItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
// Use the buttonDownPos to align the hot spot with the position when
// the user originally clicked
createDragItem(state->getId(), event->buttonDownPos(Qt::LeftButton), event->scenePos(), forceFaceDown);
createDragItem(id, event->buttonDownPos(Qt::LeftButton), event->scenePos(), forceFaceDown);
dragItem->grabMouse();
int childIndex = 0;
@ -423,11 +433,42 @@ void CardItem::playCard(bool faceDown)
emit playSelected(this);
}
} else {
state->getZone()->getPlayer()->getPlayerActions()->playCard(state, faceDown);
state->getZone()->getPlayer()->getPlayerActions()->playCard(this, 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
* is nullptr.
@ -452,7 +493,7 @@ void CardItem::handleClickedToPlay(bool shiftHeld)
if (SettingsCache::instance().userInterface().getClickPlaysAllSelected()) {
emit hideSelected(this);
} else {
state->getZone()->removeCard(state);
state->getZone()->removeCard(this);
}
} else {
playCard(shiftHeld);
@ -496,16 +537,16 @@ bool CardItem::animationEvent()
{
int rotation = ROTATION_DEGREES_PER_FRAME;
bool animationIncomplete = true;
if (!state->getTapped()) {
if (!tapped) {
rotation *= -1;
}
tapAngle += rotation;
if (state->getTapped() && (tapAngle > 90)) {
if (tapped && (tapAngle > 90)) {
tapAngle = 90;
animationIncomplete = false;
}
if (!state->getTapped() && (tapAngle < 0)) {
if (!tapped && (tapAngle < 0)) {
tapAngle = 0;
animationIncomplete = false;
}
@ -526,9 +567,7 @@ QVariant CardItem::itemChange(GraphicsItemChange change, const QVariant &value)
bool selected = value.toBool();
if (selected) {
if (auto *gameScene = qobject_cast<GameScene *>(scene())) {
gameScene->setActiveCard(this);
}
owner->getGame()->setActiveCard(this);
}
emit selectionChanged(this, selected);

View file

@ -7,6 +7,7 @@
#ifndef CARDITEM_H
#define CARDITEM_H
#include "../../game/board/card_state.h"
#include "abstract_card_item.h"
#include "graphics_item_type.h"
@ -15,7 +16,6 @@
#include <QPointF>
#include <QString>
#include <QVariantList>
#include <libcockatrice/game/board/card_state.h>
#include <qtmetamacros.h>
class CardDragItem;
@ -33,7 +33,11 @@ class CardItem : public AbstractCardItem
{
Q_OBJECT
private:
CardState *state;
QPoint gridPoint;
CardDragItem *dragItem;
QList<CardItem *> attachedCards;
void prepareDelete();
void handleClickedToPlay(bool shiftHeld);
@ -56,6 +60,10 @@ public:
CardZoneLogic *_zone = nullptr);
void retranslateUi();
[[nodiscard]] CardState *getState() const
{
return state;
}
[[nodiscard]] CardZoneLogic *getZone() const
{
return state->getZone();
@ -64,15 +72,15 @@ public:
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
[[nodiscard]] QPoint getGridPoint() const
{
return state->getGridPoint();
return gridPoint;
}
void setGridPoint(const QPoint &_gridPoint)
{
state->setGridPoint(_gridPoint);
gridPoint = _gridPoint;
}
[[nodiscard]] QPoint getGridPos() const
{
return state->getGridPoint();
return gridPoint;
}
[[nodiscard]] PlayerLogic *getOwner() const
{
@ -115,22 +123,22 @@ public:
{
state->setDestroyOnZoneChange(_destroy);
}
[[nodiscard]] CardState *getAttachedTo() const
[[nodiscard]] CardItem *getAttachedTo() const
{
return state->getAttachedTo();
}
void setAttachedTo(CardItem *_attachedTo);
void addAttachedCard(CardState *card)
void addAttachedCard(CardItem *card)
{
state->addAttachedCard(card);
attachedCards.append(card);
}
void removeAttachedCard(CardState *card)
void removeAttachedCard(CardItem *card)
{
state->removeAttachedCard(card);
attachedCards.removeOne(card);
}
[[nodiscard]] const QList<CardState *> &getAttachedCards() const
[[nodiscard]] const QList<CardItem *> &getAttachedCards() const
{
return state->getAttachedCards();
return attachedCards;
}
void resetState(bool keepAnnotations = false);
void processCardInfo(const ServerInfo_Card &_info);
@ -142,6 +150,26 @@ public:
void drawAttachArrow();
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:
void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;

View file

@ -77,7 +77,7 @@ void DeckViewCardDragItem::updatePosition(const QPointF &cursorScenePos)
void DeckViewCardDragItem::handleDrop(DeckViewCardContainer *target)
{
auto *card = static_cast<DeckViewCard *>(item.data());
auto *card = static_cast<DeckViewCard *>(item);
auto *start = static_cast<DeckViewCardContainer *>(item->parentItem());
start->removeCard(card);
target->addCard(card);

View file

@ -2,8 +2,13 @@
#include "../../client/settings/cache_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/deck_loader/deck_file_format.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_from_clipboard.h"
#include "../../interface/widgets/dialogs/dlg_load_deck_from_website.h"
@ -28,17 +33,16 @@
#include <QRect>
#include <QVBoxLayout>
#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/game/abstract_game.h>
#include <libcockatrice/game/game_event_handler.h>
#include <libcockatrice/game/game_state.h>
#include <libcockatrice/protocol/pb/command_deck_select.pb.h>
#include <libcockatrice/protocol/pb/command_ready_start.pb.h>
#include <libcockatrice/protocol/pb/command_set_playmat.pb.h>
#include <libcockatrice/protocol/pb/command_set_sideboard_lock.pb.h>
#include <libcockatrice/protocol/pb/command_set_sideboard_plan.pb.h>
#include <libcockatrice/protocol/pb/response_deck_download.pb.h>
#include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/settings/interface_settings.h>
#include <libcockatrice/settings/visual_deck_storage_settings.h>
#include <libcockatrice/utility/qt_utils.h>
#include <libcockatrice/utility/string_limits.h>
#include <optional>
#include <qnamespace.h>
@ -107,9 +111,10 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent)
connect(deckView, &DeckView::newCardAdded, this, &DeckViewContainer::newCardAdded);
connect(deckView, &DeckView::sideboardPlanChanged, this, &DeckViewContainer::sideboardPlanChanged);
deckViewLayout = QtUtils::makeLayout<QVBoxLayout>();
deckViewLayout = new QVBoxLayout;
deckViewLayout->addLayout(buttonHBox);
deckViewLayout->addWidget(deckView);
deckViewLayout->setContentsMargins(0, 0, 0, 0);
setLayout(deckViewLayout);
retranslateUi();
@ -309,7 +314,9 @@ void DeckViewContainer::loadDeckFromDeckList(const DeckList &deck)
return;
}
PendingCommand *pend = parentGame->getGame()->getGameEventHandler()->prepareDeckSelect(deckString);
Command_DeckSelect cmd;
cmd.set_deck(deckString.toStdString());
PendingCommand *pend = parentGame->getGame()->getGameEventHandler()->prepareGameCommand(cmd);
connect(pend, &PendingCommand::finished, this, &DeckViewContainer::deckSelectFinished);
parentGame->getGame()->getGameEventHandler()->sendGameCommand(pend, playerId);
@ -338,20 +345,15 @@ void DeckViewContainer::resolveAndSendPlaymat()
lastResolvedPlaymat = resolved;
PendingCommand *playmatPend = parentGame->getGame()->getGameEventHandler()->prepareSetPlaymat(resolved);
const QString sentName = resolved.card.name;
connect(playmatPend, &PendingCommand::finished, this, [this, sentName](const Response &r) {
// Only RespFunctionNotAllowed signals a card art DENY rule. Older servers that predate
// playmats answer RespInvalidCommand and strip the mat from DeckSelect, which must stay silent.
if (r.response_code() == Response::RespFunctionNotAllowed) {
if (!sentName.isEmpty()) {
notifyPlaymatBlacklisted(
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();
}
});
Command_SetPlaymat playmatCmd;
auto *pp = playmatCmd.mutable_playmat_params();
pp->set_card_name(resolved.card.name.toStdString());
pp->set_card_provider_id(resolved.card.providerId.toStdString());
pp->set_margin_pct_l(resolved.params.marginPctL);
pp->set_margin_pct_r(resolved.params.marginPctR);
pp->set_vertical_offset(resolved.params.verticalOffset);
pp->set_zoom(resolved.params.zoom);
PendingCommand *playmatPend = parentGame->getGame()->getGameEventHandler()->prepareGameCommand(playmatCmd);
parentGame->getGame()->getGameEventHandler()->sendGameCommand(playmatPend, playerId);
}
@ -369,7 +371,9 @@ void DeckViewContainer::loadRemoteDeck()
{
DlgLoadRemoteDeck dlg(parentGame->getGame()->getClientForPlayer(playerId), this);
if (dlg.exec()) {
PendingCommand *pend = parentGame->getGame()->getGameEventHandler()->prepareDeckSelectId(dlg.getDeckId());
Command_DeckSelect cmd;
cmd.set_deck_id(dlg.getDeckId());
PendingCommand *pend = parentGame->getGame()->getGameEventHandler()->prepareGameCommand(cmd);
connect(pend, &PendingCommand::finished, this, &DeckViewContainer::deckSelectFinished);
parentGame->getGame()->getGameEventHandler()->sendGameCommand(pend, playerId);
}
@ -408,15 +412,6 @@ void DeckViewContainer::deckSelectFinished(const Response &r)
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()
{
sendReadyStartCommand(!readyStartButton->getState());
@ -425,21 +420,34 @@ void DeckViewContainer::readyStart()
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.");
if (!QtUtils::confirm(this, tr("Cockatrice"), msg, QMessageBox::Yes)) {
const auto res =
QMessageBox::question(this, tr("Cockatrice"), msg, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
if (res == QMessageBox::No) {
return;
}
parentGame->getGame()->getGameEventHandler()->sendForceStart(playerId);
Command_ReadyStart cmd;
cmd.set_force_start(true);
cmd.set_ready(true);
parentGame->getGame()->getGameEventHandler()->sendGameCommand(cmd, playerId);
}
void DeckViewContainer::sideboardLockButtonClicked()
{
parentGame->getGame()->getGameEventHandler()->sendSetSideboardLock(sideboardLockButton->getState(), playerId);
Command_SetSideboardLock cmd;
cmd.set_locked(sideboardLockButton->getState());
parentGame->getGame()->getGameEventHandler()->sendGameCommand(cmd, playerId);
}
void DeckViewContainer::sideboardPlanChanged()
{
parentGame->getGame()->getGameEventHandler()->sendSetSideboardPlan(deckView->getSideboardPlan(), playerId);
Command_SetSideboardPlan cmd;
const QList<MoveCard_ToZone> &newPlan = deckView->getSideboardPlan();
for (const auto &i : newPlan) {
cmd.add_move_list()->CopyFrom(i);
}
parentGame->getGame()->getGameEventHandler()->sendGameCommand(cmd, playerId);
}
/**
@ -451,7 +459,9 @@ void DeckViewContainer::sendReadyStartCommand(bool ready)
resolveAndSendPlaymat();
}
parentGame->getGame()->getGameEventHandler()->sendReadyStart(ready, playerId);
Command_ReadyStart cmd;
cmd.set_ready(ready);
parentGame->getGame()->getGameEventHandler()->sendGameCommand(cmd, playerId);
}
/**

View file

@ -60,14 +60,12 @@ private:
VisualDeckStorageWidget *visualDeckStorageWidget;
TabGame *parentGame;
int playerId;
int playmatRotationIndex = 0; ///< Per-match cursor for round-robin playmat mode.
DeckList currentDeck; ///< Cached deck for live settings re-resolution.
PlaymatInfo lastResolvedPlaymat; ///< Tracks last sent playmat to avoid repeats in random mode.
QString lastBlacklistedPlaymatNotice; ///< Card name already reported as blacklisted, to avoid repeat popups.
int playmatRotationIndex = 0; ///< Per-match cursor for round-robin playmat mode.
DeckList currentDeck; ///< Cached deck for live settings re-resolution.
PlaymatInfo lastResolvedPlaymat; ///< Tracks last sent playmat to avoid repeats in random mode.
void tryCreateVisualDeckStorageWidget();
void sendReadyStartCommand(bool ready);
void notifyPlaymatBlacklisted(const QString &cardName, const QString &message);
private slots:
void switchToDeckSelectView();
void switchToDeckLoadedView();

View file

@ -1,13 +0,0 @@
#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);
}

View file

@ -1,22 +0,0 @@
/**
* @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

View file

@ -11,7 +11,6 @@
#include <QList>
#include <QString>
#include <QStringList>
#include <libcockatrice/game/player/token_info.h>
#include <qtmetamacros.h>
class QLabel;
@ -26,6 +25,17 @@ class CardInfoPictureWidget;
class QModelIndex;
class QWidget;
struct TokenInfo
{
QString name;
QString color;
QString pt;
QString annotation;
bool destroy = true;
bool faceDown = false;
QString providerId;
};
class DlgCreateToken : public QDialog
{
Q_OBJECT

Some files were not shown because too many files have changed in this diff Show more