diff --git a/.ci/compile.sh b/.ci/compile.sh
index 9be8ba741..63c027473 100755
--- a/.ci/compile.sh
+++ b/.ci/compile.sh
@@ -311,24 +311,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 +459,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"
diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml
index df054ea26..c97b48342 100644
--- a/.github/workflows/desktop-build.yml
+++ b/.github/workflows/desktop-build.yml
@@ -101,36 +101,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
@@ -496,7 +466,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 +584,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"
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8a25fe2c6..97b87a1d8 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -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)
@@ -250,22 +250,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)
@@ -435,9 +420,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 +431,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 +443,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()
diff --git a/README.md b/README.md
index fa9272c0f..f2a170868 100644
--- a/README.md
+++ b/README.md
@@ -15,10 +15,9 @@
---
-To get started with Cockatrice ⇢ [view our webpage](https://cockatrice.github.io/)
-To get support, or suggest changes to the app ⇢ [file an issue](https://github.com/Cockatrice/Cockatrice/issues) ([How?](https://github.com/Cockatrice/Cockatrice/wiki/How-to-Create-a-GitHub-Ticket-Regarding-Cockatrice))
-To learn about the app and its code ⇢ [read our code documentation](https://cockatrice.github.io/docs/)
-To help with development ⇢ learn [how to contribute](#contribute)
+To get started with Cockatrice ⇢ [view our webpage](https://cockatrice.github.io/)
+To get support, or suggest changes to the app ⇢ [file an issue](https://github.com/Cockatrice/Cockatrice/issues) ([How?](https://github.com/Cockatrice/Cockatrice/wiki/How-to-Create-a-GitHub-Ticket-Regarding-Cockatrice))
+To help with development ⇢ learn [how to contribute](#contribute-)
@@ -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.
+To contribute code to the project, please review our [guidelines](https://github.com/Cockatrice/Cockatrice/blob/master/.github/CONTRIBUTING.md) first.
We maintain two tags for contributors to easier find issues to potentially work on:
- [](https://github.com/Cockatrice/Cockatrice/issues?utf8=%E2%9C%93&q=is%3Aopen%20is%3Aissue%20label%3A%22Good%20first%20issue%22%20)
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 [](https://github.com/Cockatrice/Cockatrice/actions/workflows/documentation-build.yml?query=event%3Arelease+event%3Aworkflow_dispatch)
+### Documentation [](https://github.com/Cockatrice/Cockatrice/actions/workflows/documentation-build.yml?query=event%3Apush)
There are various places where useful information for different needs are maintained:
- [Official Code Documentation](https://cockatrice.github.io/docs/)
@@ -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 [](https://explore.transifex.com/cockatrice/cockatrice/)
diff --git a/cmake/NSIS.template.in b/cmake/NSIS.template.in
index 923aded9d..0ef746430 100644
--- a/cmake/NSIS.template.in
+++ b/cmake/NSIS.template.in
@@ -12,24 +12,10 @@ Var NormalDestDir
Var PortableDestDir
Var PortableMode
Var ReinstallMode
+Var AllowForceClose
Var GuardDir
Var OldUninstallString
Var OldInstallDir
-; What ExecWait reported for the previous version's uninstaller, or "missing" /
-; "launch-failed" when RunOldUninstaller could not start it. Read by WaitForOldPayloadToDisappear: an uninstaller that ran in place
-; (_?=) and failed or was cancelled will not remove anything more, however long we wait.
-Var OldUninstallExit
-; Executable path parsed out of $OldUninstallString by ResolveOldUninstallExe. Used to
-; tell an uninstaller that is missing or cannot be started ("missing" / "launch-failed"
-; in $OldUninstallExit) from one that ran and failed.
-Var OldUninstallExe
-; Set to 1 by RemoveOldPayloadLeftovers only when every leftover it is responsible for is
-; gone; WaitForOldPayloadToDisappear aborts while it is still 0.
-Var OldPayloadRemoved
-; Scratch state of AppendUninstallerLog.
-Var UninstLogHandle
-Var UninstLogLine
-Var UninstLogChar
Var DataBackupDir
Var DataBackupDone
Var DataBackupForDir
@@ -41,48 +27,9 @@ Var StaleFindName
Var DataBackupFindHandle
Var DataBackupFindName
Var IsUpgrade
-; Set together by whichever purge block ran: the verdict RetryDllPurge measured
-; and whether a purge ran at all - a portable install into a directory without
-; portable.dat is neither purge block, and the abort below measures it itself.
-Var StaleRuntimeAfterPurge
-Var DllPurgeRan
Var AbortReason
-; Diagnostics for a failed purge: the holder probe in LogProcessesUsingInstallDir writes
-; $HolderOutput (one log line) and sets $HolderFound when it named anything.
-Var HolderExit
-Var HolderOutput
-Var HolderFound
-; What the last RunHolderProbe call answered: its exit code and its line. Kept
-; apart from $HolderExit/$HolderOutput because the guard only wants to know
-; whether the payload is free, while the abort dialog reports what survived.
-Var GuardExit
-Var GuardOutput
-; How many times the guard has asked the payload programs to close by itself. Bounded, and
-; only repeated while the listing changed between rounds (see GuardAppsNotRunning).
-Var GuardCloseRounds
-; The listing the last automatic close was sent against, so a second round is spent only
-; when something actually exited and the rest is worth asking again. Empty before round one.
-Var GuardLastHolders
-; How many times the user asked to check again after closing programs by hand (logged only).
-Var GuardRecheckCount
-; The question CockEndPrompt/CockEndRecheckPrompt assemble: fixed sentences around the
-; probe's line, and the result has to stay inside the characters one NSIS string holds.
-Var GuardPrompt
Var WaitPid
Var LauncherExited
-; How long one WaitForSingleObject on the launcher's handle waits before asking the user
-; (see WaitForLauncherToExit). A variable because the first slice is short and the ones
-; after the first question are not.
-Var WaitSlice
-; Whether that handle carries PROCESS_TERMINATE as well as SYNCHRONIZE, and can therefore
-; be ended from the answer the dialog offers. Cleared when only the synchronize right could
-; be had (see WaitForLauncherToExit).
-Var WaitCanEnd
-; How far the launcher wait got with a launcher that has no window to ask: 0 before the
-; first round, 1 after the grace it was given, 2 after it was ended. A launcher whose
-; window is gone has answered its own close and is stuck in shutdown, so the dialog cannot
-; reach it and the wait may end it instead of asking the user (see WaitForLauncherToExit).
-Var WaitWindowless
Var InstallLogPath
Var LogHandle
Var LogMessage
@@ -147,10 +94,7 @@ Page Custom PortableModePageCreate PortableModePageLeave
Call CockLogWrite
!macroend
-; The body is shared because the uninstaller needs its own copy: un.* functions are
-; compiled into a separate binary, and inside one a Call may only name an un. function,
-; so CockLog's Call to CockLogWrite cannot be used there.
-!macro CockLogWriteBody
+Function CockLogWrite
Pop $LogMessage
FileOpen $LogHandle "$InstallLogPath" a
${If} $LogHandle == ""
@@ -162,20 +106,6 @@ Page Custom PortableModePageCreate PortableModePageLeave
FileSeek $LogHandle 0 END
FileWrite $LogHandle "$LogMessage$\r$\n"
FileClose $LogHandle
-!macroend
-
-Function CockLogWrite
- !insertmacro CockLogWriteBody
-FunctionEnd
-
-; CockLog for un.* code: same lines, prefixed call target.
-!macro CockLogUn text
- Push "${text}"
- Call un.CockLogWrite
-!macroend
-
-Function un.CockLogWrite
- !insertmacro CockLogWriteBody
FunctionEnd
; Logs every file matching $INSTDIR\. Used to record the DLL inventory
@@ -218,11 +148,6 @@ StrCpy $LooseDeckNoticeShown ""
; No old install location resolved yet; Backup/Restore fall back to $INSTDIR
; when this is empty (the /R path where $INSTDIR already is the old dir).
StrCpy $OldInstallDir ""
-StrCpy $OldUninstallExit ""
-; No purge has run and nothing has been measured yet. The section sets both
-; after a purge, or measures the directory itself when neither purge applies.
-StrCpy $StaleRuntimeAfterPurge 0
-StrCpy $DllPurgeRan 0
; Start each run from a clean file so the log that gets sent back is the last
; run, not every past run glued together.
FileOpen $LogHandle "$InstallLogPath" w
@@ -289,10 +214,10 @@ ${EndIf}
; /PID= carries the process id of the application that started this installer.
; The in-app updater launches the installer and only then quits itself, so
; without it the installer has to guess whether the launching process is still
-; alive from the files it still has open - a guess that can come back "nothing
-; is holding them" while the client is very much alive and still has its Qt DLLs
-; mapped. An older client does not pass /PID= and an older installer ignores it,
-; so both sides of the pair have to keep working.
+; alive by polling powershell.exe for an image name - a guess that can come back
+; "not running" or "could not determine" while the process is very much alive and
+; still has its Qt DLLs mapped. An older client does not pass /PID= and an older
+; installer ignores it, so both sides of the pair have to keep working.
; Note that /D= must stay the last parameter on the command line, which is why
; the client puts it last and this one anywhere.
StrCpy $WaitPid ""
@@ -330,16 +255,18 @@ ${Else}
${EndIf}
; Wait for the process that launched this installer to exit before anything is
-; replaced or deleted. It goes first because a real handle on that process is
-; authority: the holder probe that follows asks the same question indirectly, by
-; looking at which files are still open.
+; replaced or deleted. It goes first because it is the one check here that needs
+; no powershell.exe: on a machine where the process lookup cannot run at all, the
+; launcher is still proven gone here instead of the whole upgrade being cancelled
+; by a check that was only ever a proxy for this one.
;
; Then make sure no *other* application instance is still running (and holding
-; file locks) before the previous version is uninstalled. The probe names them
-; and the user is asked to close them; nothing is force-stopped without that
-; answer, because Cockatrice may hold an unsaved deck and Oracle writes the card
-; database in place. On /R the old version is about to be removed, so an answer
-; the probe cannot give cancels the upgrade before it starts rather than after it.
+; file locks) before the previous version is uninstalled. On a silent update
+; (/R /S) the running processes are asked to close gracefully and waited for;
+; Cockatrice itself is never force-killed (data loss, e.g. an unsaved deck) so the
+; update is aborted instead, while the console applications Oracle and Servatrice
+; are force-stopped by PID once the grace period is over. On interactive installs
+; the user is prompted to close them instead.
; Only needed this early for /R, where AutoUninstallIfNeeded runs below; for
; interactive installs the section re-checks after the directory page, when
; $INSTDIR is the directory the user actually picked.
@@ -363,14 +290,6 @@ ${If} ${NSIS_IS_64_BIT} == 1
SetRegView 64
${EndIf}
-; The uninstaller logs to its own file: the installer truncates its log on every run, so
-; sharing one would mean either side erasing the other's account of a failed upgrade.
-StrCpy $InstallLogPath "$TEMP\Cockatrice-uninstaller.log"
-FileOpen $LogHandle "$InstallLogPath" w
-FileClose $LogHandle
-!insertmacro CockLogUn "=================================================================="
-!insertmacro CockLogUn "uninstaller start: @CPACK_PACKAGE_NAME@ @CPACK_PACKAGE_VERSION@ dir=$INSTDIR"
-
; Ensure no application instance is still running before removing files.
Call un.EnsureAppsNotRunning
@@ -401,61 +320,6 @@ ${If} $ReinstallMode = 1
${EndIf}
FunctionEnd
-; Sets $OldUninstallExe to the executable named in $OldUninstallString (everything up to
-; and including the first ".exe", without a leading quote), or "" when there is none.
-Function ResolveOldUninstallExe
- Push $R7
- Push $R8
- StrLen $R8 $OldUninstallString
- StrCpy $R7 0
- roue_loop:
- ${If} $R7 >= $R8
- StrCpy $OldUninstallExe ""
- Goto roue_out
- ${EndIf}
- StrCpy $OldUninstallExe $OldUninstallString 4 $R7
- ${If} $OldUninstallExe == ".exe"
- IntOp $R7 $R7 + 4
- StrCpy $OldUninstallExe $OldUninstallString $R7
- StrCpy $R8 $OldUninstallExe 1
- ${If} $R8 == '"'
- StrCpy $OldUninstallExe $OldUninstallExe "" 1
- ${EndIf}
- Goto roue_out
- ${EndIf}
- IntOp $R7 $R7 + 1
- Goto roue_loop
- roue_out:
- Pop $R8
- Pop $R7
-FunctionEnd
-
-; Runs "$OldUninstallString _?=$OldInstallDir" in place and sets $OldUninstallExit:
-; the uninstaller's own exit code when it ran, "missing" when the executable the
-; registry names does not exist, "launch-failed" when ExecWait could not start it.
-; The last two used to leave the code empty, which WaitForOldPayloadToDisappear read
-; as a cancelled uninstall and aborted on, although nobody had cancelled anything.
-; Uses $R5 for the exit code, as the callers did before.
-Function RunOldUninstaller
- Call ResolveOldUninstallExe
- StrCpy $R5 ""
- ${If} $OldUninstallExe == ""
- ${OrIfNot} ${FileExists} "$OldUninstallExe"
- StrCpy $OldUninstallExit "missing"
- !insertmacro CockLog "old uninstall: uninstaller '$OldUninstallExe' does not exist (command '$OldUninstallString')"
- Return
- ${EndIf}
- ClearErrors
- ExecWait '$OldUninstallString _?=$OldInstallDir' $R5
- ${If} ${Errors}
- ${OrIf} $R5 == ""
- StrCpy $OldUninstallExit "launch-failed"
- !insertmacro CockLog "old uninstall: could not start '$OldUninstallString _?=$OldInstallDir'"
- Return
- ${EndIf}
- StrCpy $OldUninstallExit $R5
-FunctionEnd
-
Function AutoUninstallIfNeeded
SetShellVarContext all
@@ -480,10 +344,7 @@ StrCpy $OldInstallDir $INSTDIR
StrCmp $R0 "" done32
DetailPrint "Removing previous version (32-bit)..."
Call BackupUserDataBeforeUninstall
-StrCpy $OldUninstallString $R0
-Call RunOldUninstaller
-!insertmacro CockLog "uninstall (32-bit): exit=$OldUninstallExit"
-Call WaitForOldPayloadToDisappear
+ExecWait '$R0 _?=$OldInstallDir'
Call RestoreUserDataAfterUninstall
done32:
@@ -498,10 +359,7 @@ ${If} ${RunningX64}
StrCmp $R0 "" done64
DetailPrint "Removing previous version (64-bit)..."
Call BackupUserDataBeforeUninstall
- StrCpy $OldUninstallString $R0
- Call RunOldUninstaller
- !insertmacro CockLog "uninstall (64-bit): exit=$OldUninstallExit"
- Call WaitForOldPayloadToDisappear
+ ExecWait '$R0 _?=$OldInstallDir'
Call RestoreUserDataAfterUninstall
done64:
@@ -509,192 +367,6 @@ ${EndIf}
FunctionEnd
-; Waits for the previous installation's payload to be gone, so the sections below never
-; write into a directory an old uninstaller is still emptying.
-;
-; ExecWait only waits for as long as the uninstaller runs in place: an NSIS uninstaller
-; that did not get a usable _?= copies itself to %TEMP%, starts that copy and exits, so
-; ExecWait returns at once while the copy keeps deleting - and it finishes after this
-; window has closed, removing the version that was just installed. Waiting for
-; cockatrice.exe to disappear covers both cases, and stops with a message instead of
-;
-; When the payload is still there after the wait, $OldUninstallExit decides: a non-zero
-; code means the uninstaller was cancelled or failed, and the install aborts saying so.
-; A zero code means it finished but skipped files someone was holding (NSIS Delete does
-; that silently), so RemoveOldPayloadLeftovers frees and removes them, and the install
-; only aborts - naming the holders - when even that fails.
-; installing over a directory that is still being emptied.
-Function WaitForOldPayloadToDisappear
-${If} $OldInstallDir == ""
- Return
-${EndIf}
-; The registered uninstaller could not be run at all (missing, or ExecWait could not
-; start it): nothing is going to empty the directory, so waiting is pointless and
-; aborting as "cancelled" was wrong. Remove the previous payload the same way the
-; skipped-files recovery below does, and abort - naming why - only if that fails.
-${If} $OldUninstallExit == "missing"
-${OrIf} $OldUninstallExit == "launch-failed"
- ${IfNot} ${FileExists} "$OldInstallDir\*.*"
- !insertmacro CockLog "old uninstall: uninstaller $OldUninstallExit and $OldInstallDir is already gone"
- Return
- ${EndIf}
- !insertmacro CockLog "old uninstall: uninstaller $OldUninstallExit ('$OldUninstallString'); removing $OldInstallDir payload directly"
- Call AppendUninstallerLog
- Call RemoveOldPayloadLeftovers
- ${If} $OldPayloadRemoved != "1"
- StrCpy $AbortReason \
- "The previous version in $OldInstallDir could not be removed: its uninstaller ($OldUninstallExe) could not be run ($OldUninstallExit), and removing the files directly failed.$\r$\n$\r$\nStill in use by: $HolderOutput$\r$\n$\r$\nClose anything still using those files and run this installer again."
- Call RestoreUserDataAfterUninstall
- Call AbortInstall
- ${EndIf}
- !insertmacro CockLog "old uninstall: continuing after removing the payload of an uninstaller that could not run"
- Return
-${EndIf}
-StrCpy $R2 0
-old_uninstall_wait:
-IfFileExists "$OldInstallDir\cockatrice.exe" 0 old_uninstall_gone
-IntOp $R2 $R2 + 1
-${If} $R2 > 60
- !insertmacro CockLog "old uninstall: $OldInstallDir still holds cockatrice.exe after 30 s (uninstaller exit=$OldUninstallExit)"
- ; The uninstaller's own account of what it skipped or why it stopped lives in its
- ; separate log; copy it here so the report sent from AbortInstall carries it.
- Call AppendUninstallerLog
- ${If} $OldUninstallExit != "0"
- ; Cancelled or failed with a non-zero exit code (an uninstaller that could not be
- ; started is handled above): the old version was not meant to be removed, so
- ; nothing is deleted on its behalf.
- StrCpy $AbortReason \
- "The previous version in $OldInstallDir is still there because its uninstaller was cancelled or failed (exit code '$OldUninstallExit').$\r$\n$\r$\nClose anything still using those files and run this installer again."
- ; The backup was taken before the uninstaller ran; an abort must not leave the
- ; only copy of the user data in %TEMP%, where the next run clears it.
- Call RestoreUserDataAfterUninstall
- Call AbortInstall
- ${EndIf}
- ; The uninstaller finished but skipped files - a running instance or a scanner holding
- ; them, which NSIS Delete passes over silently. Free them and remove what is left.
- Call RemoveOldPayloadLeftovers
- ; The removal reports success only when every payload executable is gone and the
- ; DLL/Plugins purge came back clean; cockatrice.exe alone would hide a locked
- ; oracle.exe or a stale DLL the new payload is about to be written over.
- ${If} $OldPayloadRemoved != "1"
- StrCpy $AbortReason \
- "The previous version in $OldInstallDir could not be fully removed after its uninstaller ran.$\r$\n$\r$\nStill in use by: $HolderOutput$\r$\n$\r$\nClose anything still using those files and run this installer again."
- Call RestoreUserDataAfterUninstall
- Call AbortInstall
- ${EndIf}
- ; The payload is gone and ExecWait already waited for the uninstaller; keep the
- ; short settle the other paths use, then hand back to the caller's restore.
- Sleep 1000
- !insertmacro CockLog "old uninstall: continuing after removing the leftovers the uninstaller skipped"
- Return
-${EndIf}
-Sleep 500
-Goto old_uninstall_wait
-old_uninstall_gone:
-${If} $R2 > 0
- ; ExecWait returned while the old payload was still there: that uninstaller
- ; copied itself to %TEMP%, and the copy is still working through its file list.
- ; cockatrice.exe is gone before the rest of it, so let the copy settle before
- ; the sections below start writing files it would otherwise remove.
- Sleep 1000
- !insertmacro CockLog "old uninstall: ExecWait returned early, settled 1 s after $R2 * 500 ms"
-${Else}
- !insertmacro CockLog "old uninstall: $OldInstallDir was already clear when ExecWait returned"
-${EndIf}
-Return
-FunctionEnd
-
-; Called when the old uninstaller exited successfully but left its payload behind, or
-; could not be run at all (missing or not startable) and the payload is still there.
-; Frees the files the same way the guard does (close, then offer to end), deletes what
-; the uninstaller skipped, and on failure records which files survived and who holds
-; them in $HolderOutput, for the abort message. Sets $OldPayloadRemoved to 1 only when
-; every payload executable is gone and the DLL/Plugins purge is clean.
-Function RemoveOldPayloadLeftovers
- !insertmacro CockLog "old uninstall: removing leftovers of $OldInstallDir"
- StrCpy $OldPayloadRemoved 0
- StrCpy $HolderOutput "nothing could be named"
- StrCpy $GuardDir $OldInstallDir
- Call GuardAppsNotRunning
- ; The purge helpers work on $INSTDIR; point them at the old directory for the
- ; duration (on /R the two are the same anyway).
- Push $INSTDIR
- StrCpy $INSTDIR $OldInstallDir
- StrCpy $R2 0
- rop_loop:
- SetFileAttributes "$INSTDIR\cockatrice.exe" NORMAL
- SetFileAttributes "$INSTDIR\oracle.exe" NORMAL
- SetFileAttributes "$INSTDIR\servatrice.exe" NORMAL
- Delete "$INSTDIR\cockatrice.exe"
- Delete "$INSTDIR\oracle.exe"
- Delete "$INSTDIR\servatrice.exe"
- Delete "$INSTDIR\qt.conf"
- Call RetryDllPurge
- ; $R0 is RetryDllPurge's verdict (1 = a DLL or the Plugins directory survived).
- ; Every payload executable has to be gone as well: a locked oracle.exe or
- ; servatrice.exe is just as fatal to the install that follows as cockatrice.exe.
- IfFileExists "$INSTDIR\cockatrice.exe" rop_retry
- IfFileExists "$INSTDIR\oracle.exe" rop_retry
- IfFileExists "$INSTDIR\servatrice.exe" rop_retry
- ${If} $R0 == 0
- Goto rop_done
- ${EndIf}
- rop_retry:
- IntOp $R2 $R2 + 1
- ${If} $R2 < 3
- !insertmacro CockLog "old uninstall: leftovers of $OldInstallDir survived removal attempt $R2, retrying in 2 s"
- Sleep 2000
- Goto rop_loop
- ${EndIf}
- !insertmacro CockLog "old uninstall: leftovers of $OldInstallDir still present after 3 removal attempts"
- !insertmacro CockLogListing "old-leftovers" "*.*"
- ; Per-file Windows codes for the DLLs plus the holder probe's line ($HolderOutput).
- Call LogStaleRuntimeDiagnostics
- Pop $INSTDIR
- Return
- rop_done:
- StrCpy $OldPayloadRemoved 1
- ; Only now that the recovery has succeeded: deleting the old uninstaller on a
- ; failed attempt would leave a directory whose UninstallString points at nothing,
- ; so no later run could remove it. WriteUninstaller recreates it on success anyway.
- SetFileAttributes "$INSTDIR\uninstall.exe" NORMAL
- Delete "$INSTDIR\uninstall.exe"
- !insertmacro CockLog "old uninstall: leftovers of $OldInstallDir removed"
- !insertmacro CockLogListing "old-leftovers-after-removal" "*.*"
- Pop $INSTDIR
-FunctionEnd
-
-; Copies the old uninstaller's log ($TEMP\Cockatrice-uninstaller.log) into this
-; installer's log, so the report AbortInstall sends shows what the uninstaller did.
-; Uninstallers older than this log simply have no file; that is logged too.
-Function AppendUninstallerLog
- ClearErrors
- FileOpen $UninstLogHandle "$TEMP\Cockatrice-uninstaller.log" r
- ${If} ${Errors}
- ${OrIf} $UninstLogHandle == ""
- !insertmacro CockLog "uninstaller log: not found at $TEMP\Cockatrice-uninstaller.log"
- Return
- ${EndIf}
- !insertmacro CockLog "uninstaller log begin ----------------------------------------"
- aul_loop:
- ClearErrors
- FileRead $UninstLogHandle $UninstLogLine
- IfErrors aul_done
- ; Strip the line break FileRead keeps, so CockLog does not double it.
- aul_trim:
- StrCpy $R9 $UninstLogLine 1 -1
- ${If} $R9 == "$\n"
- ${OrIf} $R9 == "$\r"
- StrCpy $UninstLogLine $UninstLogLine -1
- Goto aul_trim
- ${EndIf}
- !insertmacro CockLog "uninstaller: $UninstLogLine"
- Goto aul_loop
- aul_done:
- FileClose $UninstLogHandle
- !insertmacro CockLog "uninstaller log end ------------------------------------------"
-FunctionEnd
-
; BackupUserDataBeforeUninstall / RestoreUserDataAfterUninstall
;
; The uninstallers shipped in 3.1.0/3.1.1 run a blanket "RMDir /r $INSTDIR".
@@ -996,337 +668,222 @@ FunctionEnd
; "The procedure entry point X could not be located in the dynamic link
; library ...Qt6Network.dll" on the next start.
;
-; Both questions this installer has about running processes - "may I replace
-; the payload?" and "what is holding this file I could not delete?" - are
-; answered by tools/installer-holder-probe, a small executable built from this
-; repository and embedded in the installer and the uninstaller alike. It names
-; every process and service that has one of the payload files open or mapped
-; (the Restart Manager, RmRegisterResources/RmGetList) and, separately, every
-; payload program whose image lies in the directory being replaced rather than
-; in a folder below it.
+; On a silent update (/R /S) matching processes are asked to close gracefully
+; (WM_CLOSE) and waited for up to 60 s. Cockatrice is never force-stopped: it
+; may be holding an unsaved deck or showing a "really leave this game?"
+; confirmation, so if it refuses to close the install is aborted - nothing has
+; been replaced yet, so the update is simply deferred. Oracle and Servatrice
+; are console applications without a main window, so CloseMainWindow() does
+; nothing for them and waiting cannot help; they hold no unsaved user data and
+; are force-stopped by PID once the grace period is over. On interactive
+; installs and uninstalls the user is prompted to close them instead.
;
-; The second lookup is not redundant. Restart Manager answers "who has this
-; file open", and a running instance does not have to have one open: its own
-; image and the modules it loaded are mapped into it rather than opened by it,
-; and a mapped image is not reliably reported. That is the state the failed
-; updates this probe exists for were in - every DLL still on disk, an instance
-; running from the directory, and "nothing is holding them any more" as the
-; answer. A guard built on file handles alone therefore misses exactly the
-; instance it exists to find; it would pass, the purge would fail, and the
-; install would end in a warning with no offer to close and try again.
-;
-; Why not enumerate processes from this script: the NSIS stub is a 32-bit
-; process, so a bare "powershell" started from it is the 32-bit Windows
-; PowerShell, whose Get-Process cannot read the path of a 64-bit process. A
-; guard built on that misses exactly the running instance it exists to find.
-; The probe opens each process with PROCESS_QUERY_LIMITED_INFORMATION and calls
-; QueryFullProcessImageNameW, which has no bitness mismatch, and Restart
-; Manager is a system API rather than a view of this process's architecture.
-;
-; The files that count are the payload the purge removes and the installer
-; replaces: the three executables, the top-level DLLs, qt.conf and everything
-; below Plugins. A document, a log or a screenshot the user happens to have
-; open in the install directory is not part of it and cannot block an upgrade;
-; the image lookup matches on the directory a process runs from, so it does not
-; widen that set.
-;
-; Nothing is force-stopped without the user asking for it: Cockatrice may be holding
-; an unsaved deck or showing a "really leave this game?" confirmation, Oracle writes
-; the card database in place and can be killed mid-write, and Servatrice may be serving
-; a game. A close is a request, made automatically because that is what the user started
-; the installer to do: the probe first asks every instance over its local socket, which
-; reaches a hidden window and an instance sitting on its own confirmation, and falls back
-; to WM_CLOSE for an older client. The automatic close is retried while the set of holders
-; changes, so one instance exiting does not leave the next one unasked. When a close is
-; still not answered the user is told which program is in the way and is offered to force
-; close all three, check again after closing them by hand, or abort - the last resort, with
-; what it costs spelled out in the prompt.
+; Processes are matched by image name AND by their executable path living
+; under the guarded directory ($GuardDir, normally $INSTDIR), so unrelated
+; processes that merely share an image name (e.g. the Oracle DB instance
+; "oracle.exe") are never touched.
-; One sentence for every prompt whose Yes ends the whole set, so that answer reads the same
-; wherever the user meets it: this guard before anything is replaced, the uninstaller's, and
-; the recovery prompt after a failed purge.
-!define END_COST "An unsaved deck, a running game or an unwritten setting is lost, Servatrice drops its players, and an Oracle that is mid-write can damage the card database."
-
-; What the user does instead when they say no: the prompt's other answer stops the run, so they
-; are told how to get past the same wall next time. Also used by the abort dialogs, which is
-; where the answer to "why did it stop" ends up.
-!define AFTER_CLOSING_GUIDANCE "Answer the confirmation Cockatrice may be showing - open it from the notification area if it is hidden there - and use Task Manager for a program that would not end."
-
-; The escape hatch for a user who would rather not force-close anything: the same release,
-; downloaded and run by hand once Cockatrice is closed. Every abort dialog shows this and
-; offers to open the page, so an update that cannot free the files still leaves the user a
-; way forward that does not end in Task Manager.
-!define LATEST_RELEASE_URL "https://github.com/Cockatrice/Cockatrice/releases/latest"
-!define MANUAL_UPDATE_GUIDANCE "To update safely instead, close Cockatrice and download the latest installer from:$\r$\n${LATEST_RELEASE_URL}$\r$\nThen run it manually."
-
-; usage: set the variable named by the second argument to the probe's listing, then call
-; this. Sets $GuardPrompt to the question the guard and the recovery path ask.
-;
-; One macro rather than three copies, because the wording is a promise: Yes ends the
-; programs the payload installs - which is exactly the set EndHolders acts on - and anything
-; else the probe named has to be closed by hand, so a listing that includes a scanner or a
-; service hosted elsewhere does not come with an answer that cannot do what it says.
-;
-; Two answers, one of each: the automatic close already ran when this is asked, so Yes is the
-; only thing left that can free the files and No stops the run. The guard itself uses
-; CockEndRecheckPrompt instead, whose No checks again; this one stays for the recovery path,
-; where a retry cannot change anything because the purge was already retried.
-;
-; The assembled string has to fit the characters one NSIS string holds, which is what
-; bounds HOLDER_LINE_CHAR_MAX in tools\installer-holder-probe\holder_line.h: with the
-; longest listing that can arrive it stays inside that budget instead of being cut off at
-; the tail, where the cancel line is.
-;
-; @param header the sentence that introduces the listing, with its trailing colon
-; @param listing the variable holding the probe's line, $GuardOutput or $HolderOutput
-; @param cancelWord what No gives up, "installation" or "uninstallation"
-!macro CockEndPrompt header listing cancelWord
- StrCpy $GuardPrompt "${header}$\r$\n$\r$\n${listing}"
- StrCpy $GuardPrompt "$GuardPrompt$\r$\n$\r$\nChoose Yes to force close the Cockatrice, Oracle and Servatrice programs listed above. ${END_COST}$\r$\n$\r$\nChoose No to cancel the ${cancelWord}. ${AFTER_CLOSING_GUIDANCE}"
-!macroend
-
-; usage: like CockEndPrompt, for a MB_YESNOCANCEL question whose No checks again.
-;
-; The automatic close has already run, so the three answers are what is left: force close
-; the programs the payload installs (Yes), close them by hand and check again (No), or stop
-; the run (Cancel). No used to be the only alternative to ending them, which meant a user who
-; would rather close the instance themselves had to abandon the update and start the
-; installer again. The listing the question is asked with is refreshed on every pass, so the
-; question changes as programs go away instead of repeating itself word for word.
-;
-; The manual-update guidance lives in the abort dialog, which is where a Cancel ends up, not
-; in this prompt. With the longest listing HOLDER_LINE_CHAR_MAX allows, the assembled string
-; stays under 900 characters, inside the 1024 one NSIS string holds, so the Cancel line is
-; never the part that gets cut off.
-;
-; @param header the sentence that introduces the listing, with its trailing colon
-; @param listing the variable holding the probe's line, $GuardOutput
-; @param cancelWord what Cancel gives up, "installation" or "uninstallation"
-!macro CockEndRecheckPrompt header listing cancelWord
- StrCpy $GuardPrompt "${header}$\r$\n$\r$\n${listing}"
- StrCpy $GuardPrompt "$GuardPrompt$\r$\n$\r$\nChoose Yes to force close the Cockatrice, Oracle and Servatrice programs listed above. ${END_COST}$\r$\n$\r$\nChoose No to close them yourself, then check again - answer the confirmation Cockatrice may be showing, and open it from the notification area if it is hidden there.$\r$\n$\r$\nChoose Cancel to abort the ${cancelWord}."
-!macroend
-
-; Emitted once for the installer and once for the uninstaller: the uninstaller
-; cannot call installer functions, and its own copy of the probe keeps a running
-; 64-bit instance from being missed there too.
-!macro CockHolderProbeFunctions prefix tag
-Function ${prefix}ExtractHolderProbe
- InitPluginsDir
- IfFileExists "$PLUGINSDIR\installer-holder-probe.exe" ck_probe_ready_${tag}
- File "/oname=$PLUGINSDIR\installer-holder-probe.exe" "..\..\..\installer-holder-probe.exe"
- ck_probe_ready_${tag}:
-FunctionEnd
-
-; usage: set $GuardDir to the directory to check, then call this. Sets
-; $GuardExit to the probe's exit code and $GuardOutput to its one-line answer.
-; 0 = nothing holds the payload and no payload program runs from the directory, 1 = the
-; line names what does, anything else = the probe could not answer (2 = no
-; directory was given, 3 = no payload program was named but one refuses to be replaced, or
-; the Restart Manager call failed and no payload program runs from the directory either,
-; "timeout" = it did not return in time).
-Function ${prefix}RunHolderProbe
- Call ${prefix}ExtractHolderProbe
- ; The directory travels in an environment variable, never on the command
- ; line: a path with spaces, quotes or a trailing backslash then does not
- ; have to survive NSIS quoting.
- System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")'
- ; /TIMEOUT because this can run before the first page: a Restart Manager
- ; that does not answer must not leave the installer with no window and no
- ; way out.
- nsExec::ExecToStack /TIMEOUT=20000 '"$PLUGINSDIR\installer-holder-probe.exe"'
- Pop $GuardExit
- Pop $GuardOutput
-FunctionEnd
-
-; usage: set $GuardDir to the directory being replaced, then call this. Asks the running
-; Cockatrice to close, and waits for it to exit.
-;
-; The set is what the probe names: the processes holding a file of the version being
-; replaced, plus the payload programs running from the directory itself. The probe
-; closes that set by pid, which is why nothing here has to match an executable path. Only
-; cockatrice.exe is asked: Oracle writes the card database in place and Servatrice has no
-; window to ask, so both are left alone here and only ended by the explicit answer.
-;
-; Each instance is asked to close over the local socket it listens on, which is the same
-; request as clicking its close button and reaches one whose window is hidden in the
-; notification area or is sitting on its own confirmation. A client from before that socket
-; is asked with WM_CLOSE on its own window instead, and a visible window is brought forward
-; first. Either way the close is answered, not obeyed - Cockatrice asks before it quits while
-; a card database update is running, a game is open or a deck is unsaved - and that question
-; is only useful if it is in front of the installer instead of behind it.
-;
-; The probe waits up to 30 s for the processes it asked to exit, so the check that follows
-; measures a settled directory rather than sleeping for a fixed time. Its line goes into the
-; log as it stands: it names every holder, including the ones left to the user, so a close
-; that did nothing can be told apart from one that was never sent.
-;
-; Never a force-close here: the application decides whether to quit. Ending is a separate
-; answer the guard prompt offers when a close did not free the files, spelled out with what
-; it costs - see EndHolders - and it ends every program the payload installs.
-Function ${prefix}CloseHolders
- Call ${prefix}ExtractHolderProbe
- System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")'
- ; The probe waits 30 s itself. The timeout here only bounds a probe that never returns;
- ; the Restart Manager call inside it is the same one the holder listing makes.
- nsExec::ExecToStack /TIMEOUT=40000 '"$PLUGINSDIR\installer-holder-probe.exe" --close-holders'
- Pop $R3
- Pop $R4
- ; Not the CockLog macro: this function exists twice and the uninstaller copy may only
- ; Call un. functions, so the macro's two lines are spelled with the prefix here.
- Push "close-holders: probe exit=$R3 [$R4]"
- Call ${prefix}CockLogWrite
- DetailPrint "Closing Cockatrice: $R4"
-FunctionEnd
-
-; usage: set $GuardDir to the directory being replaced, then call this. Ends the running
-; Cockatrice, Oracle and Servatrice instances the probe names.
-;
-; This is the only place anything is stopped without being asked to stop, so it is only
-; reached after the user picked it from a prompt that says what it costs: a client that is
-; still running may have an unsaved deck, a game or a settings write in flight, Oracle may
-; be part-way through writing the card database, and Servatrice may be serving a game. The
-; probe ends the three programs the payload installs, services included, and nothing else:
-; a scanner or a program the user keeps in the directory is named and left to the user.
-Function ${prefix}EndHolders
- Call ${prefix}ExtractHolderProbe
- System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")'
- ; The probe waits up to 5 s for what it ended to be gone, so the check that follows
- ; measures a settled directory instead of racing the processes it just terminated.
- nsExec::ExecToStack /TIMEOUT=20000 '"$PLUGINSDIR\installer-holder-probe.exe" --end-holders'
- Pop $R3
- Pop $R4
- Push "end-holders: probe exit=$R3 [$R4]"
- Call ${prefix}CockLogWrite
- DetailPrint "Ending Cockatrice, Oracle and Servatrice: $R4"
-FunctionEnd
-!macroend
-
-!insertmacro CockHolderProbeFunctions "" install
-!insertmacro CockHolderProbeFunctions "un." uninstall
-
-; usage: set $GuardDir to the directory to guard, then call this.
-;
-; Asks Cockatrice to close once, then asks one question with one answer that can free the
-; payload. The automatic close is the same request clicking the window's close button makes,
-; so the common case - the client was still shutting down - never turns into a question. The
-; prompt is what remains when the close cannot reach everything: Oracle writes the card
-; database in place, Servatrice has no window, and a client that is sitting on its own
-; confirmation needs an answer nobody can give it from here. It offers to end the programs
-; the payload installs - and only those, which the text says - with what that costs (Yes),
-; and its other answer stops the install (No) instead of repeating the same dialog. What
-; survives an end is reported in the abort, never asked about again - ending is the last
-; resort, see EndHolders.
-;
-; An error is always shown, in every mode. A /S install without /R has nobody at
-; the keyboard, but this installer does not promise unattended runs: when an
-; error needs a decision, asking for one is better than silently leaving a
-; half-updated install behind. The happy path has no dialog, so a /S reinstall
-; that has nothing to report stays silent.
-Function GuardAppsNotRunning
- !insertmacro CockLog "guard: checking $GuardDir (launcherExited=$LauncherExited)"
- StrCpy $GuardCloseRounds 0
- StrCpy $GuardRecheckCount 0
- StrCpy $GuardLastHolders ""
- guard_check:
- Call RunHolderProbe
- ${If} $GuardExit == "0"
- !insertmacro CockLog "guard: nothing is holding the payload"
- DetailPrint "No running Cockatrice, Oracle or Servatrice instance was found."
+; usage: set $R2 to the base image name (without extension, e.g. "cockatrice",
+; as accepted by Get-Process -Name), call this, result in $R0
+; (1 = running from $GuardDir, 0 = not running, 2 = could not determine)
+Function IsAppRunning
+ ; Every caller has to set $GuardDir; an empty one would make the path check
+ ; below match every process that merely shares the image name, which is the
+ ; opposite of what this guard is for.
+ ${If} $GuardDir == ""
+ StrCpy $R0 2
Return
${EndIf}
- ${If} $GuardExit == "1"
- !insertmacro CockLog "guard: still in use by $GuardOutput"
- ; Close it for the user first. The probe asks every instance over its local socket
- ; and falls back to WM_CLOSE for an older client, so this is the same request as
- ; clicking the window's close button: the application still answers its own "really
- ; quit?" prompt, so nothing is lost. A second round is spent only when the listing
- ; changed, which means something exited and the rest is worth asking again; repeating
- ; the same request against the same processes cannot change anything.
- ${If} $GuardCloseRounds < 2
- ${AndIf} $GuardOutput != $GuardLastHolders
- StrCpy $GuardLastHolders $GuardOutput
- IntOp $GuardCloseRounds $GuardCloseRounds + 1
- !insertmacro CockLog "guard: asking the payload programs to close (round $GuardCloseRounds)"
- Call CloseHolders
- Goto guard_check
- ${EndIf}
- DetailPrint "The install directory is in use by: $GuardOutput"
- ; The close ran and did not free the files. Force-closing them is what the installer
- ; can do (Yes); closing them by hand is what the user can do (No checks again, with
- ; the fresh listing); Cancel stops the run and the abort dialog offers the same
- ; update from the official download page. Without the middle answer a user who did
- ; not want anything killed had no way forward but to abandon the update.
- guard_ask:
- !insertmacro CockEndRecheckPrompt "Cockatrice, Oracle or Servatrice is still using files of the installation:" $GuardOutput "installation"
- MessageBox MB_YESNOCANCEL|MB_ICONEXCLAMATION|MB_DEFBUTTON2 "$GuardPrompt" IDYES guard_end IDNO guard_recheck
- Goto guard_cancel
- guard_recheck:
- IntOp $GuardRecheckCount $GuardRecheckCount + 1
- !insertmacro CockLog "guard: user closed programs by hand, checking again (recheck $GuardRecheckCount)"
- ; A fresh set gets a fresh automatic close, exactly as if the guard had just started:
- ; what the user closed by hand may have been one instance of several.
- StrCpy $GuardCloseRounds 0
- StrCpy $GuardLastHolders ""
- Goto guard_check
- guard_cancel:
- !insertmacro CockLog "guard: cancelled by user after $GuardRecheckCount recheck(s)"
- StrCpy $AbortReason \
- "The installation was cancelled because Cockatrice, Oracle or Servatrice is still running and holding files of the previous version.$\r$\n$\r$\n${AFTER_CLOSING_GUIDANCE}$\r$\n$\r$\nClose them and run the installer again."
- ; RemoveOldPayloadLeftovers runs with the pre-uninstall backup already taken;
- ; restoring here keeps this abort from stranding the only copy in %TEMP%.
- Call RestoreUserDataAfterUninstall
- Call AbortInstall
- ; Only reached through Yes: AbortInstall does not return.
- guard_end:
- !insertmacro CockLog "guard: ending the payload programs on request, then checking again"
- Call EndHolders
- ; Measure once and report, rather than asking the same question again. Everything the
- ; payload installs was ended, so what survived is a program this installer may not
- ; open or does not own - another dialog offering to end it again would only repeat
- ; itself.
- Call RunHolderProbe
- ${If} $GuardExit == "0"
- !insertmacro CockLog "guard: nothing is holding the payload"
+ ; Pass the directory to guard to PowerShell via an environment variable so
+ ; it never has to be embedded into the -Command string: apostrophes in a
+ ; path would terminate the command early, and the path in a -like pattern
+ ; would treat [ and ] as wildcard characters.
+ System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")'
+ ; Exit the process explicitly so nsExec's pop reflects whether a matching
+ ; process was found: powershell.exe exits 0 whether or not the pipeline
+ ; produced objects. 0 = not running, 1 = running, anything else = could not
+ ; be determined (see IsProcessLookupAvailable).
+ ; A process is only a match when its path is readable and lives under
+ ; $GuardDir. A process we cannot inspect is not one of ours: the updater is
+ ; always started by the application it updates, so it runs in the same user
+ ; session, and an interactive install is elevated and can read the paths of
+ ; the user's own processes. Assuming a match instead would be a hard stop
+ ; rather than a fail-closed guard - an Oracle database instance running as
+ ; SYSTEM has a process named "oracle" whose path raises Access denied, and
+ ; that would make every silent update abort and every interactive install
+ ; loop on the Retry prompt with no way out but Cancel.
+ nsExec::ExecToLog 'powershell -NoProfile -Command "if (Get-Process -Name $\'$R2$\' -ErrorAction SilentlyContinue | Where-Object { try { $$p = $$_.Path; if ($$p) { $$p.StartsWith($$env:COCKATRICE_GUARD_DIR, [StringComparison]::OrdinalIgnoreCase) } else { $$false } } catch { $$false } } | Select-Object -First 1) { exit 1 } else { exit 0 }"'
+ Pop $R0
+ ${If} $R0 == "0"
+ StrCpy $R0 0
+ ${ElseIf} $R0 == "1"
+ StrCpy $R0 1
+ ${Else}
+ ; powershell.exe could not be run at all (nsExec pushes the string
+ ; "error"), or died with an unexpected code. That is "could not
+ ; determine", which has to stay distinguishable from "definitely
+ ; running": a state that can never clear would block the installer
+ ; forever. IsProcessLookupAvailable warns about that case once.
+ StrCpy $R0 2
+ ${EndIf}
+FunctionEnd
+
+; Sets $R0 to 1 if PowerShell can be started at all, 0 if it cannot. The
+; running instance check is built on top of it, so a machine where
+; powershell.exe is missing, blocked by policy or refused by antivirus is
+; reported once instead of being reported as "Cockatrice is running" on every
+; single check.
+Function IsProcessLookupAvailable
+ nsExec::ExecToLog 'powershell -NoProfile -Command "exit 0"'
+ Pop $R0
+ ${If} $R0 == "0"
+ StrCpy $R0 1
+ ${Else}
+ StrCpy $R0 0
+ ${EndIf}
+FunctionEnd
+
+Function CloseMatchingApps
+ ; gracefully ask every matching instance to close (sends WM_CLOSE)
+ ;
+ ; The filter here is deliberately the strict one: a process is only asked to
+ ; close when its path is readable and lives under $GuardDir. Treating a
+ ; process whose path cannot be read (an unrelated oracle.exe / servatrice.exe,
+ ; or one owned by another account running as SYSTEM) as a match would make
+ ; the installer close a third party application, and it runs elevated, so
+ ; CloseMainWindow() would succeed on it.
+ System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")'
+ nsExec::ExecToLog 'powershell -NoProfile -Command "Get-Process -Name $\'$R2$\' -ErrorAction SilentlyContinue | Where-Object { try { $$p = $$_.Path; if ($$p) { $$p.StartsWith($$env:COCKATRICE_GUARD_DIR, [StringComparison]::OrdinalIgnoreCase) } else { $$false } } catch { $$false } } | ForEach-Object { $$null = $$_.CloseMainWindow() }"'
+ Pop $R3
+FunctionEnd
+
+Function ForceCloseApps
+ ; stop the matching instances by PID once the grace period is over. Only
+ ; used for the console applications ($AllowForceClose), which have no window
+ ; to close and hold no unsaved user data. Same strict filter as above.
+ System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")'
+ nsExec::ExecToLog 'powershell -NoProfile -Command "Get-Process -Name $\'$R2$\' -ErrorAction SilentlyContinue | Where-Object { try { $$p = $$_.Path; if ($$p) { $$p.StartsWith($$env:COCKATRICE_GUARD_DIR, [StringComparison]::OrdinalIgnoreCase) } else { $$false } } catch { $$false } } | ForEach-Object { Stop-Process -Id $$_.Id -Force -ErrorAction SilentlyContinue }"'
+ Pop $R3
+FunctionEnd
+
+Function WaitForAppToClose
+ ; usage: set $R1 to the display name (e.g. "cockatrice.exe") and $R2 to the
+ ; base image name (e.g. "cockatrice")
+ Call IsAppRunning
+ !insertmacro CockLog "guard $R1: state=$R0 (dir=$GuardDir)"
+ ; Only a definite "running" is worth waiting for. "Not running" is done, and
+ ; "could not determine" must not keep the installer waiting on an answer it
+ ; will never get - but it cannot simply be waved through either, see
+ ; HandleUnknownAppState.
+ ${If} $R0 = 0
+ Return
+ ${EndIf}
+ ${If} $R0 = 2
+ Call HandleUnknownAppState
+ Return
+ ${EndIf}
+
+ ${If} ${Silent}
+ ; ask the application to close gracefully (WM_CLOSE), then wait for it to exit
+ DetailPrint "Closing $R1 ..."
+ Call CloseMatchingApps
+ ; wall-clock deadline: each poll has to spawn a fresh powershell.exe, so
+ ; a bare loop counter would not bound the real wait time to 60 s.
+ System::Call 'kernel32::GetTickCount()i.s'
+ Pop $R8
+ ck_wait_loop:
+ Sleep 500
+ Call IsAppRunning
+ ${If} $R0 = 0
+ !insertmacro CockLog "guard $R1: closed gracefully"
+ DetailPrint "$R1 closed."
Return
${EndIf}
- !insertmacro CockLog "guard: still in use after ending them ($GuardExit): $GuardOutput"
- ${If} $GuardExit == "1"
- StrCpy $AbortReason \
- "The installation was cancelled because these programs still hold files of the previous version and could not be ended:$\r$\n$\r$\n$GuardOutput$\r$\n$\r$\n${AFTER_CLOSING_GUIDANCE}$\r$\n$\r$\nClose them and run the installer again."
- ${Else}
- StrCpy $AbortReason \
- "The installation was cancelled because, after ending Cockatrice, Oracle and Servatrice, the installer could not check whether they still hold files of the previous version.$\r$\n$\r$\nThe check reported: $GuardOutput$\r$\n$\r$\nClose them and run the installer again."
+ ${If} $R0 = 2
+ Call HandleUnknownAppState
+ Return
${EndIf}
- Call RestoreUserDataAfterUninstall
+ System::Call 'kernel32::GetTickCount()i.s'
+ Pop $R9
+ IntOp $R9 $R9 - $R8
+ ${If} $R9 < 60000
+ Goto ck_wait_loop
+ ${EndIf}
+ ; The grace period is over. Cockatrice is never force-stopped: it may be
+ ; showing a "really leave this game?" confirmation or holding an unsaved
+ ; deck, and nothing has been replaced yet, so aborting just defers the
+ ; update and preserves the user's data.
+ ${If} $AllowForceClose = 0
+ !insertmacro CockLog "guard $R1: still running after 60s, aborting (no force close)"
+ DetailPrint "$R1 is still running after 60 s; aborting the update so that no data is lost."
+ StrCpy $AbortReason \
+ "$R1 did not close within 60 seconds, so the installation was aborted to avoid losing unsaved data.$\r$\n$\r$\nPlease close $R1 and run the installer again."
+ Call AbortInstall
+ ${EndIf}
+ ; Oracle and Servatrice are console applications: CloseMainWindow()
+ ; returned false for them and waiting longer cannot help. They hold no
+ ; unsaved user data, so stop them by PID.
+ !insertmacro CockLog "guard $R1: still running after 60s, force-stopping"
+ DetailPrint "Force-stopping $R1 ..."
+ Call ForceCloseApps
+ Sleep 2000
+ Call IsAppRunning
+ ${If} $R0 = 0
+ !insertmacro CockLog "guard $R1: force-stopped"
+ DetailPrint "$R1 closed."
+ Return
+ ${EndIf}
+ ${If} $R0 = 2
+ Call HandleUnknownAppState
+ Return
+ ${EndIf}
+ !insertmacro CockLog "guard $R1: could not be stopped, aborting"
+ DetailPrint "$R1 is still running after 60 s; aborting the update."
+ StrCpy $AbortReason \
+ "$R1 could not be stopped, so the installation was aborted.$\r$\n$\r$\nPlease close $R1 and run the installer again."
+ Call AbortInstall
+ ${Else}
+ ck_wait_prompt:
+ MessageBox MB_RETRYCANCEL|MB_ICONEXCLAMATION|MB_DEFBUTTON1 \
+ "$R1 is still running.$\r$\n$\r$\nPlease close it, then click Retry.$\r$\nClick Cancel to abort." \
+ IDCANCEL ck_abort_install
+ Call IsAppRunning
+ ${If} $R0 = 0
+ Return
+ ${EndIf}
+ ${If} $R0 = 2
+ Call HandleUnknownAppState
+ Return
+ ${EndIf}
+ Goto ck_wait_prompt
+ ck_abort_install:
+ Abort
+ ${EndIf}
+FunctionEnd
+
+; A process lookup came back "could not determine": powershell.exe ran but the
+; answer was not usable, e.g. the process could not be inspected at all. That
+; state is fine to carry on from when the user is at the keyboard - they can
+; close the applications by hand and the stale-runtime check still stands
+; between a failed guard and a mismatched install.
+;
+; It is not fine on a silent upgrade. There the whole point of this guard is to
+; know that nothing holds locks on the files about to be replaced, and this is
+; exactly the state where that is unknown, with no one to click a dialog: the
+; old code returned and let the install carry on into the mixed Qt DLL set that
+; made the next start fail. Cancel the update instead; the next attempt runs
+; with the applications closed and succeeds.
+Function HandleUnknownAppState
+ ${If} $ReinstallMode = 1
+ StrCpy $AbortReason \
+ "The installation was cancelled because it could not determine whether Cockatrice is still running.$\r$\n$\r$\nPlease close Cockatrice, Oracle and Servatrice and run the installer again."
Call AbortInstall
${EndIf}
- ; Not an answer at all: the probe was not configured, the Restart Manager
- ; call failed and no payload program runs from the directory, a payload
- ; program refuses to be replaced but no lookup could name it (it may run
- ; under another account), or the probe timed out. Carrying on from that is a
- ; decision, so it is asked for rather than assumed - the stale-runtime check
- ; after the purge still stands between a failed guard and a mismatched
- ; install, but it only fires after files have been replaced, and on an
- ; upgrade the launcher asked for nobody is watching the installer's exit
- ; code. Retry asks the probe again (a Restart Manager that was busy answers
- ; the second time, and a program the user has closed frees its file), Ignore
- ; installs anyway, Abort cancels instead of writing a payload next to files
- ; the guard could not rule out.
- !insertmacro CockLog "guard: the holder probe could not answer ($GuardExit): $GuardOutput"
- DetailPrint "Could not confirm whether Cockatrice is still running: $GuardOutput"
- MessageBox MB_ABORTRETRYIGNORE|MB_ICONEXCLAMATION|MB_DEFBUTTON1 \
- "This installer could not confirm whether Cockatrice, Oracle or Servatrice is still running.$\r$\n$\r$\nThe check reported: $GuardOutput$\r$\n$\r$\nClose them, then choose Retry to check again. Choose Ignore to install anyway, or Abort to cancel the installation." \
- IDRETRY guard_check IDABORT guard_unknown_abort
- !insertmacro CockLog "guard: continuing without an answer from the probe"
- Return
- guard_unknown_abort:
- StrCpy $AbortReason \
- "The installation was cancelled because it could not confirm whether Cockatrice, Oracle or Servatrice is still running.$\r$\n$\r$\nThe check reported: $GuardOutput$\r$\n$\r$\nPlease close them and run the installer again."
- ; Same reason as the cancel path above: this abort can be reached from the
- ; leftover-removal guard, after the backup was taken.
- Call RestoreUserDataAfterUninstall
- Call AbortInstall
+ DetailPrint "Could not determine whether $R1 is running; close Cockatrice, Oracle and Servatrice manually if the installation reports a problem."
+ ${IfNot} ${Silent}
+ MessageBox MB_OK|MB_ICONEXCLAMATION \
+ "This installer could not determine whether $R1 is still running.$\r$\n$\r$\nPlease close Cockatrice, Oracle and Servatrice manually before continuing."
+ ${EndIf}
FunctionEnd
Function EnsureAppsNotRunning
@@ -1337,155 +894,103 @@ FunctionEnd
; Aborts the installation, showing $AbortReason to the user.
;
-; Every abort goes through here so no failure ends silently: the in-app updater
-; runs this installer silent and detached and never looks at its exit code, and
-; a /S install without /R has nobody watching its exit code either, so a plain
-; Abort would leave the user with no update and no explanation. An error always
-; shows its dialog, in every mode; a silent run stays silent only for as long
-; as nothing goes wrong. The log path is part of the dialog, so a report can be
-; acted on without asking the user to find the file.
+; Every abort goes through here so the unattended case is handled the same way
+; everywhere: the in-app updater runs this installer silent and detached and
+; never looks at its exit code, so a plain Abort would leave the user with no
+; update and no explanation, while a /S install without /R is unattended - by a
+; package manager, for instance - and a message box there would block forever.
+; Only the first of those two gets a dialog; the second reports through the log
+; and the exit code.
Function AbortInstall
!insertmacro CockLog "ABORT: $AbortReason"
!insertmacro CockLog "full installer log: $InstallLogPath"
- !insertmacro CockLog "manual update: ${LATEST_RELEASE_URL}"
- ; The reason names what stopped the run; the manual-update paragraph is the way forward
- ; that does not need this installer to free anything. Shown in every mode, exactly like
- ; the reason, because a /R upgrade has nobody watching its exit code.
- MessageBox MB_OK|MB_ICONEXCLAMATION "$AbortReason$\r$\n$\r$\n${MANUAL_UPDATE_GUIDANCE}$\r$\n$\r$\nA detailed installer log was written to:$\r$\n$InstallLogPath"
- ; Offer the page as well: copying a URL out of a message box is a step users skip.
- ; explorer.exe hands the URL to the already-running shell, so the browser opens at the
- ; user's integrity level rather than as the elevated installer.
- MessageBox MB_YESNO|MB_ICONQUESTION|MB_DEFBUTTON2 "Open the download page now?$\r$\n$\r$\n${LATEST_RELEASE_URL}" IDNO abort_no_link
- !insertmacro CockLog "manual update: opening the download page"
- Exec '"$WINDIR\explorer.exe" "${LATEST_RELEASE_URL}"'
- abort_no_link:
- ; Offer to send the installer log to Sentry so aborted installs can be diagnosed.
- ; Only a package built with a baked DSN offers to send, so the prompt can only appear
- ; when a send can actually go out. A preprocessor guard rather than a runtime test: a
- ; package built without a DSN compiles the block out entirely, so it cannot even carry
- ; the helper or its baked endpoint. No /SD: the abort is exactly the case the offer
- ; exists for, including on the silent /R upgrade.
- !if "@CPACK_SENTRY_ENABLED@" == "1"
- MessageBox MB_YESNO|MB_ICONQUESTION "Send the installer log to the developers to help fix this? The log contains file paths from this machine, which may include your Windows user name." IDYES sentry_send
- !insertmacro CockLog "sentry send: declined by user"
- Goto sentry_done
- sentry_send:
- ; The abort reason is already in the log file (CockLog writes "ABORT: ..." as the
- ; first thing AbortInstall does), so the helper reads it from there. Nothing
- ; reason-shaped crosses the command line, which keeps quoting trivial and needs
- ; no NSIS string functions.
- InitPluginsDir
- ; Embedded by the same relative path that resolves NSIS.definitions.nsh above (both are
- ; configure_file'd into the build root, which is three levels up from project.nsi). An
- ; absolute CMake path here carries forward slashes, which makensis does not treat as
- ; absolute and so fails to find the file at package time.
- File "/oname=$PLUGINSDIR\send-installer-log.ps1" "..\..\..\SendInstallerLog.ps1"
- ; The send can take up to the /TIMEOUT below, so put something on the details pane
- ; the moment the user says yes rather than leaving the window blank until it returns.
- DetailPrint "Sending installer log..."
- nsExec::ExecToStack /TIMEOUT=60000 'powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "$PLUGINSDIR\send-installer-log.ps1" -LogPath "$InstallLogPath"'
- Pop $SentryResult
- Pop $SentryOutput
- ; $SentryOutput is a single line by construction: the helper flattens its message
- ; and this log file is uploaded verbatim, so a stray newline must not be able to
- ; forge extra lines into the artifact we send.
- !insertmacro CockLog "sentry send: result=$SentryResult output=$SentryOutput"
- ${If} $SentryResult == "0"
- MessageBox MB_OK|MB_ICONINFORMATION "The installer log was sent. Thank you for helping improve Cockatrice!"
- ${Else}
- MessageBox MB_OK|MB_ICONEXCLAMATION "The installer log could not be sent. You can still share it manually; it was written to:$\r$\n$InstallLogPath"
- ${EndIf}
- sentry_done:
- !endif
+ ; Show a dialog whenever someone is at the keyboard to see it: an interactive
+ ; install, or the in-app updater's /R upgrade. The /R upgrade is silent, but
+ ; the client that launched the installer has already quit, so the user is
+ ; still there. Only a /S install without /R is attended by a package manager
+ ; and must go straight to the exit code.
+ ${If} $ReinstallMode = 1
+ ${OrIfNot} ${Silent}
+ MessageBox MB_OK|MB_ICONEXCLAMATION "$AbortReason$\r$\n$\r$\nA detailed installer log was written to:$\r$\n$InstallLogPath"
+ ; Offer to send the installer log to Sentry so aborted installs can be diagnosed. This block already runs
+ ; only when someone is at the keyboard (attended install, or the updater's /R upgrade whose launcher has
+ ; quit), so a silent or package-manager install never reaches the prompt and its behavior is unchanged.
+ ; Only a package built with a baked DSN offers to send, so the prompt can only appear when a send
+ ; can actually go out. A preprocessor guard rather than a runtime test: a package built without a
+ ; DSN compiles the block out entirely, so it cannot even carry the helper or its baked endpoint.
+ ; No /SD here: a /SD would suppress this prompt on the silent /R upgrade, which is exactly the
+ ; attended-update case this offer exists for. The runtime gate above (ReinstallMode/Silent) is the
+ ; hang protection — a package-manager /S install without /R never reaches this box, matching the
+ ; two sibling MessageBox sites in this file.
+ !if "@CPACK_SENTRY_ENABLED@" == "1"
+ MessageBox MB_YESNO|MB_ICONQUESTION "Send the installer log to the developers to help fix this? The log contains file paths from this machine, which may include your Windows user name." IDYES sentry_send
+ !insertmacro CockLog "sentry send: declined by user"
+ Goto sentry_done
+ sentry_send:
+ ; The abort reason is already in the log file (CockLog writes "ABORT: ..." as the
+ ; first thing AbortInstall does), so the helper reads it from there. Nothing
+ ; reason-shaped crosses the command line, which keeps quoting trivial and needs
+ ; no NSIS string functions.
+ InitPluginsDir
+ ; Embedded by the same relative path that resolves NSIS.definitions.nsh above (both are
+ ; configure_file'd into the build root, which is three levels up from project.nsi). An
+ ; absolute CMake path here carries forward slashes, which makensis does not treat as
+ ; absolute and so fails to find the file at package time.
+ File "/oname=$PLUGINSDIR\send-installer-log.ps1" "..\..\..\SendInstallerLog.ps1"
+ ; The send can take up to the /TIMEOUT below, so put something on the details pane
+ ; the moment the user says yes rather than leaving the window blank until it returns.
+ DetailPrint "Sending installer log..."
+ nsExec::ExecToStack /TIMEOUT=60000 'powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "$PLUGINSDIR\send-installer-log.ps1" -LogPath "$InstallLogPath"'
+ Pop $SentryResult
+ Pop $SentryOutput
+ ; $SentryOutput is a single line by construction: the helper flattens its message
+ ; and this log file is uploaded verbatim, so a stray newline must not be able to
+ ; forge extra lines into the artifact we send.
+ !insertmacro CockLog "sentry send: result=$SentryResult output=$SentryOutput"
+ ${If} $SentryResult == "0"
+ MessageBox MB_OK|MB_ICONINFORMATION "The installer log was sent. Thank you for helping improve Cockatrice!"
+ ${Else}
+ MessageBox MB_OK|MB_ICONEXCLAMATION "The installer log could not be sent. You can still share it manually; it was written to:$\r$\n$InstallLogPath"
+ ${EndIf}
+ sentry_done:
+ !endif
+ ${EndIf}
SetErrorLevel 1
Abort
FunctionEnd
-; usage: set $GuardDir and $WaitPid, then call this. Asks the one process the installer
-; waits on to close and sets $R3 to what it could do: 1 = a window exists and was asked to
-; close (or is waiting on a question of its own), 4 = it has no window a user could answer,
-; anything else = the probe could not answer. Only 4 lets the wait act on its own; every
-; other code keeps the user in the decision, because the installer holds a live handle and
-; "the probe did not find it" is not the same answer as "it is gone".
-;
-; The guard's CloseHolders answers the same question for every holder and leaves a
-; windowless instance to the user, which is right there: the prompt names it. The launcher
-; is the one process nobody can be asked about - the in-app updater runs the installer
-; silent and detached, and a client that has already hidden its window has answered its
-; own prompts - so this answer has to say whether there was a window to ask at all.
-Function CloseLauncher
- Call ExtractHolderProbe
- System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")'
- System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_LAUNCHER_PID", "$WaitPid")'
- ; The probe waits 30 s itself when it posts a close. The timeout here only bounds a
- ; probe that never returns; the Restart Manager call the other modes make is not
- ; reached in this one.
- nsExec::ExecToStack /TIMEOUT=40000 '"$PLUGINSDIR\installer-holder-probe.exe" --close-launcher'
- Pop $R3
- Pop $R4
- Push "close-launcher: probe exit=$R3 [$R4]"
- Call CockLogWrite
- DetailPrint "Closing Cockatrice: $R4"
-FunctionEnd
-
; Waits for the application that launched this installer to exit, using the /PID=
; it passed. Sets $LauncherExited to 1 when that process is provably gone.
;
; This is the authority for the one question the rest of the installer can only
; approximate: is the process that started us still holding its Qt DLLs mapped?
-; A mapped module cannot be deleted, so an answer of "nothing is holding them"
-; obtained from file handles is what allows the update to write a new payload on
+; A mapped module cannot be deleted, so an answer of "not running" obtained by
+; enumerating process names is what allows the update to write a new payload on
; top of files it could not remove. Waiting on a real handle has no such gap,
-; needs no process enumeration at all, and is unaffected by a process the holder
-; probe cannot inspect.
+; needs no powershell.exe, and is unaffected by a process whose path cannot be
+; read - the case the name based check has to treat as "not ours" so it never
+; closes a third party application.
;
; Sets $LauncherExited to 0 when no usable /PID= was given, which is the older
; client that does not pass one; the caller then has no authority available and
-; falls back to the holder probe.
-;
-; A launcher that survives the first grace is asked to close by the probe, and the
-; probe also answers whether there was a window a close could reach. A client hides
-; its window the moment its own close is accepted, so a launcher without one has
-; already answered its own prompts and is stuck in shutdown: no dialog can reach it
-; there, and the wait ends it after one more grace instead of asking the user a
-; question whose "keep waiting" answer can never change anything.
+; falls back to the name based guard.
Function WaitForLauncherToExit
StrCpy $LauncherExited 0
- StrCpy $WaitWindowless 0
!insertmacro CockLog "wait-launcher: pid='$WaitPid'"
${If} $WaitPid == ""
!insertmacro CockLog "wait-launcher: no pid given, falling back to name check"
Return
${EndIf}
+ ; PROCESS_SYNCHRONIZE is the only access this needs - enough to wait on the
+ ; handle, and the least likely to be refused for a reason that has nothing to
+ ; do with the upgrade.
+ ;
; System.dll maps its output registers so that .rN writes $N and .RN writes
; $RN. The caller reads $R0/$R1, so the uppercase .RN form is required; the
; original .r0/.r1 wrote $0/$1 and left $R1 empty, which made the "did not
; exit" test below compare "" against "0" and abort on every /PID= upgrade.
- ; Reopened on every round below, so a launcher that exited in the meantime answers
- ; "already exited" instead of being waited on again.
- ;
- ; PROCESS_TERMINATE rides along with PROCESS_SYNCHRONIZE so that the "end it"
- ; answer below acts on this very handle: the pid looked up again after a dialog
- ; that can stay open as long as the user takes would name whatever process holds
- ; that pid by then, and a handle cannot be handed to another process.
- ;
- ; When those rights are refused - a launcher started by another account, a
- ; protected process - the wait still has to happen, so the ask is repeated with
- ; synchronize access alone and $WaitCanEnd records that ending is not on offer.
- ; Reporting "already exited" instead would be the worse answer: the payload would
- ; be written over a live process, which is what this wait exists to prevent.
- ;
- ; Ten seconds, not the full grace: a client that started an update exits in well
- ; under a second, so at ten seconds it is either flushing a card database or - far
- ; more often - sitting on its own "quit anyway?" confirmation with nobody looking at
- ; it. Silence for a minute is what makes that read as a hung installer. The full
- ; grace is spent after the first question, so a user who answers Cockatrice's
- ; confirmation is not asked again while they do it.
- StrCpy $WaitSlice 10000
- wait_launcher_open:
ClearErrors
- StrCpy $WaitCanEnd 1
- System::Call 'kernel32::OpenProcess(i 0x00100001, i 0, i $WaitPid) p.R0'
+ System::Call 'kernel32::OpenProcess(i 0x00100000, i 0, i $WaitPid) p.R0'
; OpenProcess returns a null handle for a process that is already gone. The
; System plugin does not raise the NSIS error flag from a called function's
; return value, so ${Errors} alone never saw that case and the null handle was
@@ -1494,118 +999,27 @@ Function WaitForLauncherToExit
${If} ${Errors}
StrCpy $R0 0
${EndIf}
- ${If} $R0 == 0
- StrCpy $WaitCanEnd 0
- ClearErrors
- System::Call 'kernel32::OpenProcess(i 0x00100000, i 0, i $WaitPid) p.R0'
- ${If} ${Errors}
- StrCpy $R0 0
- ${EndIf}
- ${EndIf}
${If} $R0 == 0
; The launcher is gone - OpenProcess cannot open a dead process - which is
- ; exactly the state the wait was for. A handle refused for want of the terminate
- ; right is not this case; the synchronize-only ask above covers it.
+ ; exactly the state the wait was for.
!insertmacro CockLog "wait-launcher: already exited"
DetailPrint "The application that started this installer has already exited."
StrCpy $LauncherExited 1
Return
${EndIf}
DetailPrint "Waiting for the application that started this installer to exit ..."
+ ; Same 60 s grace period the name based check uses, so a client that is slow
+ ; to flush its card database does not get less room than it used to.
; WAIT_OBJECT_0 is 0; WAIT_TIMEOUT is 258 and a failed wait reports 0xFFFFFFFF.
- System::Call 'kernel32::WaitForSingleObject(p R0, i $WaitSlice) i.R1'
+ System::Call 'kernel32::WaitForSingleObject(p R0, i 60000) i.R1'
+ System::Call 'kernel32::CloseHandle(p R0)'
${If} $R1 != 0
- ; $R0 is deliberately still open: it is the process that timed out, and the answer
- ; below has to act on that process and not on whatever $WaitPid names by then.
- !insertmacro CockLog "wait-launcher: did not exit within $WaitSlice ms (wait=$R1, canEnd=$WaitCanEnd)"
- DetailPrint "The application that started this installer did not exit in time."
- ; Ask the launcher itself before asking the user. A client that has already hidden
- ; its window has answered its own prompts and is stuck in shutdown: there is nothing
- ; left a dialog could reach, and the "ask it to close again" answer the dialog used
- ; to offer could only repeat itself until the user cancelled.
- StrCpy $GuardDir $INSTDIR
- Call CloseLauncher
- ; $R3 = what the probe could do (see CloseLauncher), $R4 = its line, already logged.
- ; A process that answered the close during the probe's own wait is gone by now.
- System::Call 'kernel32::WaitForSingleObject(p R0, i 0) i.R1'
- ${If} $R1 == 0
- System::Call 'kernel32::CloseHandle(p R0)'
- StrCpy $R0 0
- Goto wait_launcher_open
- ${EndIf}
- ${If} $R3 == "4"
- ; Nothing a user could answer: the window is gone, so the launcher is past its
- ; own prompts and stuck in shutdown. It cannot be asked, and it is not going to
- ; ask the user anything either, so the dialog below would be unanswerable.
- ${If} $WaitCanEnd == 1
- ${If} $WaitWindowless == 0
- ; Hiding the window is what the client does when its close is accepted,
- ; so what remains is the shutdown it already started: one full grace for
- ; that, without a question nobody could act on.
- StrCpy $WaitWindowless 1
- StrCpy $WaitSlice 60000
- !insertmacro CockLog "wait-launcher: no window to ask, giving it one more minute"
- System::Call 'kernel32::CloseHandle(p R0)'
- StrCpy $R0 0
- Goto wait_launcher_open
- ${EndIf}
- ${If} $WaitWindowless == 1
- ; It did not use that minute either. Ending it is what the dialog's Yes
- ; answer would have done, and there is no question left to put to the
- ; user: the instance lost its window before this round started.
- StrCpy $WaitWindowless 2
- !insertmacro CockLog "wait-launcher: still no window to ask; ending the launcher"
- DetailPrint "The application that started this installer could not be asked to close; ending it."
- System::Call 'kernel32::TerminateProcess(p R0, i 1) i.R2'
- !insertmacro CockLog "wait-launcher: ending the launcher returned $R2"
- System::Call 'kernel32::CloseHandle(p R0)'
- StrCpy $R0 0
- Goto wait_launcher_open
- ${EndIf}
- ${EndIf}
- ${EndIf}
- ; A window exists, or the launcher cannot be ended from here: the three answers
- ; below. "Carry on anyway" is not one of them - a live process has its image and its
- ; Qt DLLs mapped, so writing the payload over them is the broken install this wait
- ; exists to prevent. Yes ends the launcher, Cancel cancels, and No keeps waiting:
- ; the close and the bring-forward just happened above.
- MessageBox MB_YESNOCANCEL|MB_ICONEXCLAMATION|MB_DEFBUTTON2 \
- "Cockatrice has not finished closing, so the installation is paused rather than replace files it is still using.$\r$\n$\r$\nIt may be waiting for you to answer its own question about an unsaved deck, an open game or a card database update.$\r$\n$\r$\nChoose Yes to end it now - an unsaved deck, a running game or an unwritten setting is lost.$\r$\n$\r$\nChoose No to keep waiting, or Cancel to cancel the installation." \
- IDYES wait_launcher_end IDNO wait_launcher_retry
- !insertmacro CockLog "wait-launcher: cancelled by user"
+ !insertmacro CockLog "wait-launcher: did not exit within 60s (wait=$R1)"
+ DetailPrint "The application that started this installer did not exit within 60 s."
StrCpy $AbortReason \
"The application that started this update has not exited yet, so the installation was cancelled rather than replace files it is still using.$\r$\n$\r$\nPlease close Cockatrice and run the installer again."
Call AbortInstall
- ; Only reached through Yes or No: AbortInstall does not return.
- wait_launcher_end:
- ; End it through the handle waited on above, and through nothing else: no pid is
- ; looked up again, so a process that inherited the number in the meantime - or a
- ; /PID= that was not the launcher to begin with - cannot be hit by this.
- !insertmacro CockLog "wait-launcher: ending the launcher on request"
- ${If} $WaitCanEnd == 0
- !insertmacro CockLog "wait-launcher: the launcher could not be opened to end it, asking it to close instead"
- Goto wait_launcher_retry
- ${EndIf}
- System::Call 'kernel32::TerminateProcess(p R0, i 1) i.R3'
- System::Call 'kernel32::CloseHandle(p R0)'
- StrCpy $R0 0
- !insertmacro CockLog "wait-launcher: ending the launcher returned $R3"
- Goto wait_launcher_open
- wait_launcher_retry:
- System::Call 'kernel32::CloseHandle(p R0)'
- StrCpy $R0 0
- ; The question has been asked and answered once; from here the wait is the full
- ; grace again, so a slow flush or a confirmation being answered does not turn
- ; into a stream of prompts. The close itself and the bring-forward already
- ; happened just before the dialog, so this only waits. The windowless grace starts
- ; over as well: "keep waiting" must not be answered by ending the process a minute
- ; later without asking again.
- StrCpy $WaitWindowless 0
- StrCpy $WaitSlice 60000
- Goto wait_launcher_open
${EndIf}
- System::Call 'kernel32::CloseHandle(p R0)'
- StrCpy $R0 0
!insertmacro CockLog "wait-launcher: launcher exited"
DetailPrint "The application that started this installer has exited."
StrCpy $LauncherExited 1
@@ -1629,6 +1043,57 @@ Function EnsureOldInstallNotRunning
Call GuardAppsNotRunning
FunctionEnd
+Function GuardAppsNotRunning
+ ; usage: set $GuardDir to the directory to guard, then call this
+ Call IsProcessLookupAvailable
+ !insertmacro CockLog "guard: powershell available=$R0 dir=$GuardDir launcherExited=$LauncherExited"
+ ${If} $R0 = 0
+ ; Without PowerShell we cannot tell whether a running instance is
+ ; holding locks on the files we are about to replace.
+ !insertmacro CockLog "guard: powershell unavailable, skipping instance check"
+ DetailPrint "Could not start powershell.exe; skipping the running instance check. Close Cockatrice, Oracle and Servatrice manually if the installation reports a problem."
+ ; On a silent upgrade that used to be a warning and nothing more, which
+ ; left the very process that launched the installer free to go on
+ ; replacing files it still had mapped - the one outcome this guard exists
+ ; to prevent - and then writing the new payload on top of them. Cancel
+ ; instead. An interactive install can still continue: the user is at the
+ ; keyboard, and refusing outright would leave them with an installer that
+ ; can never complete.
+ ;
+ ; Unless the launcher has already been waited on directly: WaitForLauncherToExit
+ ; needs no powershell.exe, so when it has run, Cockatrice is known to be
+ ; gone and the lookup failing to run costs nothing but the Oracle and
+ ; Servatrice checks. An older client that passes no /PID= leaves no such
+ ; authority, and there the upgrade is cancelled as before.
+ ${If} $LauncherExited != 1
+ ${AndIf} $ReinstallMode = 1
+ StrCpy $AbortReason \
+ "The installation was cancelled because it could not check whether Cockatrice is still running, because powershell.exe could not be started.$\r$\n$\r$\nPlease close Cockatrice, Oracle and Servatrice and run the installer again."
+ Call AbortInstall
+ ${EndIf}
+ ; The user has to be told to close the applications by hand, but a /S
+ ; install without /R is unattended and would block on this forever.
+ ${If} $ReinstallMode = 1
+ ${OrIfNot} ${Silent}
+ MessageBox MB_OK|MB_ICONEXCLAMATION "This installer could not check whether Cockatrice is still running, because powershell.exe could not be started.$\r$\n$\r$\nPlease close Cockatrice, Oracle and Servatrice manually before continuing."
+ ${EndIf}
+ Return
+ ${EndIf}
+ StrCpy $R1 "cockatrice.exe"
+ StrCpy $R2 "cockatrice"
+ ; a GUI application with unsaved user data: graceful close only
+ StrCpy $AllowForceClose 0
+ Call WaitForAppToClose
+ StrCpy $R1 "oracle.exe"
+ StrCpy $R2 "oracle"
+ StrCpy $AllowForceClose 1
+ Call WaitForAppToClose
+ StrCpy $R1 "servatrice.exe"
+ StrCpy $R2 "servatrice"
+ StrCpy $AllowForceClose 1
+ Call WaitForAppToClose
+FunctionEnd
+
; Sets $R0 to 1 if $INSTDIR looks like an existing Cockatrice install. Used to
; guard the stale-runtime-DLL purge so a first-time install into a pre-existing
; directory with an unrelated "Plugins" folder is never touched, while an
@@ -1706,351 +1171,142 @@ Function CheckStaleRuntimeMask
${EndIf}
FunctionEnd
-; Clears the read-only attribute of everything the purge is about to delete.
-;
-; Delete and RMDir only set the error flag: a read-only file is left on disk
-; untouched, and the retry loop then cannot make progress however long it waits,
-; because the file is not held - it is protected. The abort diagnostics have a
-; branch for exactly that ("read-only, which stops Delete"), so a survivor that
-; is both free and read-only was a dead end until now. This runs on every
-; attempt, so an attribute set again between attempts is cleared as well.
-; SetFileAttributes has no wildcard support, hence the walks.
-Function ClearReadOnlyPurgeTargets
- FindFirst $StaleFindHandle $StaleFindName "$INSTDIR\*.dll"
- ${If} $StaleFindHandle != ""
- crp_dll_loop:
- SetFileAttributes "$INSTDIR\$StaleFindName" NORMAL
- ClearErrors
- FindNext $StaleFindHandle $StaleFindName
- IfErrors 0 crp_dll_loop
- FindClose $StaleFindHandle
- ${EndIf}
- StrCpy $R6 "$INSTDIR\Plugins"
- Call ClearReadOnlyTree
-FunctionEnd
-
-; usage: set $R6 to a directory. Clears the read-only attribute of every entry
-; below it, so RMDir /r is not stopped by a single file. Saves and restores the
-; registers it works in, because it recurses into itself.
-Function ClearReadOnlyTree
- Push $R6
- Push $R7
- Push $R8
- FindFirst $R7 $R8 "$R6\*.*"
- ${If} $R7 != ""
- crt_loop:
- ${If} $R8 != "."
- ${AndIf} $R8 != ".."
- SetFileAttributes "$R6\$R8" NORMAL
- ; Only an entry with contents has anything below it that the purge
- ; would fail on; an empty directory's own attribute is already
- ; cleared above and there is nothing left to descend into.
- ${If} ${FileExists} "$R6\$R8\*.*"
- Push $R6
- StrCpy $R6 "$R6\$R8"
- Call ClearReadOnlyTree
- Pop $R6
- ${EndIf}
- ${EndIf}
- ClearErrors
- FindNext $R7 $R8
- IfErrors 0 crt_loop
- FindClose $R7
- ${EndIf}
- Pop $R8
- Pop $R7
- Pop $R6
-FunctionEnd
-
-; Removes the Plugins directory and all top-level DLLs from $INSTDIR, then
-; retries up to two more times (2 s apart) when files remain locked, so that
-; transient holders such as antivirus scanners have a chance to release their
-; handles before the installer gives up. Three attempts total. The read-only
-; attribute is cleared before each attempt: a protected file is not a held one
-; and would never clear on its own.
-; Sets $R0 to 0 when the purge is clean, or to 1 when stale DLLs remain
-; after all attempts.
-Function RetryDllPurge
- StrCpy $R5 0 ; counts retries after the initial attempt
- rdp_loop:
- Call ClearReadOnlyPurgeTargets
- RMDir /r "$INSTDIR\Plugins"
- Delete "$INSTDIR\*.dll"
- Call HasStaleRuntimeFiles
- ; $R0 = 0: all clear, nothing left to do.
- ${If} $R0 = 0
+; Uninstaller copies of the same routines (the uninstaller gets its own
+; function set compiled in, it cannot call the installer functions).
+Function un.IsAppRunning
+ ; An unset $GuardDir would match every process that merely shares the image
+ ; name, see IsAppRunning in the installer.
+ ${If} $GuardDir == ""
+ StrCpy $R0 2
Return
${EndIf}
- IntOp $R5 $R5 + 1
- ; Allow two retries after the initial attempt ($R5 is the retry counter,
- ; so the loop runs at most 3 times: attempt + retry 1 + retry 2).
- ${If} $R5 > 2
- !insertmacro CockLog "RetryDllPurge: stale files remain after 3 attempts, giving up"
- Return ; $R0 is already 1
- ${EndIf}
- !insertmacro CockLog "RetryDllPurge: retry $R5 of 2 after stale purge, sleeping 2 s"
- Sleep 2000
- Goto rdp_loop
-FunctionEnd ; RetryDllPurge
-; Turns a Win32 error code into the wording that goes into the log.
-; usage: set $0 to the code, result in $R0.
-;
-; These are Windows' own names for the codes and deliberately nothing more. A
-; code on its own cannot say whether an upgrade was blocked by a running instance
-; or by a permission: deleting a file whose image is mapped by another process
-; surfaces as ACCESS_DENIED, exactly like a file whose ACL denies delete does.
-; LogProcessesUsingInstallDir is what separates the two, which is why it runs
-; right after this and why the two are read together.
-Function GetWin32ErrorText
- ${If} $0 = 2
- StrCpy $R0 "already gone"
- ${ElseIf} $0 = 3
- StrCpy $R0 "path not found"
- ${ElseIf} $0 = 5
- StrCpy $R0 "access denied"
- ${ElseIf} $0 = 19
- StrCpy $R0 "write-protected media"
- ${ElseIf} $0 = 32
- StrCpy $R0 "sharing violation"
- ${ElseIf} $0 = 33
- StrCpy $R0 "lock violation"
- ${ElseIf} $0 = 1224
- StrCpy $R0 "file is mapped in memory"
+ ; Pass the directory to guard to PowerShell via an environment variable so
+ ; it never has to be embedded into the -Command string: apostrophes in a
+ ; path would terminate the command early, and the path in a -like pattern
+ ; would treat [ and ] as wildcard characters.
+ System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")'
+ ; Exit the process explicitly so nsExec's pop reflects whether a matching
+ ; process was found: powershell.exe exits 0 whether or not the pipeline
+ ; produced objects. 0 = not running, 1 = running, anything else = could not
+ ; be determined (see un.IsProcessLookupAvailable).
+ ; Strict match: a readable path under $GuardDir, same as in the installer.
+ nsExec::ExecToLog 'powershell -NoProfile -Command "if (Get-Process -Name $\'$R2$\' -ErrorAction SilentlyContinue | Where-Object { try { $$p = $$_.Path; if ($$p) { $$p.StartsWith($$env:COCKATRICE_GUARD_DIR, [StringComparison]::OrdinalIgnoreCase) } else { $$false } } catch { $$false } } | Select-Object -First 1) { exit 1 } else { exit 0 }"'
+ Pop $R0
+ ${If} $R0 == "0"
+ StrCpy $R0 0
+ ${ElseIf} $R0 == "1"
+ StrCpy $R0 1
${Else}
- StrCpy $R0 "windows error"
+ ; powershell.exe could not be run at all (nsExec pushes the string
+ ; "error"), or died with an unexpected code. That is "could not
+ ; determine", which has to stay distinguishable from "definitely
+ ; running": a state that can never clear would block the uninstaller
+ ; forever. un.IsProcessLookupAvailable warns about that case once.
+ StrCpy $R0 2
${EndIf}
FunctionEnd
-; Records which runtime files the purge left behind, what Windows said about each
-; one, and which processes are using the install directory.
-;
-; Diagnostic only, and only on the abort path: it deletes nothing, so it can never
-; turn a failed upgrade into a half-removed old install, and it decides nothing -
-; the abort that called it is already decided. It exists because NSIS Delete/RMDir
-; only set the error flag: without this, a report of "files of a previous version
-; survived the purge" said which files were present but never which one was held,
-; and never whether the cause was a lock or a permission.
-;
-; Each survivor is opened for DELETE access and then asked the question DeleteFile
-; asks: SetFileInformationByHandle(FileDispositionInfo) marks it for deletion on
-; close, and the mark is cleared again right away, so nothing is deleted (and if
-; that cancel ever failed, the file would go when the handle closes - a file the
-; purge already tried to delete, and the line says so). That separates the three
-; causes a failed Delete cannot tell apart: another handle that does not share
-; delete refuses the mark, a read-only attribute refuses it, and a file nobody
-; holds accepts it and would have been deleted. The mark is read through ?e, so a
-; refusal reports Windows' own code; clearing LastError first keeps a zero code
-; meaning "accepted". The wording below reports what Windows said and never claims
-; a cause the code cannot prove. Which process is holding a survivor, if any, is
-; what the holders line after the loop answers.
-Function LogStaleRuntimeDiagnostics
- FindFirst $StaleFindHandle $StaleFindName "$INSTDIR\*.dll"
- ${If} $StaleFindHandle != ""
- stale_diag_loop:
- ; ClearErrors tells a plugin failure apart from an API failure: the System
- ; plugin sets the error flag only when it could not make the call at all,
- ; in which case it pushes its own message instead of the call's value and
- ; the pop below must not run.
- ClearErrors
- System::Call 'kernel32::SetLastError(i 0)'
- System::Call 'kernel32::CreateFileW(w "$INSTDIR\$StaleFindName", i 0x00010000, i 0, p 0, i 3, i 0, p 0) p.R1 ?e'
- ${If} ${Errors}
- StrCpy $R0 "the delete probe could not be run"
- ${Else}
- Pop $R2
- ${If} $R2 = 0
- ; The open worked, which says nothing about Delete: a DLL a running
- ; process has mapped opens for DELETE and still refuses DeleteFile, and
- ; a read-only file does the same until its attribute is cleared. Ask
- ; Windows the question the purge actually asks - mark the file for
- ; deletion on close, then cancel that again. This is the same request
- ; DeleteFile makes on a temporary handle, so its answer says whether a
- ; delete would succeed now.
- ClearErrors
- System::Call '*(&i1 1) p.R4'
- System::Call 'kernel32::SetFileInformationByHandle(p R1, i 4, p R4, i 1) i.R5 ?e'
- ${If} ${Errors}
- StrCpy $R0 "a delete-access open now succeeds, but Windows could not be asked whether it would delete it"
- ${Else}
- Pop $R6
- ${If} $R5 != 0
- ; Accepted: cancel the mark again, so this stays a question and
- ; does not turn into the deletion it is asking about. A cancel
- ; that fails leaves the file to go when the handle closes, which
- ; is what the purge wanted anyway; the line says so.
- ClearErrors
- System::Call '*(&i1 0) p.R4'
- System::Call 'kernel32::SetFileInformationByHandle(p R1, i 4, p R4, i 1) i.R5 ?e'
- ${If} ${Errors}
- StrCpy $R0 "a delete would succeed now, but the check could not undo the mark it set"
- ${Else}
- Pop $R6
- ${If} $R5 != 0
- StrCpy $R0 "a delete would succeed now - the earlier failure was transient"
- ${Else}
- StrCpy $R0 "a delete would succeed now, and it is marked for deletion from here on"
- ${EndIf}
- ${EndIf}
- ${Else}
- StrCpy $0 $R6
- Call GetWin32ErrorText
- StrCpy $R0 "a delete-access open now succeeds, but Windows refused to mark it for deletion: win32 error $R6: $R0"
- ${EndIf}
- ${EndIf}
- System::Call 'kernel32::CloseHandle(p R1)'
- ; The read-only attribute is a cause on its own, and the purge clears it
- ; before every attempt, so a survivor that is read-only had it set again.
- System::Call 'kernel32::GetFileAttributesW(w "$INSTDIR\$StaleFindName") i.R3'
- ${If} $R3 != -1
- IntOp $R3 $R3 & 1
- ${If} $R3 = 1
- StrCpy $R0 "$R0; it is read-only again"
- ${EndIf}
- ${EndIf}
- ${Else}
- StrCpy $0 $R2
- Call GetWin32ErrorText
- StrCpy $R0 "win32 error $R2: $R0"
- ${EndIf}
- ${EndIf}
- !insertmacro CockLog "stale-runtime: '$StaleFindName' is still present: $R0"
- ClearErrors
- FindNext $StaleFindHandle $StaleFindName
- IfErrors 0 stale_diag_loop
- FindClose $StaleFindHandle
- ${EndIf}
- Call LogProcessesUsingInstallDir
-FunctionEnd
-
-; Names the applications and services that still have the payload open or mapped.
-; Writes one line to the log and sets $HolderFound (1 when anything was named), so the
-; abort message can repeat that line to the user.
-;
-; The lookup is tools/installer-holder-probe, the same executable the guard runs before
-; anything is replaced; here it answers the other question, about files a purge could not
-; remove. It asks the Restart Manager (RmRegisterResources/RmGetList), which names a
-; process holding a plain handle on a file - an antivirus scanner - and names services as
-; well as user programs, and it adds every payload program running from the directory
-; itself - not one in a folder below it, which is another application the user installed
-; there - because a running instance holds its payload as mapped images rather than as open
-; handles and the Restart Manager does not reliably report those. Its remaining blind spot
-; is a handle taken in kernel mode, where there is no process to name; a payload executable
-; among the files then still refuses to be replaced, and the probe answers with that file
-; instead of naming nothing.
-;
-; $INSTDIR travels through the same COCKATRICE_GUARD_DIR environment variable the guard
-; uses, so a path with spaces or apostrophes never has to be embedded in a command line.
-Function LogProcessesUsingInstallDir
- StrCpy $HolderExit 0
- StrCpy $HolderOutput ""
- StrCpy $HolderFound 0
- ; The directory that just failed the purge, not whatever a check before the directory
- ; page looked at: a portable install into a custom folder reaches here with the two
- ; being different.
- StrCpy $GuardDir $INSTDIR
- Call RunHolderProbe
- StrCpy $HolderExit $GuardExit
- StrCpy $HolderOutput $GuardOutput
- ; The exit code, not the text, decides what happened: it cannot be mangled by what
- ; nsExec leaves on the end of the captured output. 1 = the line below names what is
- ; holding the files, 0 = nothing holds them any more, anything else = the probe could
- ; not answer, and says why in its own output.
- ${If} $HolderExit == "1"
- StrCpy $HolderFound 1
- ${ElseIf} $HolderExit == "0"
- StrCpy $HolderOutput "nothing is holding them and no payload program is running from the directory"
+Function un.IsProcessLookupAvailable
+ nsExec::ExecToLog 'powershell -NoProfile -Command "exit 0"'
+ Pop $R0
+ ${If} $R0 == "0"
+ StrCpy $R0 1
${Else}
- StrCpy $HolderOutput "the holder probe could not name what holds them: $HolderOutput"
+ StrCpy $R0 0
${EndIf}
- !insertmacro CockLog "holders: $HolderOutput"
FunctionEnd
-; Called after a purge left files behind and LogStaleRuntimeDiagnostics named who is
-; holding them. Instead of aborting straight away - which on /R strands the user, since
-; the old version is already uninstalled - it closes once, purges again, and only then
-; asks: end what is still holding the files, or try the removal again by hand. Sets
-; $StaleRuntimeAfterPurge to the last purge's verdict, so the caller still aborts when the
-; files survive.
-;
-; The shape is the guard's, deliberately: one automatic attempt at a graceful close, then a
-; prompt whose Yes ends the payload programs and whose No retries. A client that was still
-; shutting down releases the files in the automatic round and never sees a question, and an
-; Oracle or Servatrice - which a close cannot reach - is what the prompt exists for, so the
-; user is never left with a dialog whose answers cannot free anything.
-;
-; The automatic close is a request (WM_CLOSE) and the client keeps the last word: it may be
-; waiting for an answer to its own "really quit?" confirmation (a running card database
-; update, an open game, an unsaved deck), and that stays its decision. The instance is
-; brought forward first, so that confirmation is on screen rather than behind this
-; installer's prompt.
-;
-; Ending is the same last resort the guard offers, with the same cost spelled out, and only
-; ever the programs the payload installs - see EndHolders.
-;
-; Restart Manager's RmShutdown was the alternative and was rejected: it shuts down
-; services as well as applications, and the only way to bring them back is RmRestart,
-; which would restart this very application into files that are about to be replaced.
-;
-; Always offered, in every mode: a /S install without /R is the only case without someone
-; at the keyboard, and this installer does not promise unattended runs - if an error needs
-; a decision, it asks for one. No offer when the probe named nobody: there is nothing to
-; end, and the abort's per-file diagnostics already record what Windows said.
-;
-; One question after the automatic close, and never a loop: Yes ends the programs the
-; payload installs - and says so, because anything else the probe named is left to the user -
-; and purges again; the other answer gives up, and the caller's abort names what survived.
-; The listing is re-read before the question, so it says what is left now rather than
-; repeating the first one.
-Function RecoverFromHolders
- ${If} $HolderFound != 1
- !insertmacro CockLog "recover-holders: no holder was named, nothing to offer"
- Return
- ${EndIf}
- ; The automatic attempt, the same one the guard makes before it asks anything.
- !insertmacro CockLog "recover-holders: closing cockatrice, then trying the removal again"
- StrCpy $GuardDir $INSTDIR
- Call CloseHolders
- Call RetryDllPurge
- StrCpy $StaleRuntimeAfterPurge $R0
- !insertmacro CockLogListing "after-retry-purge" "*.dll"
- !insertmacro CockLogListing "after-retry-plugins" "Plugins\*.*"
- !insertmacro CockLog "recover-holders: stale-runtime after retry=$StaleRuntimeAfterPurge"
- ${If} $StaleRuntimeAfterPurge = 0
- DetailPrint "Files of the previous version were removed; continuing the installation."
- Return
- ${EndIf}
- ; Still stuck: refresh the names, so the prompt and the log describe what survived the
- ; last attempt and not what was there before it.
- Call LogStaleRuntimeDiagnostics
- ${If} $HolderFound != 1
- Return
- ${EndIf}
- ; One question, the same one the guard asks and with the same two answers. No retry loop:
- ; the purge has already been tried after the close, so the only thing that can still free
- ; the files is ending them, and the caller's abort names whatever survives that.
- !insertmacro CockEndPrompt "Files of the previous version of Cockatrice are still in use by:" $HolderOutput "installation"
- MessageBox MB_YESNO|MB_ICONEXCLAMATION|MB_DEFBUTTON2 "$GuardPrompt" IDYES rfh_end
- !insertmacro CockLog "recover-holders: cancelled by user"
- Return
- ; Only reached through Yes: Return does not come back here.
- rfh_end:
- !insertmacro CockLog "recover-holders: ending the payload programs on request, then trying the removal again"
- Call EndHolders
- Call RetryDllPurge
- StrCpy $StaleRuntimeAfterPurge $R0
- !insertmacro CockLog "recover-holders: stale-runtime after end=$StaleRuntimeAfterPurge"
- ${If} $StaleRuntimeAfterPurge = 0
- DetailPrint "Files of the previous version were removed; continuing the installation."
- Return
- ${EndIf}
- ; Refresh the names one last time so the abort that follows describes what survived the
- ; end, and then let it stop the run: the same dialog again would only repeat itself.
- Call LogStaleRuntimeDiagnostics
+Function un.CloseMatchingApps
+ ; gracefully ask every matching instance to close (sends WM_CLOSE)
+ ;
+ ; Strict filter, same as in the installer: never close a process whose path
+ ; cannot be read, it is not necessarily one of ours.
+ System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")'
+ nsExec::ExecToLog 'powershell -NoProfile -Command "Get-Process -Name $\'$R2$\' -ErrorAction SilentlyContinue | Where-Object { try { $$p = $$_.Path; if ($$p) { $$p.StartsWith($$env:COCKATRICE_GUARD_DIR, [StringComparison]::OrdinalIgnoreCase) } else { $$false } } catch { $$false } } | ForEach-Object { $$null = $$_.CloseMainWindow() }"'
+ Pop $R3
+FunctionEnd
+
+Function un.ForceCloseApps
+ ; stop the matching instances by PID once the grace period is over, see
+ ; ForceCloseApps in the installer.
+ System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")'
+ nsExec::ExecToLog 'powershell -NoProfile -Command "Get-Process -Name $\'$R2$\' -ErrorAction SilentlyContinue | Where-Object { try { $$p = $$_.Path; if ($$p) { $$p.StartsWith($$env:COCKATRICE_GUARD_DIR, [StringComparison]::OrdinalIgnoreCase) } else { $$false } } catch { $$false } } | ForEach-Object { Stop-Process -Id $$_.Id -Force -ErrorAction SilentlyContinue }"'
+ Pop $R3
+FunctionEnd
+
+Function un.WaitForAppToClose
+ Call un.IsAppRunning
+ ; Only a definite "running" is worth waiting for, see WaitForAppToClose.
+ ${If} $R0 != 1
+ Return
+ ${EndIf}
+
+ ${If} ${Silent}
+ DetailPrint "Closing $R1 ..."
+ Call un.CloseMatchingApps
+ ; wall-clock deadline: each poll has to spawn a fresh powershell.exe, so
+ ; a bare loop counter would not bound the real wait time to 60 s.
+ System::Call 'kernel32::GetTickCount()i.s'
+ Pop $R8
+ un_ck_wait_loop:
+ Sleep 500
+ Call un.IsAppRunning
+ ${If} $R0 = 0
+ DetailPrint "$R1 closed."
+ Return
+ ${EndIf}
+ ${If} $R0 != 1
+ Return
+ ${EndIf}
+ System::Call 'kernel32::GetTickCount()i.s'
+ Pop $R9
+ IntOp $R9 $R9 - $R8
+ ${If} $R9 < 60000
+ Goto un_ck_wait_loop
+ ${EndIf}
+ ; The grace period is over. Cockatrice is never force-stopped, see
+ ; WaitForAppToClose in the installer. A silent uninstall only reports
+ ; through the log and its exit code: it is also run unattended (the /R
+ ; updater uses it via QuietUninstallString, package managers use it
+ ; directly), and a message box would block there. The installer that ran
+ ; us reports the incomplete result to the user itself.
+ ${If} $AllowForceClose = 0
+ DetailPrint "$R1 is still running after 60 s; aborting so that no data is lost."
+ SetErrorLevel 1
+ Abort
+ ${EndIf}
+ DetailPrint "Force-stopping $R1 ..."
+ Call un.ForceCloseApps
+ Sleep 2000
+ Call un.IsAppRunning
+ ${If} $R0 = 0
+ DetailPrint "$R1 closed."
+ Return
+ ${EndIf}
+ ${If} $R0 != 1
+ Return
+ ${EndIf}
+ DetailPrint "$R1 is still running after 60 s; aborting."
+ SetErrorLevel 1
+ Abort
+ ${Else}
+ un_ck_wait_prompt:
+ MessageBox MB_RETRYCANCEL|MB_ICONEXCLAMATION|MB_DEFBUTTON1 \
+ "$R1 is still running.$\r$\n$\r$\nPlease close it, then click Retry.$\r$\nClick Cancel to abort." \
+ IDCANCEL un_ck_abort_install
+ Call un.IsAppRunning
+ ${If} $R0 = 0
+ Return
+ ${EndIf}
+ ${If} $R0 != 1
+ Return
+ ${EndIf}
+ Goto un_ck_wait_prompt
+ un_ck_abort_install:
+ Abort
+ ${EndIf}
FunctionEnd
-; Uninstaller copy of the guard (the uninstaller gets its own function set compiled in, it
-; cannot call the installer functions). ExtractHolderProbe and RunHolderProbe come from
-; CockHolderProbeFunctions above; only the policy differs.
Function un.EnsureAppsNotRunning
; guards the install directory; the uninstaller only ever has to check this one
StrCpy $GuardDir $INSTDIR
@@ -2058,80 +1314,29 @@ Function un.EnsureAppsNotRunning
FunctionEnd
Function un.GuardAppsNotRunning
- StrCpy $GuardCloseRounds 0
- StrCpy $GuardRecheckCount 0
- StrCpy $GuardLastHolders ""
- un_guard_check:
- !insertmacro CockLogUn "uninst-guard: checking $GuardDir"
- Call un.RunHolderProbe
- ${If} $GuardExit == "0"
- !insertmacro CockLogUn "uninst-guard: nothing is holding the payload"
+ Call un.IsProcessLookupAvailable
+ ${If} $R0 = 0
+ ; Same reasoning as in the installer: without PowerShell we cannot tell
+ ; whether a running instance still holds file locks, and refusing to
+ ; uninstall would leave the user with an uninstaller that never
+ ; completes. Warn once and continue.
+ DetailPrint "Could not start powershell.exe; skipping the running instance check. Close Cockatrice, Oracle and Servatrice manually if the uninstall reports a problem."
+ MessageBox MB_OK|MB_ICONEXCLAMATION "This uninstaller could not check whether Cockatrice is still running, because powershell.exe could not be started.$\r$\n$\r$\nPlease close Cockatrice, Oracle and Servatrice manually before continuing."
Return
${EndIf}
- ${If} $GuardExit == "1"
- !insertmacro CockLogUn "uninst-guard: still in use by $GuardOutput"
- ; Same convenience as in the installer: ask every instance to close, repeat only
- ; while the set changed, then report what the close could not reach.
- ${If} $GuardCloseRounds < 2
- ${AndIf} $GuardOutput != $GuardLastHolders
- StrCpy $GuardLastHolders $GuardOutput
- IntOp $GuardCloseRounds $GuardCloseRounds + 1
- !insertmacro CockLogUn "uninst-guard: asking the payload programs to close (round $GuardCloseRounds)"
- Call un.CloseHolders
- Goto un_guard_check
- ${EndIf}
- DetailPrint "The install directory is in use by: $GuardOutput"
- ; Same question and the same three answers as the installer's guard: force close,
- ; close by hand and check again, or abort. CockEndRecheckPrompt words it, so the two
- ; copies promise the same thing.
- !insertmacro CockEndRecheckPrompt "Cockatrice, Oracle or Servatrice is still using files of the installation:" $GuardOutput "uninstallation"
- MessageBox MB_YESNOCANCEL|MB_ICONEXCLAMATION|MB_DEFBUTTON2 "$GuardPrompt" IDYES un_guard_end IDNO un_guard_recheck
- !insertmacro CockLogUn "uninst-guard: cancelled by user after $GuardRecheckCount recheck(s)"
- ; The installer's Cancel lands in AbortInstall, which explains what to do next. The
- ; uninstaller has no such dialog, so say the same thing here: a bare Abort would
- ; close the window and leave the user with no reason and no next step.
- MessageBox MB_OK|MB_ICONEXCLAMATION \
- "The uninstallation was cancelled because Cockatrice, Oracle or Servatrice is still running and holding files of the installation.$\r$\n$\r$\n${AFTER_CLOSING_GUIDANCE}$\r$\n$\r$\nClose them and run the uninstaller again."
- SetErrorLevel 1
- Abort
- un_guard_recheck:
- IntOp $GuardRecheckCount $GuardRecheckCount + 1
- !insertmacro CockLogUn "uninst-guard: user closed programs by hand, checking again (recheck $GuardRecheckCount)"
- StrCpy $GuardCloseRounds 0
- StrCpy $GuardLastHolders ""
- Goto un_guard_check
- ; Only reached through Yes: Abort does not return.
- un_guard_end:
- !insertmacro CockLogUn "uninst-guard: ending the payload programs on request, then checking again"
- Call un.EndHolders
- ; One measurement and a stop, not another question: what survived a terminate is a
- ; program this uninstaller may not open or does not own, and ending it again cannot
- ; change that.
- Call un.RunHolderProbe
- ${If} $GuardExit == "0"
- !insertmacro CockLogUn "uninst-guard: nothing is holding the payload"
- Return
- ${EndIf}
- !insertmacro CockLogUn "uninst-guard: still in use after ending them ($GuardExit): $GuardOutput"
- SetErrorLevel 1
- Abort
- ${EndIf}
- ; Same reasoning as in the installer: without an answer from the probe we cannot tell
- ; whether a running instance still holds file locks, and refusing outright would leave
- ; the user with an uninstaller that never completes. Same three answers too, so an
- ; uninstall that hits a Restart Manager that was busy, or a payload program the user has
- ; not closed yet, can simply be retried.
- !insertmacro CockLogUn "uninst-guard: the holder probe could not answer ($GuardExit): $GuardOutput"
- DetailPrint "Could not confirm whether Cockatrice is still running: $GuardOutput"
- MessageBox MB_ABORTRETRYIGNORE|MB_ICONEXCLAMATION|MB_DEFBUTTON1 \
- "This uninstaller could not confirm whether Cockatrice, Oracle or Servatrice is still running.$\r$\n$\r$\nThe check reported: $GuardOutput$\r$\n$\r$\nClose them, then choose Retry to check again. Choose Ignore to uninstall anyway, or Abort to cancel the uninstallation." \
- IDRETRY un_guard_check IDABORT un_guard_unknown_abort
- !insertmacro CockLogUn "uninst-guard: continuing without an answer from the probe"
- Return
- un_guard_unknown_abort:
- !insertmacro CockLogUn "uninst-guard: cancelled after the probe could not answer"
- SetErrorLevel 1
- Abort
+ StrCpy $R1 "cockatrice.exe"
+ StrCpy $R2 "cockatrice"
+ ; a GUI application with unsaved user data: graceful close only
+ StrCpy $AllowForceClose 0
+ Call un.WaitForAppToClose
+ StrCpy $R1 "oracle.exe"
+ StrCpy $R2 "oracle"
+ StrCpy $AllowForceClose 1
+ Call un.WaitForAppToClose
+ StrCpy $R1 "servatrice.exe"
+ StrCpy $R2 "servatrice"
+ StrCpy $AllowForceClose 1
+ Call un.WaitForAppToClose
FunctionEnd
Function PortableModePageCreate
@@ -2187,10 +1392,7 @@ ${If} $PortableMode = 0
${If} $ReinstallMode = 0
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "A previous version of Cockatrice must be uninstalled before installing the new one." IDOK uninst32
- ; Abort would only skip this page and install over the old version anyway;
- ; the message said the old one must go first, so Cancel means cancel.
- !insertmacro CockLog "componentsPagePre: cancelled at the previous-version prompt"
- Quit
+ Abort
${Else}
Goto uninst32
${EndIf}
@@ -2207,24 +1409,10 @@ ${If} $PortableMode = 0
StrCpy $OldUninstallString $R0
Call EnsureOldInstallNotRunning
StrCpy $OldInstallDir $GuardDir
- ; Run it silently. The question above is the one the user answered; the
- ; uninstaller's own confirmation page would be a second one, and that one opens
- ; behind this window, where nobody can answer it. The installer then looks hung,
- ; the user clicks Next on it, the new version is installed, and the uninstaller
- ; removes everything once this window closes. /S leaves no window to be behind,
- ; so ExecWait is waiting for something that cannot outlive it.
- ReadRegStr $R1 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "QuietUninstallString"
- ${If} $R1 == ""
- StrCpy $OldUninstallString "$OldUninstallString /S"
- ${Else}
- StrCpy $OldUninstallString $R1
- ${EndIf}
!insertmacro CockLog "componentsPagePre: old 32-bit uninstall '$OldUninstallString _?=$OldInstallDir' (oldDir=$OldInstallDir InstDir=$INSTDIR)"
ClearErrors
Call BackupUserDataBeforeUninstall
- Call RunOldUninstaller
- !insertmacro CockLog "componentsPagePre: old 32-bit uninstall exit=$OldUninstallExit"
- Call WaitForOldPayloadToDisappear
+ ExecWait "$OldUninstallString _?=$OldInstallDir"
Call RestoreUserDataAfterUninstall
done32:
@@ -2238,8 +1426,7 @@ ${If} $PortableMode = 0
${If} $ReinstallMode = 0
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "A previous version of Cockatrice must be uninstalled before installing the new one." IDOK uninst64
- !insertmacro CockLog "componentsPagePre: cancelled at the previous-version prompt"
- Quit
+ Abort
${Else}
Goto uninst64
${EndIf}
@@ -2248,19 +1435,10 @@ ${If} $PortableMode = 0
StrCpy $OldUninstallString $R0
Call EnsureOldInstallNotRunning
StrCpy $OldInstallDir $GuardDir
- ; Silent, for the reason given in the 32-bit branch above.
- ReadRegStr $R1 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "QuietUninstallString"
- ${If} $R1 == ""
- StrCpy $OldUninstallString "$OldUninstallString /S"
- ${Else}
- StrCpy $OldUninstallString $R1
- ${EndIf}
!insertmacro CockLog "componentsPagePre: old 64-bit uninstall '$OldUninstallString _?=$OldInstallDir' (oldDir=$OldInstallDir InstDir=$INSTDIR)"
ClearErrors
Call BackupUserDataBeforeUninstall
- Call RunOldUninstaller
- !insertmacro CockLog "componentsPagePre: old 64-bit uninstall exit=$OldUninstallExit"
- Call WaitForOldPayloadToDisappear
+ ExecWait "$OldUninstallString _?=$OldInstallDir"
Call RestoreUserDataAfterUninstall
done64:
@@ -2300,23 +1478,6 @@ ${If} $ReinstallMode = 0
Call EnsureAppsNotRunning
${EndIf}
-; Measured before either purge block, because it has to describe the directory as
-; it arrived here and not what a purge left of it: IsExistingCockatriceInstall
-; looks for cockatrice.exe, oracle.exe, servatrice.exe, uninstall.exe, Qt6*.dll
-; and qt.conf, and the portable purge below deletes exactly those. Taken after
-; that purge it answered "not an install" as soon as the only survivor was a file
-; it does not look for - a locked abseil_dll.dll or libssl-3-x64.dll, say - and
-; the abort further down was then skipped for the very directory the purge had
-; just failed in.
-; Remembered because the stale-runtime check below must only ever fire for a
-; directory this installer recognised as a Cockatrice install. Scanning for
-; leftovers in an unrelated directory (a first-time install into a folder that
-; happens to hold some Qt5 DLLs, say) would otherwise turn those files into an
-; abort reason even though the payload is about to overwrite nothing of theirs.
-Call IsExistingCockatriceInstall
-!insertmacro CockLog "section: IsExistingCockatriceInstall=$R0"
-StrCpy $IsUpgrade $R0
-
${If} $PortableMode = 1
${AndIf} ${FileExists} "$INSTDIR\portable.dat"
; upgrade portable mode
@@ -2340,12 +1501,7 @@ ${AndIf} ${FileExists} "$INSTDIR\portable.dat"
; what this has to guarantee is that nothing from the previous build is left
; to be loaded next to the new executables. Naming the families by hand is
; what let abseil_dll.dll through; see HasStaleRuntimeFiles.
- Call RetryDllPurge
- ; The purge's own verdict: HasStaleRuntimeFiles measures *.dll + Plugins\*.*,
- ; the set that catches any leftover runtime DLL, Qt or otherwise. Held in a
- ; variable because $R0 is reused by the next function call in this section.
- StrCpy $StaleRuntimeAfterPurge $R0
- StrCpy $DllPurgeRan 1
+ Delete "$INSTDIR\*.dll"
Delete "$INSTDIR\qt.conf"
Delete "$INSTDIR\qdebug.txt"
Delete "$INSTDIR\servatrice.sql"
@@ -2356,33 +1512,20 @@ ${EndIf}
; Remove any runtime DLLs left over from older versions, so a mismatched
; Qt/OpenSSL/protobuf set cannot survive next to the new executables.
-;
-; A purge that fails is reported with evidence, not only as a survivor list: the
-; abort below logs which files survived, what Windows said about each of them and
-; which processes still have the install directory mapped (see
-; LogStaleRuntimeDiagnostics).
-;
-; Re-running the running-instance check here was considered and dropped: it would only
-; narrow the window the uninstaller ExecWait and the backup/restore copy open between
-; .onInit and this point, and on /R it would now abort after the old version has already
-; been removed - leaving no install at all. The surviving files are the authority on
-; whether anything still holds the directory, and they are measured anyway.
-;
-; The holder list comes from Restart Manager (RmRegisterResources/RmGetList), the API
-; that names every process and service with one of these files open or mapped. It is
-; called from tools/installer-holder-probe rather than from this script, because the
-; RM_PROCESS_INFO array is a table of hand-computed offsets when it is marshalled
-; through the System plugin, and that table is the one thing here that cannot be
-; checked without a Windows toolchain. Compiled by the same build that compiles this
-; installer, the layout belongs to the Windows SDK, and .ci/compile.sh runs the probe
-; on the Windows runners instead of trusting that it compiles.
+Call IsExistingCockatriceInstall
+!insertmacro CockLog "section: IsExistingCockatriceInstall=$R0"
+; Remembered because the stale-runtime check below must only ever fire for a
+; directory this installer recognised as a Cockatrice install. Scanning for
+; leftovers in an unrelated directory (a first-time install into a folder that
+; happens to hold some Qt5 DLLs, say) would otherwise turn those files into an
+; abort reason even though the payload is about to overwrite nothing of theirs.
+StrCpy $IsUpgrade $R0
${If} $PortableMode = 0
-${AndIf} $IsUpgrade = 1
+${AndIf} $R0 = 1
!insertmacro CockLog "section: non-portable upgrade purge"
!insertmacro CockLogListing "before-purge" "*.dll"
- Call RetryDllPurge
- StrCpy $StaleRuntimeAfterPurge $R0
- StrCpy $DllPurgeRan 1
+ RMDir /r "$INSTDIR\Plugins"
+ Delete "$INSTDIR\*.dll"
!insertmacro CockLogListing "after-purge" "*.dll"
!insertmacro CockLogListing "after-plugins" "Plugins\*.*"
${EndIf}
@@ -2397,78 +1540,22 @@ ${EndIf}
; run on Windows or it contains an error", so stop here instead of compounding
; it. Nothing of the new payload has been written at this point, so the next run,
; with the applications closed, gets a clean install.
-; $StaleRuntimeAfterPurge is RetryDllPurge's verdict, measured with
-; HasStaleRuntimeFiles (*.dll + Plugins\*.*) - the set that catches any leftover
-; runtime DLL, Qt or otherwise. It exists only when a purge ran, and the two
-; purge blocks above cover every upgrade except one: a portable install into a
-; directory without portable.dat is neither of them, so that directory is
-; measured here instead. A test of $IsUpgrade alone could not stand in for it -
-; that only says the directory was ours before the purge, not what the purge
-; left behind.
-; A directory this installer did not recognise as an install never aborts: the
-; unrelated DLLs already in it (a first-time install into a folder that happens
-; to hold some Qt5 DLLs, say) are not leftovers, and the payload overwrites
-; nothing of theirs.
${If} $IsUpgrade = 1
- ${If} $DllPurgeRan = 0
- !insertmacro CockLog "section: no purge applies, measuring the directory instead"
- Call HasStaleRuntimeFiles
- StrCpy $StaleRuntimeAfterPurge $R0
- ${EndIf}
- !insertmacro CockLog "section: stale-runtime after purge=$StaleRuntimeAfterPurge"
+ Call HasStaleRuntimeFiles
+ !insertmacro CockLog "section: HasStaleRuntimeFiles=$R0"
${Else}
- StrCpy $StaleRuntimeAfterPurge 0
+ StrCpy $R0 0
${EndIf}
-${If} $StaleRuntimeAfterPurge = 1
- !insertmacro CockLog "section: files of a previous version survived the purge"
- ; Who is holding the payload is recorded first, so the log that reaches us
- ; answers the question the message can only guess at, and so the prompt can
- ; name them and offer to close the ones this installer owns.
- Call LogStaleRuntimeDiagnostics
- ${If} $HolderFound = 1
- ; On /R the previous version is already uninstalled at this point, so an abort
- ; leaves the user with no working Cockatrice. It closes once, then offers to end
- ; what is left; this updates $StaleRuntimeAfterPurge with the verdict after that
- ; close, or after the end when the user asked for one.
- Call RecoverFromHolders
- ${Else}
- ; Nothing is holding them. The three attempts in RetryDllPurge were two seconds
- ; apart and all failed with no process to blame, which is the transient case a
- ; virus scanner or a background indexer creates - or a permission problem that
- ; will not clear. Give the delete one last chance before giving up: it costs a
- ; second on a path that is about to abort anyway, and it keeps a lock that was
- ; released in the meantime from turning into a failed upgrade.
- !insertmacro CockLog "section: no holder named, one final purge attempt"
- Sleep 1000
- Call RetryDllPurge
- StrCpy $StaleRuntimeAfterPurge $R0
- !insertmacro CockLog "section: final-purge stale-runtime=$StaleRuntimeAfterPurge"
- ${EndIf}
-${EndIf}
-${If} $StaleRuntimeAfterPurge = 1
+${If} $R0 = 1
!insertmacro CockLog "section: aborting, files of a previous version survived the purge"
DetailPrint "Files of a previous version could not be removed; aborting the installation."
- ; Say what is known and nothing more: the guard ran before anything was replaced,
- ; so "most likely still running" is no longer a fair guess and would send the user
- ; to close programs that are already closed.
+ ; The in-app updater runs this installer silent and detached and never looks
+ ; at its exit code, so the user has to be told what happened. A /S install
+ ; without /R is unattended (a package manager, for instance) and a message
+ ; box there would block forever, so that case only reports through the log
+ ; and the exit code. AbortInstall owns that decision.
StrCpy $AbortReason \
- "The installation could not remove all files of a previous version of Cockatrice, and was cancelled rather than leave a mismatched installation behind."
- ${If} $HolderFound = 1
- StrCpy $AbortReason "$AbortReason$\r$\n$\r$\nStill using the install directory:$\r$\n$HolderOutput$\r$\n$\r$\nClose them and run the installer again."
- ${ElseIf} $HolderExit == "0"
- StrCpy $AbortReason "$AbortReason$\r$\n$\r$\nNo running program was found using those files. A file permission or a virus scanner may be preventing their removal; the installer log records what Windows reported for each file."
- ${Else}
- ; The check itself failed, so "no running program was found" would be a claim this
- ; installer cannot make. Name the failure instead, and leave the per-file codes to
- ; the log, where they already are.
- StrCpy $AbortReason "$AbortReason$\r$\n$\r$\nThe check for a running program could not be completed: $HolderOutput$\r$\n$\r$\nClose Cockatrice, Oracle and Servatrice and run the installer again; the installer log records what Windows reported for each file."
- ${EndIf}
- ; "Run the installer again" is not enough on an in-app update: the old version
- ; was uninstalled before the purge, so point at the installer that was downloaded.
- ${If} $ReinstallMode = 1
- ${AndIf} $PortableMode = 0
- StrCpy $AbortReason "$AbortReason$\r$\n$\r$\nThe previous version has already been removed. To finish the update, run this installer again:$\r$\n$EXEPATH"
- ${EndIf}
+ "The installation could not remove all files of a previous version of Cockatrice, most likely because it is still running or was started as administrator, and was cancelled rather than leave a mismatched install behind.$\r$\n$\r$\nPlease close Cockatrice, Oracle and Servatrice and run the installer again."
Call AbortInstall
${EndIf}
diff --git a/cmake/SendInstallerLog.ps1.in b/cmake/SendInstallerLog.ps1.in
index 585c726dd..dfb8c3d63 100644
--- a/cmake/SendInstallerLog.ps1.in
+++ b/cmake/SendInstallerLog.ps1.in
@@ -57,29 +57,6 @@ try {
}
}
- # --- 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')
@@ -98,9 +75,6 @@ try {
extra = @{
reason = $Reason
os = [System.Environment]::OSVersion.VersionString
- holders = $Holders
- close_result = $CloseResult
- end_result = $EndResult
}
}
$eventJson = $sentryEvent | ConvertTo-Json -Compress -Depth 6
diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt
index ade7c3976..ebd2a2f78 100644
--- a/cockatrice/CMakeLists.txt
+++ b/cockatrice/CMakeLists.txt
@@ -25,7 +25,9 @@ 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
@@ -69,27 +71,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 +118,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
@@ -172,7 +195,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 +314,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 +346,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 +451,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 +512,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 +533,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 +545,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 +557,7 @@ qt_add_shaders(
src/interface/widgets/onboarding/shaders/brand_plate.frag
)
-qt_add_resources(
+qt6_add_resources(
cockatrice
"onboarding_qml"
PREFIX
@@ -554,7 +573,6 @@ target_link_libraries(
PUBLIC libcockatrice_card
libcockatrice_deck_list
libcockatrice_filters
- libcockatrice_game
libcockatrice_utility
libcockatrice_network
libcockatrice_models
@@ -771,7 +789,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 +799,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 +825,4 @@ if(Qt6LinguistTools_FOUND)
endforeach()
endif()
-qt_finalize_target(cockatrice)
+qt6_finalize_target(cockatrice)
diff --git a/cockatrice/src/client/latency_status_widget.cpp b/cockatrice/src/client/latency_status_widget.cpp
index 907a50668..940074581 100644
--- a/cockatrice/src/client/latency_status_widget.cpp
+++ b/cockatrice/src/client/latency_status_widget.cpp
@@ -14,7 +14,6 @@
#include
#include
#include
-#include
#include
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(this, 4);
+ auto *layout = new QHBoxLayout(this);
+ layout->setContentsMargins(0, 0, 0, 0);
+ layout->setSpacing(4);
layout->addWidget(latencyGraph);
layout->addWidget(pingLabel);
diff --git a/cockatrice/src/client/network/connection_controller/remote_connection_controller.cpp b/cockatrice/src/client/network/connection_controller/remote_connection_controller.cpp
index c702f91f1..037565c6a 100644
--- a/cockatrice/src/client/network/connection_controller/remote_connection_controller.cpp
+++ b/cockatrice/src/client/network/connection_controller/remote_connection_controller.cpp
@@ -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
#include
#include
-#include
#include
class QWidget;
@@ -100,10 +98,6 @@ void ConnectionController::wireClientSignals()
connect(remoteClient, &RemoteClient::sigPasswordVerifierReady, this,
&ConnectionController::onPasswordVerifierReady);
-
- connect(remoteClient, &RemoteClient::sigPasswordChangeSuccess, this,
- &ConnectionController::onPasswordChangeSuccess);
- connect(remoteClient, &RemoteClient::sigPasswordChangeError, this, &ConnectionController::onPasswordChangeError);
}
void ConnectionController::connectToServer()
@@ -249,27 +243,17 @@ void ConnectionController::onLoginError(int r,
{
switch (static_cast(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 +300,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 +341,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 +507,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(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 +517,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(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()
diff --git a/cockatrice/src/client/network/connection_controller/remote_connection_controller.h b/cockatrice/src/client/network/connection_controller/remote_connection_controller.h
index 2987a8d99..150ec9e5b 100644
--- a/cockatrice/src/client/network/connection_controller/remote_connection_controller.h
+++ b/cockatrice/src/client/network/connection_controller/remote_connection_controller.h
@@ -87,17 +87,13 @@ 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();
diff --git a/cockatrice/src/client/network/interfaces/deck_stats_interface.cpp b/cockatrice/src/client/network/interfaces/deck_stats_interface.cpp
index 508f0bdc9..d5cfdbbc9 100644
--- a/cockatrice/src/client/network/interfaces/deck_stats_interface.cpp
+++ b/cockatrice/src/client/network/interfaces/deck_stats_interface.cpp
@@ -20,12 +20,11 @@
#include
#include
#include
-#include
#include
DeckStatsInterface::DeckStatsInterface(QObject *parent) : QObject(parent)
{
- manager = NetworkManagerFactory::create(this);
+ manager = new QNetworkAccessManager(this);
connect(manager, &QNetworkAccessManager::finished, this, &DeckStatsInterface::queryFinished);
}
diff --git a/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp b/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp
index 34dd03765..30044c2b6 100644
--- a/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp
+++ b/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp
@@ -26,12 +26,11 @@
#include
#include
#include
-#include
#include
TappedOutInterface::TappedOutInterface(QObject *parent) : QObject(parent)
{
- manager = NetworkManagerFactory::create(this);
+ manager = new QNetworkAccessManager(this);
connect(manager, &QNetworkAccessManager::finished, this, &TappedOutInterface::queryFinished);
}
diff --git a/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.cpp b/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.cpp
index 62668c4d9..192029b23 100644
--- a/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.cpp
+++ b/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.cpp
@@ -27,7 +27,6 @@
#include
#include
#include
-#include
#include
#include
#include
@@ -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);
diff --git a/cockatrice/src/client/network/update/client/release_channel.cpp b/cockatrice/src/client/network/update/client/release_channel.cpp
index ce80a2a76..45c503623 100644
--- a/cockatrice/src/client/network/update/client/release_channel.cpp
+++ b/cockatrice/src/client/network/update/client/release_channel.cpp
@@ -20,7 +20,6 @@
#include
#include
#include
-#include
#include
#include
#include
@@ -63,7 +62,7 @@ std::optional getProductVersionMajor()
} // namespace
-ReleaseChannel::ReleaseChannel() : netMan(NetworkManagerFactory::create(this)), response(nullptr), lastRelease(nullptr)
+ReleaseChannel::ReleaseChannel() : netMan(new QNetworkAccessManager(this)), response(nullptr), lastRelease(nullptr)
{
}
diff --git a/cockatrice/src/client/network/update/client/update_downloader.cpp b/cockatrice/src/client/network/update/client/update_downloader.cpp
index 8868876fa..c86972d60 100644
--- a/cockatrice/src/client/network/update/client/update_downloader.cpp
+++ b/cockatrice/src/client/network/update/client/update_downloader.cpp
@@ -9,11 +9,10 @@
#include
#include
#include
-#include
UpdateDownloader::UpdateDownloader(QObject *parent) : QObject(parent), response(nullptr)
{
- netMan = NetworkManagerFactory::create(this);
+ netMan = new QNetworkAccessManager(this);
}
void UpdateDownloader::beginDownload(QUrl downloadUrl)
diff --git a/cockatrice/src/client/settings/cache_settings.h b/cockatrice/src/client/settings/cache_settings.h
index 8ccf6f76b..a6610ee0d 100644
--- a/cockatrice/src/client/settings/cache_settings.h
+++ b/cockatrice/src/client/settings/cache_settings.h
@@ -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
diff --git a/libcockatrice_game/libcockatrice/game/translation.h b/cockatrice/src/client/translation.h
similarity index 100%
rename from libcockatrice_game/libcockatrice/game/translation.h
rename to cockatrice/src/client/translation.h
diff --git a/cockatrice/src/database/interface/settings_card_preference_provider.h b/cockatrice/src/database/interface/settings_card_preference_provider.h
index f4bf23141..1feb2006b 100644
--- a/cockatrice/src/database/interface/settings_card_preference_provider.h
+++ b/cockatrice/src/database/interface/settings_card_preference_provider.h
@@ -2,18 +2,13 @@
#define COCKATRICE_SETTINGS_CARD_PREFERENCE_PROVIDER_H
#include "../../client/settings/cache_settings.h"
-#include
#include
#include
#include
-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);
diff --git a/cockatrice/src/filters/deck_filter_string.cpp b/cockatrice/src/filters/deck_filter_string.cpp
index f0df897fc..616311750 100644
--- a/cockatrice/src/filters/deck_filter_string.cpp
+++ b/cockatrice/src/filters/deck_filter_string.cpp
@@ -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
#include
#include
-#include
#include
#include
#include
diff --git a/cockatrice/src/filters/deck_filter_string.h b/cockatrice/src/filters/deck_filter_string.h
index b27cf87ca..76b891b59 100644
--- a/cockatrice/src/filters/deck_filter_string.h
+++ b/cockatrice/src/filters/deck_filter_string.h
@@ -11,7 +11,6 @@
#include
#include
#include
-#include
struct CardSearchLanguage;
struct LoadedDeck;
diff --git a/cockatrice/src/filters/filter_builder.cpp b/cockatrice/src/filters/filter_builder.cpp
index 9c2a91a5b..e785d7f1a 100644
--- a/cockatrice/src/filters/filter_builder.cpp
+++ b/cockatrice/src/filters/filter_builder.cpp
@@ -13,7 +13,6 @@
#include
#include
#include
-#include
#include
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 *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);
diff --git a/cockatrice/src/filters/filter_tree_model.cpp b/cockatrice/src/filters/filter_tree_model.cpp
index 9b55ce2fd..b3bf307f1 100644
--- a/cockatrice/src/filters/filter_tree_model.cpp
+++ b/cockatrice/src/filters/filter_tree_model.cpp
@@ -11,27 +11,6 @@
class QObject;
-namespace
-{
-/**
- * @brief Visits every filter item below @p node, depth first.
- */
-template void forEachFilter(const FilterTreeNode *node, const Callback &callback)
-{
- if (node == nullptr) {
- return;
- }
-
- if (const auto *filterItem = dynamic_cast(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 FilterTreeModel::getFiltersOfType(CardFilter::Attr filterType) const
{
QList 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 traverse;
+ traverse = [&](const FilterTreeNode *node) {
+ if (const auto *filterItem = dynamic_cast(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 FilterTreeModel::allFilters() const
{
QList filters;
- forEachFilter(fTree, [&](const FilterItem *filterItem) {
- filters.append(new CardFilter(filterItem->text(), filterItem->type(), filterItem->attr()));
- });
+ if (!fTree) {
+ return filters;
+ }
+
+ std::function traverse;
+ traverse = [&](const FilterTreeNode *node) {
+ if (const auto *filterItem = dynamic_cast(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;
}
diff --git a/cockatrice/src/filters/filter_tree_model.h b/cockatrice/src/filters/filter_tree_model.h
index 05f34f508..826fcc941 100644
--- a/cockatrice/src/filters/filter_tree_model.h
+++ b/cockatrice/src/filters/filter_tree_model.h
@@ -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 getFiltersOfType(CardFilter::Attr filterType) const;
- /// @brief Copies every active filter into new CardFilter objects. The caller owns them.
[[nodiscard]] QList 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;
diff --git a/libcockatrice_game/libcockatrice/game/abstract_game.cpp b/cockatrice/src/game/abstract_game.cpp
similarity index 88%
rename from libcockatrice_game/libcockatrice/game/abstract_game.cpp
rename to cockatrice/src/game/abstract_game.cpp
index a93f44018..efc575eff 100644
--- a/libcockatrice_game/libcockatrice/game/abstract_game.cpp
+++ b/cockatrice/src/game/abstract_game.cpp
@@ -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) {
diff --git a/libcockatrice_game/libcockatrice/game/abstract_game.h b/cockatrice/src/game/abstract_game.h
similarity index 82%
rename from libcockatrice_game/libcockatrice/game/abstract_game.h
rename to cockatrice/src/game/abstract_game.h
index 00a95a3b8..326af8ac9 100644
--- a/libcockatrice_game/libcockatrice/game/abstract_game.h
+++ b/cockatrice/src/game/abstract_game.h
@@ -11,14 +11,14 @@
#include
#include
+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
diff --git a/cockatrice/src/game_graphics/board/arrow_registry.cpp b/cockatrice/src/game/arrow_registry.cpp
similarity index 93%
rename from cockatrice/src/game_graphics/board/arrow_registry.cpp
rename to cockatrice/src/game/arrow_registry.cpp
index 10b3cde2f..c05134a75 100644
--- a/cockatrice/src/game_graphics/board/arrow_registry.cpp
+++ b/cockatrice/src/game/arrow_registry.cpp
@@ -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
-#include
void ArrowRegistry::insert(QSharedPointer data, ArrowItem *arrow)
{
diff --git a/cockatrice/src/game_graphics/board/arrow_registry.h b/cockatrice/src/game/arrow_registry.h
similarity index 94%
rename from cockatrice/src/game_graphics/board/arrow_registry.h
rename to cockatrice/src/game/arrow_registry.h
index 7103747e8..ca3b04090 100644
--- a/cockatrice/src/game_graphics/board/arrow_registry.h
+++ b/cockatrice/src/game/arrow_registry.h
@@ -4,7 +4,6 @@
#include
#include
#include
-#include
class ArrowItem;
struct ArrowData;
diff --git a/libcockatrice_game/libcockatrice/game/board/arrow_data.cpp b/cockatrice/src/game/board/arrow_data.cpp
similarity index 100%
rename from libcockatrice_game/libcockatrice/game/board/arrow_data.cpp
rename to cockatrice/src/game/board/arrow_data.cpp
diff --git a/libcockatrice_game/libcockatrice/game/board/arrow_data.h b/cockatrice/src/game/board/arrow_data.h
similarity index 100%
rename from libcockatrice_game/libcockatrice/game/board/arrow_data.h
rename to cockatrice/src/game/board/arrow_data.h
diff --git a/libcockatrice_game/libcockatrice/game/board/card_list.cpp b/cockatrice/src/game/board/card_list.cpp
similarity index 70%
rename from libcockatrice_game/libcockatrice/game/board/card_list.cpp
rename to cockatrice/src/game/board/card_list.cpp
index 361c51502..94bae67cd 100644
--- a/libcockatrice_game/libcockatrice/game/board/card_list.cpp
+++ b/cockatrice/src/game/board/card_list.cpp
@@ -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
#include
@@ -10,26 +11,26 @@
#include
#include
-CardList::CardList(bool _contentsKnown) : QList(), contentsKnown(_contentsKnown)
+CardList::CardList(bool _contentsKnown) : QList(), 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 &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 CardList::getExtractorFor(SortOption option)
+std::function 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 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 ""; };
}
\ No newline at end of file
diff --git a/libcockatrice_game/libcockatrice/game/board/card_list.h b/cockatrice/src/game/board/card_list.h
similarity index 84%
rename from libcockatrice_game/libcockatrice/game/board/card_list.h
rename to cockatrice/src/game/board/card_list.h
index b3037d0f6..b5427df0d 100644
--- a/libcockatrice_game/libcockatrice/game/board/card_list.h
+++ b/cockatrice/src/game/board/card_list.h
@@ -14,9 +14,9 @@
inline Q_LOGGING_CATEGORY(CardListLog, "card_list");
-class CardState;
+class CardItem;
-class CardList : public QList
+class CardList : public QList
{
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 &options);
- static std::function getExtractorFor(SortOption option);
+ static std::function getExtractorFor(SortOption option);
};
#endif
diff --git a/cockatrice/src/game/board/card_state.cpp b/cockatrice/src/game/board/card_state.cpp
new file mode 100644
index 000000000..3f879a849
--- /dev/null
+++ b/cockatrice/src/game/board/card_state.cpp
@@ -0,0 +1,113 @@
+#include "card_state.h"
+
+#include
+
+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();
+}
\ No newline at end of file
diff --git a/cockatrice/src/game/board/card_state.h b/cockatrice/src/game/board/card_state.h
new file mode 100644
index 000000000..ca7aaf927
--- /dev/null
+++ b/cockatrice/src/game/board/card_state.h
@@ -0,0 +1,106 @@
+#ifndef COCKATRICE_CARD_STATE_H
+#define COCKATRICE_CARD_STATE_H
+
+#include
+#include
+#include
+#include
+
+class CardZoneLogic;
+class CardItem;
+
+class CardState : public QObject
+{
+ Q_OBJECT
+
+private:
+ bool attacking = false;
+ QMap 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 &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 &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
diff --git a/libcockatrice_game/libcockatrice/game/board/counter_state.cpp b/cockatrice/src/game/board/counter_state.cpp
similarity index 100%
rename from libcockatrice_game/libcockatrice/game/board/counter_state.cpp
rename to cockatrice/src/game/board/counter_state.cpp
diff --git a/libcockatrice_game/libcockatrice/game/board/counter_state.h b/cockatrice/src/game/board/counter_state.h
similarity index 100%
rename from libcockatrice_game/libcockatrice/game/board/counter_state.h
rename to cockatrice/src/game/board/counter_state.h
diff --git a/libcockatrice_game/libcockatrice/game/game.cpp b/cockatrice/src/game/game.cpp
similarity index 75%
rename from libcockatrice_game/libcockatrice/game/game.cpp
rename to cockatrice/src/game/game.cpp
index a1a4a31a8..7343d052a 100644
--- a/libcockatrice_game/libcockatrice/game/game.cpp
+++ b/cockatrice/src/game/game.cpp
@@ -16,16 +16,13 @@ Game::Game(QObject *_parent,
bool isLocalGame,
QList &_clients,
const Event_GameJoined &event,
- const QMap &_roomGameTypes,
- IInterfaceSettingsProvider *interfaceSettings,
- ICardPreferenceProvider *cardPreferences)
+ const QMap &_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);
}
diff --git a/libcockatrice_game/libcockatrice/game/game.h b/cockatrice/src/game/game.h
similarity index 69%
rename from libcockatrice_game/libcockatrice/game/game.h
rename to cockatrice/src/game/game.h
index f80b75ea5..6a309cfb2 100644
--- a/libcockatrice_game/libcockatrice/game/game.h
+++ b/cockatrice/src/game/game.h
@@ -18,9 +18,6 @@ class QString;
template class QMap;
template class QList;
-class ICardPreferenceProvider;
-class IInterfaceSettingsProvider;
-
class Game : public AbstractGame
{
Q_OBJECT
@@ -30,9 +27,7 @@ public:
bool isLocalGame,
QList &_clients,
const Event_GameJoined &event,
- const QMap &_roomGameTypes,
- IInterfaceSettingsProvider *interfaceSettings = nullptr,
- ICardPreferenceProvider *cardPreferences = nullptr);
+ const QMap &_roomGameTypes);
};
#endif // COCKATRICE_GAME_H
diff --git a/libcockatrice_game/libcockatrice/game/game_event_handler.cpp b/cockatrice/src/game/game_event_handler.cpp
similarity index 89%
rename from libcockatrice_game/libcockatrice/game/game_event_handler.cpp
rename to cockatrice/src/game/game_event_handler.cpp
index 856475dfa..7b08af47c 100644
--- a/libcockatrice_game/libcockatrice/game/game_event_handler.cpp
+++ b/cockatrice/src/game/game_event_handler.cpp
@@ -28,20 +28,12 @@
#include
#include
#include
-#include
#include
-#include
#include
#include
#include
-#include
#include
#include
-#include
-#include
-#include
-#include
-#include
#include
#include
#include
@@ -56,7 +48,6 @@
#include
#include
#include
-#include
#include
#include
@@ -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 &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());
diff --git a/libcockatrice_game/libcockatrice/game/game_event_handler.h b/cockatrice/src/game/game_event_handler.h
similarity index 83%
rename from libcockatrice_game/libcockatrice/game/game_event_handler.h
rename to cockatrice/src/game/game_event_handler.h
index f7b11780b..30021d295 100644
--- a/libcockatrice_game/libcockatrice/game/game_event_handler.h
+++ b/cockatrice/src/game/game_event_handler.h
@@ -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
-#include
#include
#include
-#include
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 &plan, int playerId);
-
/** @} */
/** @name Incoming event processing
diff --git a/libcockatrice_game/libcockatrice/game/game_meta_info.cpp b/cockatrice/src/game/game_meta_info.cpp
similarity index 100%
rename from libcockatrice_game/libcockatrice/game/game_meta_info.cpp
rename to cockatrice/src/game/game_meta_info.cpp
diff --git a/libcockatrice_game/libcockatrice/game/game_meta_info.h b/cockatrice/src/game/game_meta_info.h
similarity index 100%
rename from libcockatrice_game/libcockatrice/game/game_meta_info.h
rename to cockatrice/src/game/game_meta_info.h
diff --git a/libcockatrice_game/libcockatrice/game/game_state.cpp b/cockatrice/src/game/game_state.cpp
similarity index 100%
rename from libcockatrice_game/libcockatrice/game/game_state.cpp
rename to cockatrice/src/game/game_state.cpp
diff --git a/libcockatrice_game/libcockatrice/game/game_state.h b/cockatrice/src/game/game_state.h
similarity index 100%
rename from libcockatrice_game/libcockatrice/game/game_state.h
rename to cockatrice/src/game/game_state.h
diff --git a/libcockatrice_game/libcockatrice/game/phase.cpp b/cockatrice/src/game/phase.cpp
similarity index 100%
rename from libcockatrice_game/libcockatrice/game/phase.cpp
rename to cockatrice/src/game/phase.cpp
diff --git a/libcockatrice_game/libcockatrice/game/phase.h b/cockatrice/src/game/phase.h
similarity index 100%
rename from libcockatrice_game/libcockatrice/game/phase.h
rename to cockatrice/src/game/phase.h
diff --git a/libcockatrice_game/libcockatrice/game/player/event_processing_options.h b/cockatrice/src/game/player/event_processing_options.h
similarity index 100%
rename from libcockatrice_game/libcockatrice/game/player/event_processing_options.h
rename to cockatrice/src/game/player/event_processing_options.h
diff --git a/libcockatrice_game/libcockatrice/game/player/player_actions.cpp b/cockatrice/src/game/player/player_actions.cpp
similarity index 84%
rename from libcockatrice_game/libcockatrice/game/player/player_actions.cpp
rename to cockatrice/src/game/player/player_actions.cpp
index 90823fec6..35682ede8 100644
--- a/libcockatrice_game/libcockatrice/game/player/player_actions.cpp
+++ b/cockatrice/src/game/player/player_actions.cpp
@@ -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
#include
#include
+#include
+#include
#include
#include
#include
@@ -44,15 +51,10 @@
#include
#include
#include
-#include
-#include
-#include
#include
#include
-#include
#include
#include
-#include
#include
#include
#include
@@ -62,12 +64,11 @@
#include
#include
#include
-#include
#include
#include
-#include
+#include
+#include
#include
-#include
#include
#include
#include
@@ -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 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(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 selectedCards, int number)
+void PlayerActions::actMoveCardXCardsFromTop(QList selectedCards, int number)
{
defaultNumberTopCardsToPlaceBelow = number;
@@ -1318,14 +1303,14 @@ void PlayerActions::actMoveCardXCardsFromTop(QList selectedCards, i
}
}
-void PlayerActions::actIncPT(QList selectedCards, int deltaP, int deltaT)
+void PlayerActions::actIncPT(QList selectedCards, int deltaP, int deltaT)
{
int playerid = player->getPlayerInfo()->getId();
QList 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 selectedCards, int deltaP, int d
player->getGame()->getGameEventHandler()->sendGameCommand(prepareGameCommand(commandList), playerid);
}
-void PlayerActions::actResetPT(QList selectedCards)
+void PlayerActions::actResetPT(QList selectedCards)
{
int playerid = player->getPlayerInfo()->getId();
QList commandList;
@@ -1384,7 +1369,7 @@ void PlayerActions::actResetPT(QList selectedCards)
}
}
-void PlayerActions::actRequestSetPTDialog(QList selectedCards)
+void PlayerActions::actRequestSetPTDialog(QList selectedCards)
{
QString oldPT;
@@ -1397,11 +1382,11 @@ void PlayerActions::actRequestSetPTDialog(QList selectedCards)
emit requestSetPTDialog(oldPT);
}
-void PlayerActions::actSetPT(QList selectedCards, const QString &pt)
+void PlayerActions::actSetPT(QList 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 commandList;
@@ -1409,7 +1394,7 @@ void PlayerActions::actSetPT(QList 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 selectedCards, const QString &pt
player->getGame()->getGameEventHandler()->sendGameCommand(prepareGameCommand(commandList), playerid);
}
-void PlayerActions::actIncP(QList selectedCards)
+void PlayerActions::actDrawArrow()
+{
+ auto *card = player->getGame()->getActiveCard();
+ if (card) {
+ card->drawArrow(Qt::red);
+ }
+}
+
+void PlayerActions::actIncP(QList selectedCards)
{
actIncPT(selectedCards, 1, 0);
}
-void PlayerActions::actDecP(QList selectedCards)
+void PlayerActions::actDecP(QList selectedCards)
{
actIncPT(selectedCards, -1, 0);
}
-void PlayerActions::actIncT(QList selectedCards)
+void PlayerActions::actIncT(QList selectedCards)
{
actIncPT(selectedCards, 0, 1);
}
-void PlayerActions::actDecT(QList selectedCards)
+void PlayerActions::actDecT(QList selectedCards)
{
actIncPT(selectedCards, 0, -1);
}
-void PlayerActions::actIncPT(QList selectedCards)
+void PlayerActions::actIncPT(QList selectedCards)
{
actIncPT(selectedCards, 1, 1);
}
-void PlayerActions::actDecPT(QList selectedCards)
+void PlayerActions::actDecPT(QList selectedCards)
{
actIncPT(selectedCards, -1, -1);
}
-void PlayerActions::actFlowP(QList selectedCards)
+void PlayerActions::actFlowP(QList selectedCards)
{
actIncPT(selectedCards, 1, -1);
}
-void PlayerActions::actFlowT(QList selectedCards)
+void PlayerActions::actFlowT(QList 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 selectedCards)
+void PlayerActions::actReduceLifeByPower(QList selectedCards)
{
// find life counter
auto lifeCounter = player->getLifeCounter();
@@ -1506,7 +1481,7 @@ void PlayerActions::actReduceLifeByPower(QList 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 selectedCards)
sendGameCommand(prepareGameCommand(cmd));
}
-void PlayerActions::actRequestSetAnnotationDialog(QList 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 selectedCards)
{
QString oldAnnotation;
for (auto card : selectedCards) {
@@ -1532,7 +1517,7 @@ void PlayerActions::actRequestSetAnnotationDialog(QList selectedCar
emit requestSetAnnotationDialog(oldAnnotation);
}
-void PlayerActions::actSetAnnotation(QList selectedCards, const QString &annotation)
+void PlayerActions::actSetAnnotation(QList selectedCards, const QString &annotation)
{
QList commandList;
for (auto card : selectedCards) {
@@ -1546,7 +1531,17 @@ void PlayerActions::actSetAnnotation(QList selectedCards, const QSt
sendGameCommand(prepareGameCommand(commandList));
}
-void PlayerActions::actUnattach(QList selectedCards)
+void PlayerActions::actAttach()
+{
+ auto *card = player->getGame()->getActiveCard();
+ if (!card) {
+ return;
+ }
+
+ card->drawAttachArrow();
+}
+
+void PlayerActions::actUnattach(QList selectedCards)
{
QList commandList;
for (auto card : selectedCards) {
@@ -1562,17 +1557,17 @@ void PlayerActions::actUnattach(QList selectedCards)
sendGameCommand(prepareGameCommand(commandList));
}
-void PlayerActions::actAddCardCounter(QList selectedCards, int counterId)
+void PlayerActions::actAddCardCounter(QList selectedCards, int counterId)
{
offsetCardCounter(selectedCards, counterId, 1);
}
-void PlayerActions::actRemoveCardCounter(QList selectedCards, int counterId)
+void PlayerActions::actRemoveCardCounter(QList selectedCards, int counterId)
{
offsetCardCounter(selectedCards, counterId, -1);
}
-void PlayerActions::offsetCardCounter(QList selectedCards, int counterId, int offset)
+void PlayerActions::offsetCardCounter(QList selectedCards, int counterId, int offset)
{
QList commandList;
for (auto card : selectedCards) {
@@ -1598,7 +1593,7 @@ void PlayerActions::offsetCardCounter(QList selectedCards, int coun
sendGameCommand(prepareGameCommand(commandList));
}
-void PlayerActions::actRequestSetCardCounterDialog(QList selectedCards, int counterId)
+void PlayerActions::actRequestSetCardCounterDialog(QList 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 selectedCa
emit requestSetCardCounterDialog(counterId, oldValueForDlg);
}
-void PlayerActions::actSetCardCounter(QList selectedCards, int counterId, const QString &counterValue)
+void PlayerActions::actSetCardCounter(QList selectedCards, int counterId, const QString &counterValue)
{
QList