diff --git a/.ci/compile.sh b/.ci/compile.sh index bf5a169af..d60683b6e 100755 --- a/.ci/compile.sh +++ b/.ci/compile.sh @@ -474,10 +474,6 @@ if [[ $RUNNER_OS == Windows && ! $MAKE_NO_CLIENT ]]; then # 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" @@ -504,149 +500,22 @@ function Invoke-Probe([string] $guardDir) { 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 + $locked = 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)" - } - + if ($locked.Code -ne 1) { throw "expected exit 1 for a locked file, got $($locked.Code) : $($locked.Output)" } + if ($locked.Output -notmatch "pid\s+$PID\b") { throw "the lookup did not name this process: $($locked.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" + Write-Host "holder lookup named pid $PID for a locked file and named nothing for an empty directory" } finally { Remove-Item -Recurse -Force $dir -ErrorAction SilentlyContinue } diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index 54068f5a0..c97b48342 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -584,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 d9acabc5e..41fe393e7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -250,15 +250,6 @@ message(STATUS "Update Translations: ${UPDATE_TRANSLATIONS}") include(FindQtRuntime) -# Reject APIs that were deprecated before the oldest Qt release we support -# (6.4). Qt 6.5 renamed the switch, so use whichever spelling the found -# version understands while still compiling everywhere. -if(Qt6_VERSION VERSION_GREATER_EQUAL 6.5) - add_compile_definitions(QT_DISABLE_DEPRECATED_UP_TO=0x060400) -else() - add_compile_definitions(QT_DISABLE_DEPRECATED_BEFORE=0x060400) -endif() - set(CMAKE_AUTOMOC TRUE) # Find other needed libraries @@ -429,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) @@ -462,5 +450,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/cmake/NSIS.template.in b/cmake/NSIS.template.in index 923aded9d..ae156313d 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,14 @@ 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 +99,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 +111,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 +153,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 +219,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 +260,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 +295,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 +325,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 +349,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 +364,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 +372,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 +673,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 +899,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 +1004,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 +1048,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,92 +1176,6 @@ 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 - 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. ; @@ -1831,18 +1215,14 @@ FunctionEnd ; 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. +; The probe opens each survivor for DELETE access and then closes it again +; without deleting anything. The code is read by the plugin right after that call +; (?e), so a failure reports what Windows refused; clearing LastError first is +; what makes a zero code mean "this open worked", i.e. the purge-time failure was +; transient (an antivirus scan that has finished, an instance still shutting +; down) rather than a standing condition. The plugin parses the call's arguments +; between the clear and the call, so a non-zero code is reported as what Windows +; said rather than as proof of a cause - the wording below never claims one. Function LogStaleRuntimeDiagnostics FindFirst $StaleFindHandle $StaleFindName "$INSTDIR\*.dll" ${If} $StaleFindHandle != "" @@ -1859,52 +1239,21 @@ Function LogStaleRuntimeDiagnostics ${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} + ; A valid handle: close it. The open proves the file is not held + ; open, but not that Delete would succeed: a read-only file opens + ; for DELETE and still fails DeleteFile until the attribute is + ; cleared, which the purge does not do. Report that standing + ; cause instead of calling the failure transient. 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 + ${If} $R3 = -1 + StrCpy $R0 "a delete-access open now succeeds, but its attributes could not be read" + ${Else} IntOp $R3 $R3 & 1 ${If} $R3 = 1 - StrCpy $R0 "$R0; it is read-only again" + StrCpy $R0 "a delete-access open now succeeds, but it is still read-only, which Delete does not clear" + ${Else} + StrCpy $R0 "a delete-access open now succeeds" ${EndIf} ${EndIf} ${Else} @@ -1922,35 +1271,45 @@ Function LogStaleRuntimeDiagnostics 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. +; Names the applications and services that still have the install directory's files +; 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. +; The lookup is a small executable built from tools/installer-holder-probe, extracted to +; $PLUGINSDIR only when the purge has already failed: the successful path pays nothing +; for it, and $PLUGINSDIR takes it away again when the installer exits. It is compiled by +; the same Windows build that compiles this installer, which is what lets it call the +; Restart Manager API directly. The same call made from this script would have to +; marshal RM_PROCESS_INFO through System::Call as a table of hand-computed offsets, and +; that table is precisely what cannot be checked without a Windows toolchain; in +; compiled code the layout belongs to the Windows SDK header. ; -; $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. +; Restart Manager is what makes the answer complete. Walking processes can only name one +; whose image lives here or that loaded one of these libraries, while Restart Manager +; names a process that holds a plain handle on a file - an antivirus scanner - and names +; services as well as user programs. Its blind spot is a handle taken in kernel mode, +; where there is no process to name; then the codes logged above are the whole story, and +; the probe says so by naming nothing rather than by failing. +; +; $INSTDIR travels through the same COCKATRICE_GUARD_DIR environment variable the +; running-instance 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 + InitPluginsDir + ; Embedded by the same relative path that resolves NSIS.definitions.nsh above (both + ; are built into the build root, which is three levels up from project.nsi). + File "/oname=$PLUGINSDIR\installer-holder-probe.exe" "..\..\..\installer-holder-probe.exe" + System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$INSTDIR")' + ; /TIMEOUT because this runs on the abort path, ahead of the message box: one call + ; into the Restart Manager cannot take the minute a walk over every process can, but + ; if that service stalls it must not leave the installer hanging with nothing on + ; screen. On a timeout nsExec reports it through the exit code like any other failure. + nsExec::ExecToStack /TIMEOUT=20000 '"$PLUGINSDIR\installer-holder-probe.exe"' + Pop $HolderExit + Pop $HolderOutput ; 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 @@ -1958,99 +1317,149 @@ Function LogProcessesUsingInstallDir ${If} $HolderExit == "1" StrCpy $HolderFound 1 ${ElseIf} $HolderExit == "0" - StrCpy $HolderOutput "nothing is holding them and no payload program is running from the directory" + StrCpy $HolderOutput "nothing is holding them any more" ${Else} - StrCpy $HolderOutput "the holder probe could not name what holds them: $HolderOutput" + StrCpy $HolderOutput "the holder probe could not run: $HolderOutput" ${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" +; 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} - ; 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 + ; 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} + ; 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 + +Function un.IsProcessLookupAvailable + nsExec::ExecToLog 'powershell -NoProfile -Command "exit 0"' + Pop $R0 + ${If} $R0 == "0" + StrCpy $R0 1 + ${Else} + StrCpy $R0 0 + ${EndIf} +FunctionEnd + +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} - ; 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 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 +1467,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 +1545,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 +1562,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 +1579,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 +1588,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 +1631,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 +1654,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" @@ -2376,13 +1685,25 @@ ${EndIf} ; 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" + ; RMDir /r reports a failure only through the error flag, and only as a + ; boolean; the surviving file names come from the listing below. + ${If} ${FileExists} "$INSTDIR\Plugins\*.*" + !insertmacro CockLog "purge: '$INSTDIR\Plugins' could not be removed completely" + ${EndIf} + Delete "$INSTDIR\*.dll" !insertmacro CockLogListing "after-purge" "*.dll" !insertmacro CockLogListing "after-plugins" "Plugins\*.*" ${EndIf} @@ -2397,77 +1718,28 @@ ${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. + ; Who is holding the payload is recorded before the abort, so the log that + ; reaches us answers the question the message can only guess at, and so the + ; message can name the processes to close whenever any were found. + Call LogStaleRuntimeDiagnostics + ; 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." + "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." ${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" + StrCpy $AbortReason "$AbortReason$\r$\n$\r$\nStill using the install directory:$\r$\n$HolderOutput" ${EndIf} 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 ce7ad5712..bec858dcb 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 @@ -322,7 +345,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 @@ -428,7 +450,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 @@ -490,7 +511,7 @@ if(APPLE) set(cockatrice_SOURCES ${cockatrice_SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/resources/appicon.icns) endif(APPLE) -qt_add_resources(cockatrice_RESOURCES_RCC ${cockatrice_RESOURCES}) +qt6_add_resources(cockatrice_RESOURCES_RCC ${cockatrice_RESOURCES}) # Declare path variables set(ICONDIR @@ -511,7 +532,7 @@ set(COCKATRICE_MAC_QM_INSTALL_DIR "cockatrice.app/Contents/Resources/translation set(COCKATRICE_UNIX_QM_INSTALL_DIR "share/cockatrice/translations") set(COCKATRICE_WIN32_QM_INSTALL_DIR "translations") -qt_add_executable( +qt6_add_executable( cockatrice WIN32 MACOSX_BUNDLE @@ -523,7 +544,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 @@ -535,7 +556,7 @@ qt_add_shaders( src/interface/widgets/onboarding/shaders/brand_plate.frag ) -qt_add_resources( +qt6_add_resources( cockatrice "onboarding_qml" PREFIX @@ -551,7 +572,6 @@ target_link_libraries( PUBLIC libcockatrice_card libcockatrice_deck_list libcockatrice_filters - libcockatrice_game libcockatrice_utility libcockatrice_network libcockatrice_models @@ -768,7 +788,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} @@ -778,7 +798,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) @@ -804,4 +824,4 @@ if(Qt6LinguistTools_FOUND) endforeach() endif() -qt_finalize_target(cockatrice) +qt6_finalize_target(cockatrice) 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 020e53877..e23e91b46 100644 --- a/cockatrice/src/client/network/connection_controller/remote_connection_controller.cpp +++ b/cockatrice/src/client/network/connection_controller/remote_connection_controller.cpp @@ -244,27 +244,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; } @@ -527,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 c8f96a9a9..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,7 +87,7 @@ private slots: // Forgot-password flow void onForgotPasswordSuccess(); - void onForgotPasswordError(int responseCode); + void onForgotPasswordError(); void onPromptForgotPasswordReset(); void onPromptForgotPasswordChallenge(); diff --git a/cockatrice/src/client/settings/cache_settings.h b/cockatrice/src/client/settings/cache_settings.h index 9aeb88a18..5f699247d 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/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 commandList; for (auto card : selectedCards) { @@ -1631,13 +1626,11 @@ void PlayerActions::actSetCardCounter(QList selectedCards, int coun sendGameCommand(prepareGameCommand(commandList)); } -void PlayerActions::actIncrementAllCardCounters(QList cardsToUpdate) +void PlayerActions::actIncrementAllCardCounters(QList cardsToUpdate) { if (cardsToUpdate.isEmpty()) { // If no cards selected, update all cards on table - for (auto *state : player->getTableZone()->getCards()) { - cardsToUpdate.append(state); - } + cardsToUpdate = static_cast>(player->getTableZone()->getCards()); } QList commandList; @@ -1680,7 +1673,7 @@ static bool isUnwritableRevealZone(CardZoneLogic *zone) return false; } -void PlayerActions::playSelectedCards(QList selectedCards, const bool faceDown) +void PlayerActions::playSelectedCards(QList selectedCards, const bool faceDown) { // CardIds will get shuffled downwards when cards leave the deck. // We need to iterate through the cards in reverse order so cardIds don't get changed out from under us as we play @@ -1695,26 +1688,27 @@ void PlayerActions::playSelectedCards(QList selectedCards, const bo } } -void PlayerActions::actPlay(QList selectedCards) +void PlayerActions::actPlay(QList selectedCards) { playSelectedCards(selectedCards, false); } -void PlayerActions::actPlayFacedown(QList selectedCards) +void PlayerActions::actPlayFacedown(QList selectedCards) { playSelectedCards(selectedCards, true); } -void PlayerActions::actHide(QList selectedCards) +void PlayerActions::actHide(QList selectedCards) { - for (const auto &card : selectedCards) { - if (isUnwritableRevealZone(card->getZone())) { + for (const auto &item : selectedCards) { + auto *card = static_cast(item); + if (card && isUnwritableRevealZone(card->getZone())) { card->getZone()->removeCard(card); } } } -void PlayerActions::actReveal(QList selectedCards, QAction *action) +void PlayerActions::actReveal(QList selectedCards, QAction *action) { const int otherPlayerId = action->data().toInt(); @@ -1805,9 +1799,9 @@ void PlayerActions::actRevealRandomGraveyardCard(int revealToPlayerId) sendGameCommand(cmd); } -void PlayerActions::cardMenuAction(QList selectedCards, CardMenuActionType type) +void PlayerActions::cardMenuAction(QList selectedCards, CardMenuActionType type) { - QList cardList = selectedCards; + QList cardList = selectedCards; QList commandList; if (type <= cmClone) { @@ -1997,7 +1991,7 @@ void PlayerActions::cardMenuAction(QList selectedCards, CardMenuAct ctm->set_tapped(info.getUiAttributes().cipt); } - cmd->set_y(TableZoneLogic::tableRowToGridY(tableRow)); + cmd->set_y(TableZone::tableRowToGridY(tableRow)); commandList.append(cmd); } break; @@ -2046,76 +2040,6 @@ PendingCommand *PlayerActions::prepareGameCommand(const QList &cards) -{ - if (!startZone || !targetZone) { - return; - } - - Command_MoveCard cmd; - cmd.set_start_player_id(startZone->getPlayer()->getPlayerInfo()->getId()); - cmd.set_start_zone(startZone->getName().toStdString()); - cmd.set_target_player_id(targetZone->getPlayer()->getPlayerInfo()->getId()); - cmd.set_target_zone(targetZone->getName().toStdString()); - cmd.set_x(x); - cmd.set_y(y); - - if (auto *viewLogic = qobject_cast(targetZone)) { - cmd.set_is_reversed(viewLogic->getIsReversed()); - } - - for (const CardMoveRequest &card : cards) { - auto *cardToMove = cmd.mutable_cards_to_move()->add_card(); - cardToMove->set_card_id(card.cardId); - if (card.faceDown) { - cardToMove->set_face_down(true); - } - if (!card.pt.isEmpty()) { - cardToMove->set_pt(card.pt.toStdString()); - } - } - - sendGameCommand(cmd); -} - -PendingCommand *PlayerActions::prepareZoneDump(CardZoneLogic *zone, int numberCards) -{ - if (!zone) { - return nullptr; - } - - Command_DumpZone cmd; - cmd.set_player_id(zone->getPlayer()->getPlayerInfo()->getId()); - cmd.set_zone_name(zone->getName().toStdString()); - cmd.set_number_cards(numberCards); - if (auto *viewLogic = qobject_cast(zone)) { - cmd.set_is_reversed(viewLogic->getIsReversed()); - } - - return prepareGameCommand(cmd); -} - -void PlayerActions::actToggleTapped(const QString &zoneName, const QList &cards, bool tapped) -{ - QList cmdList; - for (CardState *card : cards) { - if (card->getTapped() != tapped) { - auto *cmd = new Command_SetCardAttr; - cmd->set_zone(zoneName.toStdString()); - cmd->set_card_id(card->getId()); - cmd->set_attribute(AttrTapped); - cmd->set_attr_value(tapped ? "1" : "0"); - cmdList.append(cmd); - } - } - - sendGameCommand(prepareGameCommand(cmdList)); -} - void PlayerActions::sendGameCommand(const google::protobuf::Message &command) { if (player->getPlayerInfo()->getJudge() && !player->getPlayerInfo()->getLocal()) { diff --git a/libcockatrice_game/libcockatrice/game/player/player_actions.h b/cockatrice/src/game/player/player_actions.h similarity index 54% rename from libcockatrice_game/libcockatrice/game/player/player_actions.h rename to cockatrice/src/game/player/player_actions.h index 7ed52b9a1..8fff81635 100644 --- a/libcockatrice_game/libcockatrice/game/player/player_actions.h +++ b/cockatrice/src/game/player/player_actions.h @@ -8,13 +8,12 @@ #ifndef COCKATRICE_PLAYER_ACTIONS_H #define COCKATRICE_PLAYER_ACTIONS_H +#include "../../game_graphics/board/card_item.h" // IWYU pragma: keep - moc instantiates the metatype for QList, which Qt 6.4 needs complete +#include "../../game_graphics/dialogs/dlg_create_token.h" +#include "../../game_graphics/dialogs/dlg_move_top_cards_until.h" +#include "../../game_graphics/player/card_menu_action_type.h" #include "../board/card_list.h" -#include "../board/card_state.h" -#include "card_menu_action_type.h" -#include "card_move_request.h" #include "libcockatrice/card/card_info.h" -#include "move_top_cards_until_options.h" -#include "token_info.h" #include #include @@ -23,6 +22,7 @@ #include #include +class CardItem; class CardRelation; class QAction; class QTimer; @@ -36,16 +36,10 @@ class Message; } } // namespace google -class CardState; -class CardZoneLogic; class Command_MoveCard; -class GameEventContext; -class ICardPreferenceProvider; -class IInterfaceSettingsProvider; class PendingCommand; class PlayerLogic; -class QAction; -struct ArrowData; + class PlayerActions : public QObject { Q_OBJECT @@ -56,9 +50,7 @@ public: RANDOM_CARD_FROM_ZONE = -2 }; - explicit PlayerActions(PlayerLogic *player, - IInterfaceSettingsProvider *interfaceSettings = nullptr, - ICardPreferenceProvider *cardPreferences = nullptr); + explicit PlayerActions(PlayerLogic *player); void sendGameCommand(PendingCommand *pend); void sendGameCommand(const google::protobuf::Message &command); @@ -66,52 +58,7 @@ public: PendingCommand *prepareGameCommand(const ::google::protobuf::Message &cmd); PendingCommand *prepareGameCommand(const QList &cmdList); - /** - * @brief Moves the requested cards between zones on behalf of a view. - * @param startZone Zone the cards are dragged out of. - * @param targetZone Zone the cards are dropped onto. - * @param x Horizontal target index within the zone. - * @param y Vertical target index within the zone. - * @param cards The cards to move, as chosen by the view. - */ - void - moveCards(CardZoneLogic *startZone, CardZoneLogic *targetZone, int x, int y, const QList &cards); - - /** - * @brief Prepares a dump of the given zone, for the caller to connect to and send. - * @param zone Zone to dump. - * @param numberCards How many cards to dump, -1 for all. - */ - PendingCommand *prepareZoneDump(CardZoneLogic *zone, int numberCards); - - /** - * @brief Sets the tapped state of the cards that do not already have it. - * @param zoneName Zone the cards belong to. - * @param cards Cards the view wants toggled. - * @param tapped Desired tapped state. - */ - void actToggleTapped(const QString &zoneName, const QList &cards, bool tapped); - - /** - * @brief Attaches one card to another on the table on behalf of a view. - * @param startCard Card being attached. - * @param targetCard Card being attached to. - */ - void attachCards(CardState *startCard, const CardState *targetCard); - - /** - * @brief Draws an arrow from a card to another card or a player on behalf of a view. - * - * The view resolves the endpoints before playing the card out of hand, so the - * arrow still records the zone the card started in. - * - * @param startCard Card the arrow starts from. - * @param arrow Arrow endpoints and color, as resolved by the view. - * @param deleteInPhase Phase the arrow expires in, 0 to keep it until removed. - */ - void createArrow(CardState *startCard, const ArrowData &arrow, int deleteInPhase); - - void moveOneCardUntil(CardState *card); + void moveOneCardUntil(CardItem *card); void stopMoveTopCardsUntil(); [[nodiscard]] bool isMovingCardsUntil() const @@ -140,7 +87,7 @@ signals: void requestDrawBottomCardsDialog(int defaultNumberBottomCards, int maxCards); void requestRollDieDialog(); void requestCreateTokenDialog(const QStringList &predefinedTokens); - void requestCreateRelatedFromRelationDialog(const CardState *sourceCard, const CardRelation *cardRelation); + void requestCreateRelatedFromRelationDialog(const CardItem *sourceCard, const CardRelation *cardRelation); void requestMoveCardXCardsFromTopDialog(int defaultNumberTopCardsToPlaceBelow, int deckSize); void requestSetPTDialog(const QString &oldPT); void requestSetAnnotationDialog(const QString &oldAnnotation); @@ -149,18 +96,12 @@ signals: void requestSortHand(const QList &options); void requestEnableAndSetCreateAnotherTokenAction(const QString &lastTokenName); void requestSetLastToken(CardInfoPtr lastToken); - /** - * @brief The player chose a mulligan size to remember as the starting hand size. - * - * Game logic must not write client settings itself, so persistence is left to the application. - */ - void startingHandSizeChosen(int number); public slots: void setLastToken(CardInfoPtr cardInfo); void setLastTokenInfo(CardInfoPtr cardInfo); - void playCard(CardState *c, bool faceDown); - void playCardToTable(const CardState *c, bool faceDown); + void playCard(CardItem *c, bool faceDown); + void playCardToTable(const CardItem *c, bool faceDown); void actUntapAll(); void actRequestRollDieDialog(); @@ -169,9 +110,9 @@ public slots: void actRequestCreateTokenDialog(const QStringList &predefinedTokens); void actCreateToken(TokenInfo tokenToCreate); void actCreateAnotherToken(); - void actRequestCreateRelatedFromRelationDialog(const CardState *sourceCard, const CardRelation *cardRelation); - bool createRelatedFromRelation(const CardState *sourceCard, const CardRelation *cardRelation, int variableCount); - void onRelatedCardCreated(const CardState *sourceCard, const CardRelation *cardRelation); + void actRequestCreateRelatedFromRelationDialog(const CardItem *sourceCard, const CardRelation *cardRelation); + bool createRelatedFromRelation(const CardItem *sourceCard, const CardRelation *cardRelation, int variableCount); + void onRelatedCardCreated(const CardItem *sourceCard, const CardRelation *cardRelation); void setLastRelatedCreationSucceeded(bool succeeded) { lastRelatedCreationSucceeded = succeeded; @@ -191,9 +132,9 @@ public slots: void actMulliganMinusOne(); void doMulligan(int number); - void actPlay(QList selectedCards); - void actPlayFacedown(QList selectedCards); - void actHide(QList selectedCards); + void actPlay(QList selectedCards); + void actPlayFacedown(QList selectedCards); + void actHide(QList selectedCards); void actMoveTopCardToPlay(); void actMoveTopCardToPlayFaceDown(); @@ -223,6 +164,10 @@ public slots: void actRequestMoveBottomCardsToDialog(const QString &targetZone, const QString &zoneDisplayName, bool faceDown); void moveBottomCardsTo(int number, const QString &targetZone, bool faceDown); + void actSelectAll(); + void actSelectRow(); + void actSelectColumn(); + void actViewLibrary(); void actViewHand(); void actRequestViewTopCardsDialog(); @@ -242,55 +187,47 @@ public slots: void actOpenDeckInDeckEditor(); void actCreatePredefinedToken(); - void actCreateRelatedCard(const CardState *sourceCard, int index); - void actCreateAllRelatedCards(const CardState *sourceCard); + void actCreateRelatedCard(); + void actCreateAllRelatedCards(); void actRequestMoveCardXCardsFromTopDialog(); - void actMoveCardXCardsFromTop(QList selectedCards, int number); - void actRemoveCardCounter(QList selectedCards, int counterId); - void actAddCardCounter(QList selectedCards, int counterId); - void actRequestSetCardCounterDialog(QList selectedCards, int counterId); - void actSetCardCounter(QList selectedCards, int counterId, const QString &counterValue); - void actIncrementAllCardCounters(QList cardsToUpdate); - void actUnattach(QList selectedCards); - void actIncPT(QList selectedCards, int deltaP, int deltaT); - void actResetPT(QList selectedCards); - void actRequestSetPTDialog(QList selectedCards); - void actSetPT(QList selectedCards, const QString &pt); - void actIncP(QList selectedCards); - void actDecP(QList selectedCards); - void actIncT(QList selectedCards); - void actDecT(QList selectedCards); - void actIncPT(QList selectedCards); - void actDecPT(QList selectedCards); - void actFlowP(QList selectedCards); - void actFlowT(QList selectedCards); + void actMoveCardXCardsFromTop(QList selectedCards, int number); + void actRemoveCardCounter(QList selectedCards, int counterId); + void actAddCardCounter(QList selectedCards, int counterId); + void actRequestSetCardCounterDialog(QList selectedCards, int counterId); + void actSetCardCounter(QList selectedCards, int counterId, const QString &counterValue); + void actIncrementAllCardCounters(QList cardsToUpdate); + void actAttach(); + void actUnattach(QList selectedCards); + void actDrawArrow(); + void actIncPT(QList selectedCards, int deltaP, int deltaT); + void actResetPT(QList selectedCards); + void actRequestSetPTDialog(QList selectedCards); + void actSetPT(QList selectedCards, const QString &pt); + void actIncP(QList selectedCards); + void actDecP(QList selectedCards); + void actIncT(QList selectedCards); + void actDecT(QList selectedCards); + void actIncPT(QList selectedCards); + void actDecPT(QList selectedCards); + void actFlowP(QList selectedCards); + void actFlowT(QList selectedCards); - /** @brief Adjusts a player counter by a signed delta. */ - void actIncrementCounter(int counterId, int delta); + void actReduceLifeByPower(QList selectedCards); - /** @brief Sets a player counter to an absolute value. */ - void actSetCounter(int counterId, int value); - - void actReduceLifeByPower(QList selectedCards); - - void actRequestSetAnnotationDialog(QList selectedCards); - void actSetAnnotation(QList selectedCards, const QString &annotation); - void actReveal(QList selectedCards, QAction *action); + void actRequestSetAnnotationDialog(QList selectedCards); + void actSetAnnotation(QList selectedCards, const QString &annotation); + void actReveal(QList selectedCards, QAction *action); void actRevealHand(int revealToPlayerId); void actRevealRandomHandCard(int revealToPlayerId); void actRevealLibrary(int revealToPlayerId); void actSortHand(); - void cardMenuAction(QList selectedCards, CardMenuActionType type); + void cardMenuAction(QList selectedCards, CardMenuActionType type); private: PlayerLogic *player; - // Borrowed settings providers (owned by the application). Null outside the client, in which case the compiled-in - // defaults below apply. - IInterfaceSettingsProvider *interfaceSettings; - ICardPreferenceProvider *cardPreferences; int defaultNumberTopCards = 1; int defaultNumberTopCardsToPlaceBelow = 1; @@ -308,23 +245,18 @@ private: bool lastRelatedCreationSucceeded = false; - [[nodiscard]] bool getPlayToStack() const; - [[nodiscard]] int getStartingHandSize() const; - [[nodiscard]] bool getAnnotateTokens() const; - [[nodiscard]] QString getCardPreferenceOverride(const QString &cardName) const; - - void createCard(const CardState *sourceCard, + void createCard(const CardItem *sourceCard, const QString &dbCardName, CardRelationType attach = CardRelationType::DoesNotAttach, bool persistent = false, bool faceDown = false); - void playSelectedCards(QList selectedCards, bool faceDown = false); + void playSelectedCards(QList selectedCards, bool faceDown = false); void cmdSetTopCard(Command_MoveCard &cmd); void cmdSetBottomCard(Command_MoveCard &cmd); - void offsetCardCounter(QList selectedCards, int counterId, int offset); + void offsetCardCounter(QList selectedCards, int counterId, int offset); }; #endif // COCKATRICE_PLAYER_ACTIONS_H diff --git a/libcockatrice_game/libcockatrice/game/player/player_event_handler.cpp b/cockatrice/src/game/player/player_event_handler.cpp similarity index 88% rename from libcockatrice_game/libcockatrice/game/player/player_event_handler.cpp rename to cockatrice/src/game/player/player_event_handler.cpp index 6588547da..18f6573df 100644 --- a/libcockatrice_game/libcockatrice/game/player/player_event_handler.cpp +++ b/cockatrice/src/game/player/player_event_handler.cpp @@ -1,14 +1,14 @@ #include "player_event_handler.h" +#include "../../game_graphics/board/card_item.h" +#include "../../game_graphics/zones/view_zone.h" #include "../abstract_game.h" #include "../board/arrow_data.h" #include "../board/card_list.h" -#include "../board/card_state.h" #include "../board/counter_state.h" #include "../zones/card_zone_logic.h" #include "../zones/hand_zone_logic.h" #include "../zones/pile_zone_logic.h" -#include "../zones/view_zone_logic.h" #include "libcockatrice/card/card_info.h" #include "libcockatrice/card/printing/exact_card.h" #include "libcockatrice/protocol/pb/card_attributes.pb.h" @@ -27,9 +27,6 @@ #include #include #include -#include -#include -#include #include #include #include @@ -52,7 +49,6 @@ #include #include #include -#include #include #include #include @@ -80,12 +76,12 @@ void PlayerEventHandler::eventShuffle(const Event_Shuffle &event) } // close all views that contain shuffled cards - for (auto *viewLogic : zone->getViews()) { - if (viewLogic != nullptr) { - int length = viewLogic->getCards().length(); + for (ZoneViewZone *view : zone->getViews()) { + if (view != nullptr) { + int length = view->getLogic()->getCards().length(); // we want to close empty views as well if (length == 0 || length > absStart) { // note this assumes views always start at the top of the library - viewLogic->requestClose(); + view->close(); } } else { qWarning() << zone->getName() << "of" << player->getPlayerInfo()->getName() << "holds empty zoneview!"; @@ -159,29 +155,23 @@ void PlayerEventHandler::eventCreateToken(const Event_CreateToken &event) } CardRef cardRef = {QString::fromStdString(event.card_name()), QString::fromStdString(event.card_provider_id())}; + CardItem *card = new CardItem(player, nullptr, cardRef, event.card_id()); // use db PT if not provided in event and not face-down - QString pt = QString::fromStdString(event.pt()); - if (pt.isEmpty() && !event.face_down()) { - ExactCard dbCard = CardDatabaseManager::query()->getCard(cardRef); + if (!QString::fromStdString(event.pt()).isEmpty()) { + card->setPT(QString::fromStdString(event.pt())); + } else if (!event.face_down()) { + ExactCard dbCard = card->getCard(); if (dbCard) { - pt = dbCard.getInfo().getPowTough(); + card->setPT(dbCard.getInfo().getPowTough()); } } + card->setColor(QString::fromStdString(event.color())); + card->setAnnotation(QString::fromStdString(event.annotation())); + card->setDestroyOnZoneChange(event.destroy_on_zone_change()); + card->setFaceDown(event.face_down()); - emit logCreateToken(player, cardRef.name, pt, event.face_down()); - - ServerInfo_Card cardInfo; - cardInfo.set_id(event.card_id()); - cardInfo.set_name(event.card_name()); - cardInfo.set_provider_id(event.card_provider_id()); - cardInfo.set_pt(pt.toStdString()); - cardInfo.set_color(event.color()); - cardInfo.set_annotation(event.annotation()); - cardInfo.set_destroy_on_zone_change(event.destroy_on_zone_change()); - cardInfo.set_face_down(event.face_down()); - cardInfo.set_x(event.x()); - cardInfo.set_y(event.y()); - emit zone->createCardRequested(cardInfo, true); + emit logCreateToken(player, card->getName(), card->getPT(), card->getFaceDown()); + zone->addCard(card, true, event.x(), event.y()); } void PlayerEventHandler::eventSetCardAttr(const Event_SetCardAttr &event, @@ -203,7 +193,7 @@ void PlayerEventHandler::eventSetCardAttr(const Event_SetCardAttr &event, emit logSetTapped(player, nullptr, event.attr_value() == "1"); } } else { - CardState *card = zone->getCard(event.card_id()); + CardItem *card = zone->getCard(event.card_id()); if (!card) { qWarning() << "PlayerEventHandler::eventSetCardAttr: card id=" << event.card_id() << "not found"; return; @@ -213,7 +203,7 @@ void PlayerEventHandler::eventSetCardAttr(const Event_SetCardAttr &event, } void PlayerEventHandler::setCardAttrHelper(const GameEventContext &context, - CardState *card, + CardItem *card, CardAttribute attribute, const QString &avalue, bool allCards, @@ -274,7 +264,7 @@ void PlayerEventHandler::eventSetCardCounter(const Event_SetCardCounter &event) return; } - CardState *card = zone->getCard(event.card_id()); + CardItem *card = zone->getCard(event.card_id()); if (!card) { return; } @@ -351,13 +341,13 @@ void PlayerEventHandler::eventMoveCard(const Event_MoveCard &event, const GameEv if (x == -1) { x = 0; } - CardState *card = startZone->takeCard(position, event.card_id(), startZone != targetZone); + CardItem *card = startZone->takeCard(position, event.card_id(), startZone != targetZone); if (card == nullptr) { return; } - const bool zoneChanged = startZone != targetZone; - const bool ownerChanged = zoneChanged && startZone->getPlayer() != targetZone->getPlayer(); - emit cardViewRefreshRequested(card, zoneChanged, ownerChanged ? targetZone->getPlayer() : nullptr); + if (startZone != targetZone) { + card->deleteCardInfoPopup(); + } if (event.has_card_name()) { QString name = QString::fromStdString(event.card_name()); QString providerId = @@ -366,19 +356,27 @@ void PlayerEventHandler::eventMoveCard(const Event_MoveCard &event, const GameEv } if (card->getAttachedTo() && (startZone != targetZone)) { - CardState *parentCard = card->getAttachedTo(); - // CardState::setAttachedTo removes the card from the old parent's attachedCards list and emits the zone events. + CardItem *parentCard = card->getAttachedTo(); card->setAttachedTo(nullptr); parentCard->getZone()->reorganizeCards(); } + card->deleteDragItem(); + card->setId(event.new_card_id()); card->setFaceDown(event.face_down()); if (startZone != targetZone) { - const QList &attachedCards = card->getAttachedCards(); + card->setBeingPointedAt(false); + card->setHovered(false); + + const QList &attachedCards = card->getAttachedCards(); for (auto attachedCard : attachedCards) { emit targetZone->cardAdded(attachedCard); } + + if (startZone->getPlayer() != targetZone->getPlayer()) { + card->setOwner(targetZone->getPlayer()); + } } // The log event has to be sent before the card is added to the target zone @@ -406,7 +404,7 @@ void PlayerEventHandler::eventFlipCard(const Event_FlipCard &event) if (!zone) { return; } - CardState *card = zone->getCard(event.card_id()); + CardItem *card = zone->getCard(event.card_id()); if (!card) { return; } @@ -429,28 +427,20 @@ void PlayerEventHandler::eventDestroyCard(const Event_DestroyCard &event) return; } - CardState *card = zone->getCard(event.card_id()); + CardItem *card = zone->getCard(event.card_id()); if (!card) { return; } - QList attachedCards = card->getAttachedCards(); + QList attachedCards = card->getAttachedCards(); // This list is always empty except for buggy server implementations. for (auto &attachedCard : attachedCards) { - card->removeAttachedCard(attachedCard); - attachedCard->setGridPoint(QPoint(-1, attachedCard->getGridPoint().y())); attachedCard->setAttachedTo(nullptr); - if (attachedCard->getZone() == nullptr) { - attachedCard->deleteView(); - } else { - emit attachedCard->getZone()->cardAdded(attachedCard); - attachedCard->getZone()->reorganizeCards(); - } } emit logDestroyCard(player, card->getName()); zone->takeCard(-1, event.card_id(), true); - card->deleteView(); + card->deleteLater(); } void PlayerEventHandler::eventAttachCard(const Event_AttachCard &event) @@ -458,7 +448,7 @@ void PlayerEventHandler::eventAttachCard(const Event_AttachCard &event) const QMap &playerList = player->getGame()->getPlayerManager()->getPlayers(); PlayerLogic *targetPlayer = nullptr; CardZoneLogic *targetZone = nullptr; - CardState *targetCard = nullptr; + CardItem *targetCard = nullptr; if (event.has_target_player_id()) { targetPlayer = playerList.value(event.target_player_id(), 0); if (targetPlayer) { @@ -474,12 +464,12 @@ void PlayerEventHandler::eventAttachCard(const Event_AttachCard &event) return; } - CardState *startCard = startZone->getCard(event.card_id()); + CardItem *startCard = startZone->getCard(event.card_id()); if (!startCard) { return; } - CardState *oldParent = startCard->getAttachedTo(); + CardItem *oldParent = startCard->getAttachedTo(); startCard->setAttachedTo(targetCard); @@ -508,7 +498,7 @@ void PlayerEventHandler::eventDrawCards(const Event_DrawCards &event) if (listSize) { for (int i = 0; i < listSize; ++i) { const ServerInfo_Card &cardInfo = event.cards(i); - CardState *card = _deck->takeCard(0, cardInfo.id()); + CardItem *card = _deck->takeCard(0, cardInfo.id()); QString cardName = QString::fromStdString(cardInfo.name()); QString providerId = QString::fromStdString(cardInfo.provider_id()); card->setCardRef({cardName, providerId}); @@ -556,7 +546,7 @@ void PlayerEventHandler::eventRevealCards(const Event_RevealCards &event, EventP for (const auto &card : cardList) { QString cardName = QString::fromStdString(card->name()); QString providerId = QString::fromStdString(card->provider_id()); - CardState *cardItem = zone->getCard(card->id()); + CardItem *cardItem = zone->getCard(card->id()); if (!cardItem) { continue; } diff --git a/libcockatrice_game/libcockatrice/game/player/player_event_handler.h b/cockatrice/src/game/player/player_event_handler.h similarity index 89% rename from libcockatrice_game/libcockatrice/game/player/player_event_handler.h rename to cockatrice/src/game/player/player_event_handler.h index d253deef0..88254119b 100644 --- a/libcockatrice_game/libcockatrice/game/player/player_event_handler.h +++ b/cockatrice/src/game/player/player_event_handler.h @@ -27,7 +27,7 @@ #include #include -class CardState; +class CardItem; class CardZoneLogic; class PlayerLogic; class Event_AttachCard; @@ -61,7 +61,7 @@ template class QList; * Design notes: * - All event handlers assume events are authoritative and already validated * by the server. - * - Most handlers mutate both logical state (CardState, CardZoneLogic, counters) + * - Most handlers mutate both logical state (CardItem, CardZoneLogic, counters) * and visual/UI state (views, arrows, menus). * - Logging signals are emitted *after* or *during* state mutation, depending * on whether later mutations would invalidate log data. @@ -238,7 +238,7 @@ signals: void logUndoDraw(PlayerLogic *player, QString cardName); void logUndoDrawFailed(PlayerLogic *player); void logMoveCard(PlayerLogic *player, - CardState *card, + CardItem *card, CardZoneLogic *startZone, int oldX, CardZoneLogic *targetZone, @@ -248,11 +248,11 @@ signals: void logAttachCard(PlayerLogic *player, QString cardName, PlayerLogic *targetPlayer, QString targetCardName); void logUnattachCard(PlayerLogic *player, QString cardName); void logSetCardCounter(PlayerLogic *player, QString cardName, int counterId, int value, int oldValue); - void logSetTapped(PlayerLogic *player, CardState *card, bool tapped); + void logSetTapped(PlayerLogic *player, CardItem *card, bool tapped); void logSetCounter(PlayerLogic *player, QString counterName, int value, int oldValue); - void logSetDoesntUntap(PlayerLogic *player, CardState *card, bool doesntUntap); - void logSetPT(PlayerLogic *player, CardState *card, QString newPT); - void logSetAnnotation(PlayerLogic *player, CardState *card, QString newAnnotation); + void logSetDoesntUntap(PlayerLogic *player, CardItem *card, bool doesntUntap); + void logSetPT(PlayerLogic *player, CardItem *card, QString newPT); + void logSetAnnotation(PlayerLogic *player, CardItem *card, QString newAnnotation); void logDumpZone(PlayerLogic *player, CardZoneLogic *zone, int numberCards, bool isReversed = false); void logRevealCards(PlayerLogic *player, CardZoneLogic *zone, @@ -266,23 +266,15 @@ signals: void logAlwaysLookAtTopCard(PlayerLogic *player, CardZoneLogic *zone, bool reveal); /** @} */ - void cardZoneChanged(CardState *card, bool sameZone); - void requestCardMenuUpdate(CardState *card); - /** - * @brief A card was just moved by the server and its view needs refreshing. - * - * @param card The card that moved. - * @param zoneChanged Whether the card crossed a zone boundary. - * @param newOwner The player the card now belongs to, or nullptr to leave the owner alone. - */ - void cardViewRefreshRequested(CardState *card, bool zoneChanged, PlayerLogic *newOwner); + void cardZoneChanged(CardItem *card, bool sameZone); + void requestCardMenuUpdate(const CardItem *card); private: /** Owning player instance. */ PlayerLogic *player; void setCardAttrHelper(const GameEventContext &context, - CardState *card, + CardItem *card, CardAttribute attribute, const QString &avalue, bool allCards, diff --git a/libcockatrice_game/libcockatrice/game/player/player_info.cpp b/cockatrice/src/game/player/player_info.cpp similarity index 100% rename from libcockatrice_game/libcockatrice/game/player/player_info.cpp rename to cockatrice/src/game/player/player_info.cpp diff --git a/libcockatrice_game/libcockatrice/game/player/player_info.h b/cockatrice/src/game/player/player_info.h similarity index 100% rename from libcockatrice_game/libcockatrice/game/player/player_info.h rename to cockatrice/src/game/player/player_info.h diff --git a/libcockatrice_game/libcockatrice/game/player/player_logic.cpp b/cockatrice/src/game/player/player_logic.cpp similarity index 89% rename from libcockatrice_game/libcockatrice/game/player/player_logic.cpp rename to cockatrice/src/game/player/player_logic.cpp index 96bd07164..b545b7088 100644 --- a/libcockatrice_game/libcockatrice/game/player/player_logic.cpp +++ b/cockatrice/src/game/player/player_logic.cpp @@ -1,9 +1,8 @@ #include "player_logic.h" +#include "../../game_graphics/board/card_item.h" #include "../abstract_game.h" #include "../board/arrow_data.h" -#include "../board/card_list.h" -#include "../board/card_state.h" #include "../board/counter_state.h" #include "../game_meta_info.h" #include "../zones/hand_zone_logic.h" @@ -34,17 +33,10 @@ #include #include -PlayerLogic::PlayerLogic(const ServerInfo_User &info, - int _id, - bool _local, - bool _judge, - AbstractGame *_parent, - IInterfaceSettingsProvider *interfaceSettings, - ICardPreferenceProvider *cardPreferences) +PlayerLogic::PlayerLogic(const ServerInfo_User &info, int _id, bool _local, bool _judge, AbstractGame *_parent) : QObject(_parent), game(_parent), playerInfo(new PlayerInfo(info, _id, _local, _judge)), - playerEventHandler(new PlayerEventHandler(this)), - playerActions(new PlayerActions(this, interfaceSettings, cardPreferences)), active(false), conceded(false), - zoneId(0), dialogSemaphore(false) + playerEventHandler(new PlayerEventHandler(this)), playerActions(new PlayerActions(this)), active(false), + conceded(false), zoneId(0), dialogSemaphore(false) { initializeZones(); } @@ -180,20 +172,22 @@ void PlayerLogic::processPlayerInfo(const ServerInfo_Player &info) const int cardListSize = zoneInfo.card_list_size(); if (!cardListSize) { for (int j = 0; j < zoneInfo.card_count(); ++j) { - emit zone->createCardRequested(ServerInfo_Card(), false); + zone->addCard(new CardItem(this), false, -1); } } else { for (int j = 0; j < cardListSize; ++j) { - ServerInfo_Card cardInfo = zoneInfo.card_list(j); + const ServerInfo_Card &cardInfo = zoneInfo.card_list(j); + auto *card = new CardItem(this); + card->processCardInfo(cardInfo); // Zones without coordinates (hand, piles, stack) preserve the order // they arrive in on the server in the positions of their cards list. // The x coordinate of such cards is always 0, so inserting at it // would reverse the list on reconnect. Append instead. - if (!zoneInfo.with_coords()) { - cardInfo.set_x(-1); - cardInfo.set_y(-1); + if (zoneInfo.with_coords()) { + zone->addCard(card, false, cardInfo.x(), cardInfo.y()); + } else { + zone->addCard(card, false, -1); } - emit zone->createCardRequested(cardInfo, false); } } if (zoneInfo.has_always_reveal_top_card()) { @@ -225,8 +219,8 @@ void PlayerLogic::processCardAttachment(const ServerInfo_Player &info) for (int j = 0; j < cardListSize; ++j) { const ServerInfo_Card &cardInfo = zoneInfo.card_list(j); if (cardInfo.has_attach_player_id()) { - CardState *startCard = zone->getCard(cardInfo.id()); - CardState *targetCard = + CardItem *startCard = zone->getCard(cardInfo.id()); + CardItem *targetCard = game->getCard(cardInfo.attach_player_id(), QString::fromStdString(cardInfo.attach_zone()), cardInfo.attach_card_id()); if (!targetCard) { @@ -245,19 +239,19 @@ void PlayerLogic::processCardAttachment(const ServerInfo_Player &info) } } -void PlayerLogic::addCard(AbstractCardItem *card) +void PlayerLogic::addCard(CardItem *card) { emit newCardAdded(card); } -void PlayerLogic::deleteCard(CardState *card) +void PlayerLogic::deleteCard(CardItem *card) { if (card == nullptr) { return; } else if (dialogSemaphore) { cardsToDelete.append(card); } else { - card->deleteView(); + card->deleteLater(); } } @@ -338,7 +332,7 @@ bool PlayerLogic::clearCardsToDelete() for (auto &i : cardsToDelete) { if (i != nullptr) { - i->deleteView(); + i->deleteLater(); } } cardsToDelete.clear(); diff --git a/libcockatrice_game/libcockatrice/game/player/player_logic.h b/cockatrice/src/game/player/player_logic.h similarity index 88% rename from libcockatrice_game/libcockatrice/game/player/player_logic.h rename to cockatrice/src/game/player/player_logic.h index d6c4f5187..59da7cc32 100644 --- a/libcockatrice_game/libcockatrice/game/player/player_logic.h +++ b/cockatrice/src/game/player/player_logic.h @@ -7,8 +7,6 @@ #ifndef PLAYER_H #define PLAYER_H -#include "../board/arrow_data.h" -#include "../board/counter_state.h" #include "../zones/card_zone_logic.h" #include "../zones/hand_zone_logic.h" #include "../zones/pile_zone_logic.h" @@ -19,16 +17,12 @@ #include "libcockatrice/utility/playmat_params.h" #include +#include #include #include #include #include #include -#include -#include -#include -#include -#include #include #include @@ -37,6 +31,7 @@ // signatures, which the moc-generated code needs to be complete. #include +class CardItem; class CounterState; class QWidget; class ServerInfo_PlayerProperties; @@ -47,8 +42,6 @@ template class QSharedPointer; inline Q_LOGGING_CATEGORY(PlayerLog, "player"); class AbstractCardItem; class AbstractGame; -class ICardPreferenceProvider; -class IInterfaceSettingsProvider; class PlayerInfo; class PlayerEventHandler; class PlayerActions; @@ -74,7 +67,7 @@ signals: /** @brief Emitted when the remote playmat (card/params) is updated from player properties. */ void playmatChanged(); void newCardAdded(AbstractCardItem *card); - void requestCardMenuUpdate(CardState *card); + void requestCardMenuUpdate(const CardItem *card); void counterAdded(CounterState *state); void counterRemoved(int counterId); void rearrangeCounters(); @@ -94,13 +87,7 @@ public slots: void onRequestZoneViewToggle(const QString &zoneName, int numberCards, bool isReversed); public: - PlayerLogic(const ServerInfo_User &info, - int _id, - bool _local, - bool _judge, - AbstractGame *_parent, - IInterfaceSettingsProvider *interfaceSettings = nullptr, - ICardPreferenceProvider *cardPreferences = nullptr); + PlayerLogic(const ServerInfo_User &info, int _id, bool _local, bool _judge, AbstractGame *_parent); ~PlayerLogic() override; void initializeZones(); @@ -110,8 +97,8 @@ public: void processPlayerInfo(const ServerInfo_Player &info); void processCardAttachment(const ServerInfo_Player &info); - void addCard(AbstractCardItem *c); - void deleteCard(CardState *c); + void addCard(CardItem *c); + void deleteCard(CardItem *c); bool clearCardsToDelete(); @@ -263,7 +250,7 @@ private: QMap counters; bool dialogSemaphore; - QList cardsToDelete; + QList cardsToDelete; // Playmat from player properties (for opponent display) CardRef remotePlaymatCard; @@ -271,4 +258,15 @@ private: bool hasRemotePlaymat = false; }; +class AnnotationDialog : public QInputDialog +{ + Q_OBJECT + void keyPressEvent(QKeyEvent *e) override; + +public: + explicit AnnotationDialog(QWidget *parent = nullptr) : QInputDialog(parent) + { + } +}; + #endif diff --git a/libcockatrice_game/libcockatrice/game/player/player_manager.cpp b/cockatrice/src/game/player/player_manager.cpp similarity index 86% rename from libcockatrice_game/libcockatrice/game/player/player_manager.cpp rename to cockatrice/src/game/player/player_manager.cpp index 284d21f68..4730805ad 100644 --- a/libcockatrice_game/libcockatrice/game/player/player_manager.cpp +++ b/cockatrice/src/game/player/player_manager.cpp @@ -12,12 +12,9 @@ PlayerManager::PlayerManager(AbstractGame *_game, int _localPlayerId, bool _localPlayerIsJudge, - bool localPlayerIsSpectator, - IInterfaceSettingsProvider *_interfaceSettings, - ICardPreferenceProvider *_cardPreferences) + bool localPlayerIsSpectator) : QObject(_game), game(_game), players(QMap()), localPlayerId(_localPlayerId), - localPlayerIsJudge(_localPlayerIsJudge), localPlayerIsSpectator(localPlayerIsSpectator), - interfaceSettings(_interfaceSettings), cardPreferences(_cardPreferences) + localPlayerIsJudge(_localPlayerIsJudge), localPlayerIsSpectator(localPlayerIsSpectator) { } @@ -55,7 +52,7 @@ bool PlayerManager::isLocalPlayer(int playerId) PlayerLogic *PlayerManager::addPlayer(int playerId, const ServerInfo_User &info) { auto *newPlayer = new PlayerLogic(info, playerId, isLocalPlayer(playerId) || game->getGameState()->getIsLocalGame(), - isJudge(), getGame(), interfaceSettings, cardPreferences); + isJudge(), getGame()); connect(newPlayer, &PlayerLogic::concededChanged, this, &PlayerManager::onPlayerConceded); players.insert(playerId, newPlayer); emit playerAdded(newPlayer); diff --git a/libcockatrice_game/libcockatrice/game/player/player_manager.h b/cockatrice/src/game/player/player_manager.h similarity index 84% rename from libcockatrice_game/libcockatrice/game/player/player_manager.h rename to cockatrice/src/game/player/player_manager.h index c95bbbb96..7a9f7645b 100644 --- a/libcockatrice_game/libcockatrice/game/player/player_manager.h +++ b/cockatrice/src/game/player/player_manager.h @@ -16,8 +16,6 @@ #include class AbstractGame; -class ICardPreferenceProvider; -class IInterfaceSettingsProvider; class PlayerLogic; class PlayerManager : public QObject @@ -25,12 +23,7 @@ class PlayerManager : public QObject Q_OBJECT public: - PlayerManager(AbstractGame *_game, - int _localPlayerId, - bool _localPlayerIsJudge, - bool localPlayerIsSpectator, - IInterfaceSettingsProvider *interfaceSettings = nullptr, - ICardPreferenceProvider *cardPreferences = nullptr); + PlayerManager(AbstractGame *_game, int _localPlayerId, bool _localPlayerIsJudge, bool localPlayerIsSpectator); AbstractGame *game; QMap players; @@ -38,9 +31,6 @@ public: bool localPlayerIsJudge; bool localPlayerIsSpectator; QMap spectators; - // Borrowed settings providers forwarded to each PlayerLogic (owned by the application). - IInterfaceSettingsProvider *interfaceSettings; - ICardPreferenceProvider *cardPreferences; [[nodiscard]] bool isSpectator() const { diff --git a/libcockatrice_game/libcockatrice/game/replay.cpp b/cockatrice/src/game/replay.cpp similarity index 51% rename from libcockatrice_game/libcockatrice/game/replay.cpp rename to cockatrice/src/game/replay.cpp index 4d41a12ea..4ab4a6321 100644 --- a/libcockatrice_game/libcockatrice/game/replay.cpp +++ b/cockatrice/src/game/replay.cpp @@ -9,15 +9,10 @@ class GameReplay; class QObject; -Replay::Replay(QObject *_parent, - const GameReplay *_replay, - bool isLocalGame, - IInterfaceSettingsProvider *interfaceSettings, - ICardPreferenceProvider *cardPreferences) - : AbstractGame(_parent) +Replay::Replay(QObject *_parent, const GameReplay *_replay, bool isLocalGame) : AbstractGame(_parent) { gameState = new GameState(this, 0, -1, isLocalGame, {}, false, false, -1, false); connect(gameMetaInfo, &GameMetaInfo::startedChanged, gameState, &GameState::onStartedChanged); - playerManager = new PlayerManager(this, -1, false, true, interfaceSettings, cardPreferences); + playerManager = new PlayerManager(this, -1, false, true); loadReplay(_replay); } diff --git a/cockatrice/src/game/replay.h b/cockatrice/src/game/replay.h new file mode 100644 index 000000000..cc995dbac --- /dev/null +++ b/cockatrice/src/game/replay.h @@ -0,0 +1,26 @@ +/** + * @file replay.h + * @ingroup GameLogic + * @ingroup Replay + */ +//! \todo Document this file. + +#ifndef COCKATRICE_REPLAY_H +#define COCKATRICE_REPLAY_H + +#include "abstract_game.h" + +#include + +class GameReplay; +class QObject; + +class Replay : public AbstractGame +{ + Q_OBJECT + +public: + explicit Replay(QObject *_parent, const GameReplay *_replay, bool isLocalGame); +}; + +#endif // COCKATRICE_REPLAY_H diff --git a/libcockatrice_game/libcockatrice/game/zones/card_zone_algorithms.h b/cockatrice/src/game/zones/card_zone_algorithms.h similarity index 81% rename from libcockatrice_game/libcockatrice/game/zones/card_zone_algorithms.h rename to cockatrice/src/game/zones/card_zone_algorithms.h index 35e96b86c..118a7d29d 100644 --- a/libcockatrice_game/libcockatrice/game/zones/card_zone_algorithms.h +++ b/cockatrice/src/game/zones/card_zone_algorithms.h @@ -5,9 +5,9 @@ namespace CardZoneAlgorithms { /** - * Shared insertion logic for zones where cards follow the standard pattern: - * clamp index, insert, clear identity if contents unknown, reset state. - * Visibility and parenting are handled by the graphics zone's onCardAdded(). + * Shared insertion logic for zones where cards become visible on add and follow + * the standard pattern: clamp index, insert, clear identity if contents unknown, + * reset state, show card. * * Zones with different post-add behavior (signal connections, positional resets, * hidden cards, or coordinate-based placement) should NOT use this — implement @@ -18,7 +18,8 @@ namespace CardZoneAlgorithms * * @tparam CardList Must provide: size() -> int, insert(int, CardType*), * getContentsKnown() -> bool - * @tparam CardType Must provide: setId(int), setCardRef(CardRefType), resetState(bool) + * @tparam CardType Must provide: setId(int), setCardRef(CardRefType), + * resetState(bool), setVisible(bool) * @param keepAnnotations Forwarded to card->resetState(). Stack-like zones preserve * annotations across zone transitions; hand-like zones clear them. */ @@ -36,6 +37,7 @@ void addCardToList(CardList &cards, CardType *card, int x, bool keepAnnotations) } card->resetState(keepAnnotations); + card->setVisible(true); } } // namespace CardZoneAlgorithms diff --git a/libcockatrice_game/libcockatrice/game/zones/card_zone_logic.cpp b/cockatrice/src/game/zones/card_zone_logic.cpp similarity index 79% rename from libcockatrice_game/libcockatrice/game/zones/card_zone_logic.cpp rename to cockatrice/src/game/zones/card_zone_logic.cpp index a160dcc1d..d997ab9ad 100644 --- a/libcockatrice_game/libcockatrice/game/zones/card_zone_logic.cpp +++ b/cockatrice/src/game/zones/card_zone_logic.cpp @@ -1,11 +1,13 @@ #include "card_zone_logic.h" +#include "../../client/translation.h" +#include "../../game_graphics/board/card_item.h" +#include "../../game_graphics/zones/card_zone.h" +#include "../../game_graphics/zones/view_zone.h" #include "../board/card_list.h" -#include "../board/card_state.h" #include "../player/player_actions.h" #include "../player/player_info.h" #include "../player/player_logic.h" -#include "../translation.h" #include "libcockatrice/card/database/card_database.h" #include "view_zone_logic.h" @@ -42,24 +44,19 @@ CardZoneLogic::CardZoneLogic(PlayerLogic *_player, &CardZoneLogic::refreshCardInfos); } -void CardZoneLogic::addCard(CardState *card, const bool reorganize, const int x, const int y) +void CardZoneLogic::addCard(CardItem *card, const bool reorganize, const int x, const int y) { if (!card) { - qCWarning(CardZoneLogicLog) << "CardZoneLogic::addCard() card is null; this shouldn't normally happen"; + qCWarning(CardZoneLog) << "CardZoneLogic::addCard() card is null; this shouldn't normally happen"; return; } - for (auto *viewLogic : views) { - if (viewLogic->prepareAddCard(x)) { - ServerInfo_Card cardInfo; - const CardRef &cardRef = card->getCardRef(); - cardInfo.set_name(cardRef.name.toStdString()); - cardInfo.set_provider_id(cardRef.providerId.toStdString()); - cardInfo.set_id(card->getId()); - cardInfo.set_face_down(card->getFaceDown()); - cardInfo.set_x(x); - cardInfo.set_y(y); - emit viewLogic->createCardRequested(cardInfo, reorganize); + for (auto *view : views) { + if (qobject_cast(view->getLogic())->prepareAddCard(x)) { + auto copy = new CardItem(player, nullptr, card->getCardRef(), card->getId()); + copy->setFaceDown(card->getFaceDown()); + + view->getLogic()->addCard(copy, reorganize, x, y); } } @@ -74,7 +71,7 @@ void CardZoneLogic::addCard(CardState *card, const bool reorganize, const int x, emit cardCountChanged(); } -CardState *CardZoneLogic::takeCard(int position, int cardId, bool toNewZone) +CardItem *CardZoneLogic::takeCard(int position, int cardId, bool toNewZone) { if (position == -1) { // position == -1 means either that the zone is indexed by card id @@ -93,11 +90,11 @@ CardState *CardZoneLogic::takeCard(int position, int cardId, bool toNewZone) return nullptr; } - for (auto *viewLogic : views) { - viewLogic->removeCard(position, toNewZone); + for (auto *view : views) { + qobject_cast(view->getLogic())->removeCard(position, toNewZone); } - CardState *c = cards.takeAt(position); + CardItem *c = cards.takeAt(position); c->setId(cardId); @@ -106,11 +103,11 @@ CardState *CardZoneLogic::takeCard(int position, int cardId, bool toNewZone) return c; } -CardState *CardZoneLogic::getCard(int cardId) +CardItem *CardZoneLogic::getCard(int cardId) { - CardState *c = cards.findCard(cardId); + CardItem *c = cards.findCard(cardId); if (!c) { - qCWarning(CardZoneLogicLog) << "CardZoneLogic::getCard: card id=" << cardId << "not found"; + qCWarning(CardZoneLog) << "CardZoneLogic::getCard: card id=" << cardId << "not found"; return nullptr; } // If the card's id is -1, this zone is invisible, @@ -122,10 +119,10 @@ CardState *CardZoneLogic::getCard(int cardId) return c; } -void CardZoneLogic::removeCard(CardState *card) +void CardZoneLogic::removeCard(CardItem *card) { if (!card) { - qCWarning(CardZoneLogicLog) << "CardZoneLogic::removeCard: card is null, this shouldn't normally happen"; + qCWarning(CardZoneLog) << "CardZoneLogic::removeCard: card is null, this shouldn't normally happen"; return; } @@ -138,8 +135,8 @@ void CardZoneLogic::removeCard(CardState *card) void CardZoneLogic::refreshCardInfos() { - for (const auto &cardState : cards) { - cardState->refreshCardInfo(); + for (const auto &cardItem : cards) { + cardItem->refreshCardInfo(); } } @@ -171,17 +168,17 @@ void CardZoneLogic::clearContents() const CardList toClear = cards; // Detach and notify attached cards and zones *before* deleting anything. - for (CardState *card : toClear) { + for (CardItem *card : toClear) { // If an incorrectly implemented server doesn't return attached cards to whom they belong before dropping a // player, we have to return them to avoid a crash. - const QList &attachedCards = card->getAttachedCards(); - for (CardState *attachedCard : attachedCards) { + const QList &attachedCards = card->getAttachedCards(); + for (CardItem *attachedCard : attachedCards) { emit attachedCard->getZone()->cardAdded(attachedCard); } } // Now request deletions after all manipulations are done. - for (CardState *card : toClear) { + for (CardItem *card : toClear) { player->deleteCard(card); } diff --git a/libcockatrice_game/libcockatrice/game/zones/card_zone_logic.h b/cockatrice/src/game/zones/card_zone_logic.h similarity index 73% rename from libcockatrice_game/libcockatrice/game/zones/card_zone_logic.h rename to cockatrice/src/game/zones/card_zone_logic.h index d3cd9121f..2e232b258 100644 --- a/libcockatrice_game/libcockatrice/game/zones/card_zone_logic.h +++ b/cockatrice/src/game/zones/card_zone_logic.h @@ -7,30 +7,28 @@ #ifndef COCKATRICE_CARD_ZONE_LOGIC_H #define COCKATRICE_CARD_ZONE_LOGIC_H +#include "../../client/translation.h" #include "../board/card_list.h" -#include "../translation.h" #include #include #include #include -#include #include +class CardItem; + inline Q_LOGGING_CATEGORY(CardZoneLogicLog, "card_zone_logic"); class PlayerLogic; -class ZoneViewZoneLogic; -class CardDragItem; +class ZoneViewZone; class CardZoneLogic : public QObject { Q_OBJECT signals: - void cardAdded(CardState *addedCard); - /** @brief Asks the graphics side to build the card item for @p cardInfo and add it to this zone. */ - void createCardRequested(const ServerInfo_Card &cardInfo, bool reorganize); + void cardAdded(CardItem *addedCard); void cardCountChanged(); void reorganizeCards(); void updateGraphics(); @@ -45,17 +43,16 @@ public: bool _contentsKnown, QObject *parent = nullptr); - void addCard(CardState *card, bool reorganize, int x, int y = -1); + void addCard(CardItem *card, bool reorganize, int x, int y = -1); // getCard() finds a card by id. - CardState *getCard(int cardId); - void removeCard(CardState *card); + CardItem *getCard(int cardId); + void removeCard(CardItem *card); // takeCard() finds a card by position and removes it from the zone and from all of its views. - virtual CardState *takeCard(int position, int cardId, bool canResize = true); + virtual CardItem *takeCard(int position, int cardId, bool canResize = true); - void rawInsertCard(CardState *card, int index) + void rawInsertCard(CardItem *card, int index) { cards.insert(index, card); - emit cardCountChanged(); } [[nodiscard]] const CardList &getCards() const @@ -66,7 +63,6 @@ public: void sortCards(const QList &options) { cards.sortBy(options); - emit cardCountChanged(); } [[nodiscard]] QString getName() const { @@ -81,7 +77,7 @@ public: { return cards.getContentsKnown(); } - QList &getViews() + QList &getViews() { return views; } @@ -113,12 +109,12 @@ protected: PlayerLogic *player; QString name; CardList cards; - QList views; + QList views; bool hasCardAttr; bool isShufflable; bool alwaysRevealTopCard; - virtual void addCardImpl(CardState *card, int x, int y) = 0; + virtual void addCardImpl(CardItem *card, int x, int y) = 0; }; #endif // COCKATRICE_CARD_ZONE_LOGIC_H diff --git a/libcockatrice_game/libcockatrice/game/zones/hand_zone_logic.cpp b/cockatrice/src/game/zones/hand_zone_logic.cpp similarity index 82% rename from libcockatrice_game/libcockatrice/game/zones/hand_zone_logic.cpp rename to cockatrice/src/game/zones/hand_zone_logic.cpp index 1d3f54929..cea17d031 100644 --- a/libcockatrice_game/libcockatrice/game/zones/hand_zone_logic.cpp +++ b/cockatrice/src/game/zones/hand_zone_logic.cpp @@ -1,6 +1,6 @@ #include "hand_zone_logic.h" -#include "../board/card_state.h" +#include "../../game_graphics/board/card_item.h" #include "card_zone_algorithms.h" class PlayerLogic; @@ -16,7 +16,7 @@ HandZoneLogic::HandZoneLogic(PlayerLogic *_player, { } -void HandZoneLogic::addCardImpl(CardState *card, int x, int /*y*/) +void HandZoneLogic::addCardImpl(CardItem *card, int x, int /*y*/) { CardZoneAlgorithms::addCardToList(cards, card, x, false); } diff --git a/libcockatrice_game/libcockatrice/game/zones/hand_zone_logic.h b/cockatrice/src/game/zones/hand_zone_logic.h similarity index 91% rename from libcockatrice_game/libcockatrice/game/zones/hand_zone_logic.h rename to cockatrice/src/game/zones/hand_zone_logic.h index f8756eafd..c7d63bc0d 100644 --- a/libcockatrice_game/libcockatrice/game/zones/hand_zone_logic.h +++ b/cockatrice/src/game/zones/hand_zone_logic.h @@ -26,7 +26,7 @@ public: QObject *parent = nullptr); protected: - void addCardImpl(CardState *card, int x, int y) override; + void addCardImpl(CardItem *card, int x, int y) override; }; #endif // COCKATRICE_HAND_ZONE_LOGIC_H diff --git a/libcockatrice_game/libcockatrice/game/zones/pile_zone_logic.cpp b/cockatrice/src/game/zones/pile_zone_logic.cpp similarity index 70% rename from libcockatrice_game/libcockatrice/game/zones/pile_zone_logic.cpp rename to cockatrice/src/game/zones/pile_zone_logic.cpp index e33d8fe79..40af25cc5 100644 --- a/libcockatrice_game/libcockatrice/game/zones/pile_zone_logic.cpp +++ b/cockatrice/src/game/zones/pile_zone_logic.cpp @@ -1,6 +1,10 @@ #include "pile_zone_logic.h" -#include "../board/card_state.h" +#include "../../game_graphics/board/card_item.h" +#include "../board/card_list.h" + +class PlayerLogic; +class QObject; PileZoneLogic::PileZoneLogic(PlayerLogic *_player, const QString &_name, @@ -12,14 +16,15 @@ PileZoneLogic::PileZoneLogic(PlayerLogic *_player, { } -void PileZoneLogic::addCardImpl(CardState *card, int x, int /*y*/) +void PileZoneLogic::addCardImpl(CardItem *card, int x, int /*y*/) { - connect(card, &CardState::cardPixmapUpdated, this, &PileZoneLogic::callUpdate); + connect(card, &CardItem::sigPixmapUpdated, this, &PileZoneLogic::callUpdate); // if x is negative set it to add at end if (x < 0 || x >= cards.size()) { x = cards.size(); } cards.insert(x, card); + card->setPos(0, 0); if (!contentsKnown()) { card->setCardRef({}); card->setId(-1); @@ -28,5 +33,6 @@ void PileZoneLogic::addCardImpl(CardState *card, int x, int /*y*/) cards.at(x + 1)->setCardRef({}); } } - card->resetState(false); + card->setVisible(false); + card->resetState(); } \ No newline at end of file diff --git a/libcockatrice_game/libcockatrice/game/zones/pile_zone_logic.h b/cockatrice/src/game/zones/pile_zone_logic.h similarity index 91% rename from libcockatrice_game/libcockatrice/game/zones/pile_zone_logic.h rename to cockatrice/src/game/zones/pile_zone_logic.h index 662b0377f..ed6841b75 100644 --- a/libcockatrice_game/libcockatrice/game/zones/pile_zone_logic.h +++ b/cockatrice/src/game/zones/pile_zone_logic.h @@ -31,7 +31,7 @@ public: QObject *parent = nullptr); protected: - void addCardImpl(CardState *card, int x, int y) override; + void addCardImpl(CardItem *card, int x, int y) override; }; #endif // COCKATRICE_PILE_ZONE_LOGIC_H diff --git a/libcockatrice_game/libcockatrice/game/zones/stack_zone_logic.cpp b/cockatrice/src/game/zones/stack_zone_logic.cpp similarity index 83% rename from libcockatrice_game/libcockatrice/game/zones/stack_zone_logic.cpp rename to cockatrice/src/game/zones/stack_zone_logic.cpp index ccf0d32f3..454274971 100644 --- a/libcockatrice_game/libcockatrice/game/zones/stack_zone_logic.cpp +++ b/cockatrice/src/game/zones/stack_zone_logic.cpp @@ -1,6 +1,6 @@ #include "stack_zone_logic.h" -#include "../board/card_state.h" +#include "../../game_graphics/board/card_item.h" #include "card_zone_algorithms.h" class PlayerLogic; @@ -16,7 +16,7 @@ StackZoneLogic::StackZoneLogic(PlayerLogic *_player, { } -void StackZoneLogic::addCardImpl(CardState *card, int x, int /*y*/) +void StackZoneLogic::addCardImpl(CardItem *card, int x, int /*y*/) { CardZoneAlgorithms::addCardToList(cards, card, x, true); } diff --git a/libcockatrice_game/libcockatrice/game/zones/stack_zone_logic.h b/cockatrice/src/game/zones/stack_zone_logic.h similarity index 91% rename from libcockatrice_game/libcockatrice/game/zones/stack_zone_logic.h rename to cockatrice/src/game/zones/stack_zone_logic.h index ac6c4617c..657d44ecf 100644 --- a/libcockatrice_game/libcockatrice/game/zones/stack_zone_logic.h +++ b/cockatrice/src/game/zones/stack_zone_logic.h @@ -26,7 +26,7 @@ public: QObject *parent = nullptr); protected: - void addCardImpl(CardState *card, int x, int y) override; + void addCardImpl(CardItem *card, int x, int y) override; }; #endif // COCKATRICE_STACK_ZONE_LOGIC_H diff --git a/libcockatrice_game/libcockatrice/game/zones/table_zone_logic.cpp b/cockatrice/src/game/zones/table_zone_logic.cpp similarity index 60% rename from libcockatrice_game/libcockatrice/game/zones/table_zone_logic.cpp rename to cockatrice/src/game/zones/table_zone_logic.cpp index 834794268..07e4a759b 100644 --- a/libcockatrice_game/libcockatrice/game/zones/table_zone_logic.cpp +++ b/cockatrice/src/game/zones/table_zone_logic.cpp @@ -1,9 +1,14 @@ #include "table_zone_logic.h" -#include "../board/card_state.h" +#include "../../game_graphics/board/card_item.h" +#include "../board/card_list.h" +#include "libcockatrice/card/card_info.h" #include +class PlayerLogic; +class QObject; + TableZoneLogic::TableZoneLogic(PlayerLogic *_player, const QString &_name, bool _hasCardAttr, @@ -14,7 +19,7 @@ TableZoneLogic::TableZoneLogic(PlayerLogic *_player, { } -void TableZoneLogic::addCardImpl(CardState *card, int _x, int _y) +void TableZoneLogic::addCardImpl(CardItem *card, int _x, int _y) { cards.append(card); if (!card->getFaceDown() && card->getPT().isEmpty()) { @@ -24,33 +29,15 @@ void TableZoneLogic::addCardImpl(CardState *card, int _x, int _y) card->setDoesntUntap(true); } card->setGridPoint(QPoint(_x, _y)); + card->setVisible(true); } -CardState *TableZoneLogic::takeCard(int position, int cardId, bool toNewZone) +CardItem *TableZoneLogic::takeCard(int position, int cardId, bool toNewZone) { - CardState *result = CardZoneLogic::takeCard(position, cardId); + CardItem *result = CardZoneLogic::takeCard(position, cardId); if (toNewZone) { emit contentSizeChanged(); } return result; -} - -int TableZoneLogic::clampValidTableRow(const int row) -{ - if (row < 0) { - return 0; - } - if (row >= ROW_COUNT) { - return ROW_COUNT - 1; - } - return row; -} - -int TableZoneLogic::tableRowToGridY(int tableRow) -{ - if (tableRow > 2) { - tableRow = 1; - } - return clampValidTableRow(2 - tableRow); -} +} \ No newline at end of file diff --git a/libcockatrice_game/libcockatrice/game/zones/table_zone_logic.h b/cockatrice/src/game/zones/table_zone_logic.h similarity index 57% rename from libcockatrice_game/libcockatrice/game/zones/table_zone_logic.h rename to cockatrice/src/game/zones/table_zone_logic.h index c828d8b91..8078bfd05 100644 --- a/libcockatrice_game/libcockatrice/game/zones/table_zone_logic.h +++ b/cockatrice/src/game/zones/table_zone_logic.h @@ -29,23 +29,8 @@ public: bool _contentsKnown, QObject *parent = nullptr); - /** @brief Number of rows in the table zone grid (0=creatures, 1=noncreatures, 2=lands). */ - static const int ROW_COUNT = 3; - - /** - * Clamps a grid row index into [0, ROW_COUNT - 1]. - */ - static int clampValidTableRow(const int row); - - /** - * Converts a card's logical table row (0=creatures, 1=noncreatures, 2=lands) - * to the corresponding grid Y coordinate. Cards with tableRow > 2 (e.g., - * instants/sorceries) default to the noncreatures row. - */ - static int tableRowToGridY(int tableRow); - protected: - void addCardImpl(CardState *card, int x, int y) override; + void addCardImpl(CardItem *card, int x, int y) override; /** * @brief Removes a card from view. @@ -53,9 +38,9 @@ protected: * @param position card position * @param cardId id of card to take * @param toNewZone Whether the destination of the card is not the same as the starting zone. Defaults to true - * @return CardState that has been removed + * @return CardItem that has been removed */ - CardState *takeCard(int position, int cardId, bool toNewZone = true) override; + CardItem *takeCard(int position, int cardId, bool toNewZone = true) override; }; #endif // COCKATRICE_TABLE_ZONE_LOGIC_H diff --git a/libcockatrice_game/libcockatrice/game/zones/view_zone_logic.cpp b/cockatrice/src/game/zones/view_zone_logic.cpp similarity index 81% rename from libcockatrice_game/libcockatrice/game/zones/view_zone_logic.cpp rename to cockatrice/src/game/zones/view_zone_logic.cpp index db1fa32c3..eb3b7715f 100644 --- a/libcockatrice_game/libcockatrice/game/zones/view_zone_logic.cpp +++ b/cockatrice/src/game/zones/view_zone_logic.cpp @@ -1,11 +1,10 @@ #include "view_zone_logic.h" -#include "../board/card_state.h" -#include "../player/player_actions.h" -#include "../player/player_logic.h" +#include "../../client/settings/cache_settings.h" +#include "../../game_graphics/board/card_item.h" +#include "../board/card_list.h" #include -#include #include class PlayerLogic; @@ -23,19 +22,13 @@ ZoneViewZoneLogic::ZoneViewZoneLogic(PlayerLogic *_player, bool _revealZone, bool _writeableRevealZone, bool _isReversed, - IInterfaceSettingsProvider *_interfaceSettings, QObject *parent) : CardZoneLogic(_player, _origZone->getName(), false, false, true, parent), origZone(_origZone), numberCards(_numberCards), revealZone(_revealZone), writeableRevealZone(_writeableRevealZone), - isReversed(_isReversed), interfaceSettings(_interfaceSettings) + isReversed(_isReversed) { } -bool ZoneViewZoneLogic::getCloseEmptyCardView() const -{ - return interfaceSettings ? interfaceSettings->getCloseEmptyCardView() : true; -} - /** * Checks if inserting a card at the given position requires an actual new card to be created and added to the view. * Also does any cardId updates that would be required if a card is inserted in that position. @@ -43,7 +36,7 @@ bool ZoneViewZoneLogic::getCloseEmptyCardView() const * Note that this method can end up modifying the cardIds despite returning false. * (for example, if the card is inserted into a hidden portion of the deck while the view is reversed) * - * Make sure to call this method once before calling addCard(), so that you skip creating the card and calling + * Make sure to call this method once before calling addCard(), so that you skip creating a new CardItem and calling * addCard() if it's not required. * * @param x The position to insert the card at. @@ -71,7 +64,7 @@ bool ZoneViewZoneLogic::prepareAddCard(int x) // autoclose check is done both here and in removeCard - if (cards.isEmpty() && !doInsert && getCloseEmptyCardView()) { + if (cards.isEmpty() && !doInsert && SettingsCache::instance().userInterface().getCloseEmptyCardView()) { emit closeView(); } @@ -82,7 +75,7 @@ bool ZoneViewZoneLogic::prepareAddCard(int x) * Make sure prepareAddCard() was called before calling addCard(). * This method assumes we already checked that the card is being inserted into the visible portion */ -void ZoneViewZoneLogic::addCardImpl(CardState *card, int x, int /*y*/) +void ZoneViewZoneLogic::addCardImpl(CardItem *card, int x, int /*y*/) { if (!isReversed) { // if x is negative set it to add at end @@ -151,14 +144,14 @@ void ZoneViewZoneLogic::removeCard(int position, bool toNewZone) return; } - CardState *card = cards.takeAt(position); - card->deleteView(); + CardItem *card = cards.takeAt(position); + card->deleteLater(); // The toNewZone check is to prevent the view from auto-closing if the view contains only a single card and that // card gets dragged within the view. // Another autoclose check is done in prepareAddCard so that the view autocloses if the last card was moved to an // unrevealed portion of the same zone. - if (cards.isEmpty() && getCloseEmptyCardView() && toNewZone) { + if (cards.isEmpty() && SettingsCache::instance().userInterface().getCloseEmptyCardView() && toNewZone) { emit closeView(); return; } @@ -167,18 +160,12 @@ void ZoneViewZoneLogic::removeCard(int position, bool toNewZone) reorganizeCards(); } -void ZoneViewZoneLogic::requestClose() -{ - emit closeView(); -} - void ZoneViewZoneLogic::clearCards() { for (auto card : cards) { - card->deleteView(); + card->deleteLater(); } cards.clear(); - emit cardCountChanged(); } void ZoneViewZoneLogic::setWriteableRevealZone(bool _writeableRevealZone) @@ -190,16 +177,4 @@ void ZoneViewZoneLogic::setWriteableRevealZone(bool _writeableRevealZone) emit removeFromViews(); } writeableRevealZone = _writeableRevealZone; -} - -PendingCommand *ZoneViewZoneLogic::prepareZoneDump() -{ - return getPlayer()->getPlayerActions()->prepareZoneDump(this, numberCards); -} - -void ZoneViewZoneLogic::sendZoneDump(PendingCommand *pendingCommand) -{ - if (pendingCommand != nullptr) { - getPlayer()->getPlayerActions()->sendGameCommand(pendingCommand); - } -} +} \ No newline at end of file diff --git a/libcockatrice_game/libcockatrice/game/zones/view_zone_logic.h b/cockatrice/src/game/zones/view_zone_logic.h similarity index 55% rename from libcockatrice_game/libcockatrice/game/zones/view_zone_logic.h rename to cockatrice/src/game/zones/view_zone_logic.h index a29e543a3..6fae301e4 100644 --- a/libcockatrice_game/libcockatrice/game/zones/view_zone_logic.h +++ b/cockatrice/src/game/zones/view_zone_logic.h @@ -12,8 +12,6 @@ class PlayerLogic; class QObject; -class PendingCommand; -class IInterfaceSettingsProvider; class ZoneViewZoneLogic : public CardZoneLogic { @@ -28,8 +26,6 @@ private: int numberCards; bool revealZone, writeableRevealZone; bool isReversed; - // Borrowed settings provider (owned by the application). Null outside the client, in which case views stay open. - IInterfaceSettingsProvider *interfaceSettings; public: enum CardAction @@ -45,25 +41,15 @@ public: bool _revealZone, bool _writeableRevealZone, bool _isReversed, - IInterfaceSettingsProvider *interfaceSettings = nullptr, QObject *parent = nullptr); bool prepareAddCard(int x); void removeCard(int position, bool toNewZone); void updateCardIds(CardAction action); - /** - * @brief Asks the graphics view showing this zone to close itself. - * - * Logic code has no handle on the view, so it goes through the logic object the view was built from. - */ - void requestClose(); - /** @brief Removes all cards from the view, without touching anything else. Used in replay rewind */ void clearCards(); - [[nodiscard]] bool getCloseEmptyCardView() const; - int getNumberCards() const { return numberCards; @@ -87,25 +73,8 @@ public: return origZone; } - /** - * @brief Builds the zone-dump command for this view without sending it. - * @return The pending command, or nullptr when there is no zone to dump. - * - * Keeps command construction in game/ so graphics views don't reach - * through the player into PlayerActions to set it up. The response can - * arrive synchronously (local games), so callers must connect to - * PendingCommand::finished before calling sendZoneDump(). - */ - PendingCommand *prepareZoneDump(); - - /** - * @brief Sends a command returned by prepareZoneDump(). - * @param pendingCommand The prepared command; nullptr is ignored. - */ - void sendZoneDump(PendingCommand *pendingCommand); - protected: - void addCardImpl(CardState *card, int x, int y) override; + void addCardImpl(CardItem *card, int x, int y) override; }; #endif // COCKATRICE_VIEW_ZONE_LOGIC_H diff --git a/cockatrice/src/game_graphics/board/abstract_card_item.cpp b/cockatrice/src/game_graphics/board/abstract_card_item.cpp index 0e8b6ced3..cc121cfb2 100644 --- a/cockatrice/src/game_graphics/board/abstract_card_item.cpp +++ b/cockatrice/src/game_graphics/board/abstract_card_item.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -42,23 +43,13 @@ class QSizeF; AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef, PlayerLogic *_owner, int _id) - : ArrowTarget(_owner, parent), state(new CardState(this, nullptr, cardRef, _id)), tapAngle(0), - bgColor(Qt::transparent), isHovered(false), realZValue(0) + : ArrowTarget(_owner, parent), id(_id), cardRef(cardRef), tapped(false), facedown(false), tapAngle(0), + bgColor(Qt::transparent), isHovered(false), realZValue(0), pendingSelectionCollapse(false) { setCursor(Qt::OpenHandCursor); setFlag(ItemIsSelectable); setCacheMode(DeviceCoordinateCache); - connect(state, &CardState::cardPixmapUpdated, this, &AbstractCardItem::pixmapUpdated); - connect(state, &CardState::cardInfoChanged, this, &AbstractCardItem::onCardInfoChanged); - connect(state, &CardState::cardRefChanged, this, &AbstractCardItem::onCardRefChanged); - connect(state, &CardState::tappedChanged, this, &AbstractCardItem::onTappedChanged); - connect(state, &CardState::facedownChanged, this, [this] { update(); }); - connect(state, &CardState::colorChanged, this, [this] { - cacheBgColor(); - update(); - }); - connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::displayCardNamesChanged, this, [this] { update(); }); connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this, @@ -76,7 +67,7 @@ AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef AbstractCardItem::~AbstractCardItem() { - emit deleteCardInfoPopup(state->getCardRef().name); + emit deleteCardInfoPopup(cardRef.name); } QRectF AbstractCardItem::boundingRect() const @@ -99,20 +90,21 @@ void AbstractCardItem::pixmapUpdated() emit sigPixmapUpdated(); } -void AbstractCardItem::onCardInfoChanged() -{ - cacheBgColor(); - update(); -} - -void AbstractCardItem::onCardRefChanged(const CardRef &oldCardRef, const CardRef & /*newCardRef*/) -{ - emit deleteCardInfoPopup(oldCardRef.name); -} - void AbstractCardItem::refreshCardInfo() { - state->refreshCardInfo(); + exactCard = CardDatabaseManager::query()->getCard(cardRef); + + if (!exactCard && !cardRef.name.isEmpty()) { + CardInfo::UiAttributes attributes = {.tableRow = -1}; + auto info = CardInfo::newInstance(cardRef.name, "", true, {}, {}, {}, {}, attributes); + exactCard = ExactCard(info); + } + if (exactCard) { + connect(exactCard.getCardPtr().data(), &CardInfo::pixmapUpdated, this, &AbstractCardItem::pixmapUpdated); + } + + cacheBgColor(); + update(); } /** @@ -121,7 +113,7 @@ void AbstractCardItem::refreshCardInfo() */ const CardInfo &AbstractCardItem::getCardInfo() const { - return state->getCardInfo(); + return exactCard.getInfo(); } void AbstractCardItem::setRealZValue(qreal _zValue) @@ -166,13 +158,13 @@ void AbstractCardItem::paintPicture(QPainter *painter, const QSizeF &translatedS QPixmap translatedPixmap; bool paintImage = true; - if (state->getFaceDown() || state->getCardRef().name.isEmpty()) { + if (facedown || cardRef.name.isEmpty()) { // never reveal card color, always paint the card back CardPictureLoader::getCardBackPixmap(translatedPixmap, translatedSize.toSize()); } else { // don't even spend time trying to load the picture if our size is too small if (translatedSize.width() > 10) { - CardPictureLoader::getPixmap(translatedPixmap, state->getCard(), translatedSize.toSize()); + CardPictureLoader::getPixmap(translatedPixmap, exactCard, translatedSize.toSize()); if (translatedPixmap.isNull()) { paintImage = false; } @@ -193,20 +185,19 @@ void AbstractCardItem::paintPicture(QPainter *painter, const QSizeF &translatedS painter->drawPath(shape()); } - if (translatedPixmap.isNull() || SettingsCache::instance().cardsDisplay().getDisplayCardNames() || - state->getFaceDown()) { + if (translatedPixmap.isNull() || SettingsCache::instance().cardsDisplay().getDisplayCardNames() || facedown) { painter->save(); transformPainter(painter, translatedSize, angle); painter->setPen(Qt::white); painter->setBackground(Qt::black); painter->setBackgroundMode(Qt::OpaqueMode); QString nameStr; - if (state->getFaceDown()) { - nameStr = "# " + QString::number(state->getId()); + if (facedown) { + nameStr = "# " + QString::number(id); } else { QString prefix = ""; if (SettingsCache::instance().debug().getShowCardId()) { - prefix = "#" + QString::number(state->getId()) + " "; + prefix = "#" + QString::number(id) + " "; } nameStr = prefix + CardLocalization::displayName(getCardInfo()); } @@ -244,6 +235,21 @@ void AbstractCardItem::paint(QPainter *painter, const QStyleOptionGraphicsItem * painter->restore(); } +void AbstractCardItem::setCardRef(const CardRef &_cardRef) +{ + if (cardRef == _cardRef) { + return; + } + + emit deleteCardInfoPopup(cardRef.name); + if (exactCard) { + disconnect(exactCard.getCardPtr().data(), nullptr, this, nullptr); + } + cardRef = _cardRef; + + refreshCardInfo(); +} + void AbstractCardItem::setHovered(bool _hovered) { if (isHovered == _hovered) { @@ -269,17 +275,18 @@ void AbstractCardItem::setHovered(bool _hovered) void AbstractCardItem::setColor(const QString &_color) { - state->setColor(_color); + color = _color; + cacheBgColor(); + update(); } void AbstractCardItem::cacheBgColor() { QChar colorChar; - const QString cardColor = state->getColor(); - if (cardColor.isEmpty()) { - colorChar = state->getCardInfo().getColorChar(); + if (color.isEmpty()) { + colorChar = exactCard.getInfo().getColorChar(); } else { - colorChar = cardColor.at(0); + colorChar = color.at(0); } switch (colorChar.toLower().toLatin1()) { @@ -307,12 +314,17 @@ void AbstractCardItem::cacheBgColor() } } -void AbstractCardItem::onTappedChanged(bool newTapped, bool canAnimate) +void AbstractCardItem::setTapped(bool _tapped, bool canAnimate) { + if (tapped == _tapped) { + return; + } + + tapped = _tapped; if (SettingsCache::instance().cardsDisplay().getTapAnimation() && canAnimate) { static_cast(scene())->registerAnimationItem(this); } else { - tapAngle = newTapped ? 90 : 0; + tapAngle = tapped ? 90 : 0; setTransform(QTransform() .translate(CardDimensions::WIDTH_HALF_F, CardDimensions::HEIGHT_HALF_F) .rotate(tapAngle) @@ -326,6 +338,12 @@ bool AbstractCardItem::animationEvent() return false; } +void AbstractCardItem::setFaceDown(bool _facedown) +{ + facedown = _facedown; + update(); +} + void AbstractCardItem::mousePressEvent(QGraphicsSceneMouseEvent *event) { cancelPendingSelectionCollapse(); @@ -339,7 +357,7 @@ void AbstractCardItem::mousePressEvent(QGraphicsSceneMouseEvent *event) } } if ((event->modifiers() & Qt::AltModifier) && event->button() == Qt::LeftButton) { - emit cardShiftClicked(state->getCardRef().name); + emit cardShiftClicked(cardRef.name); } else if ((event->modifiers() & Qt::ControlModifier)) { setSelected(!isSelected()); } else if (!isSelected()) { @@ -353,7 +371,7 @@ void AbstractCardItem::mousePressEvent(QGraphicsSceneMouseEvent *event) if (event->button() == Qt::LeftButton) { setCursor(Qt::ClosedHandCursor); } else if (event->button() == Qt::MiddleButton) { - emit showCardInfoPopup(event->screenPos(), state->getCardRef()); + emit showCardInfoPopup(event->screenPos(), cardRef); } event->accept(); } @@ -387,7 +405,7 @@ void AbstractCardItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) } if (event->button() == Qt::MiddleButton) { - emit deleteCardInfoPopup(state->getCardRef().name); + emit deleteCardInfoPopup(cardRef.name); } // This function ensures the parent function doesn't mess around with our selection. diff --git a/cockatrice/src/game_graphics/board/abstract_card_item.h b/cockatrice/src/game_graphics/board/abstract_card_item.h index eee8ec743..e2b150ce8 100644 --- a/cockatrice/src/game_graphics/board/abstract_card_item.h +++ b/cockatrice/src/game_graphics/board/abstract_card_item.h @@ -16,7 +16,8 @@ #include #include #include -#include +#include +#include #include class PlayerLogic; @@ -28,8 +29,13 @@ class AbstractCardItem : public ArrowTarget, public IAnimatedItem { Q_OBJECT protected: - CardState *state; + ExactCard exactCard; + int id; + CardRef cardRef; + bool tapped; + bool facedown; int tapAngle; + QString color; QColor bgColor; private: @@ -39,9 +45,6 @@ private: int selectionCollapseGeneration = 0; ///< Guards the deferred collapse timer against stale firings private slots: void pixmapUpdated(); - void onCardInfoChanged(); - void onCardRefChanged(const CardRef &oldCardRef, const CardRef &newCardRef); - void onTappedChanged(bool newTapped, bool canAnimate); public slots: void refreshCardInfo(); @@ -77,38 +80,31 @@ public: QSizeF getTranslatedSize(QPainter *painter) const; void paintPicture(QPainter *painter, const QSizeF &translatedSize, int angle); void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; - [[nodiscard]] CardState *getState() const - { - return state; - } ExactCard getCard() const { - return state->getCard(); + return exactCard; } const CardInfo &getCardInfo() const; int getId() const { - return state->getId(); + return id; } void setId(int _id) { - state->setId(_id); + id = _id; } QString getName() const { - return state->getCardRef().name; + return cardRef.name; } QString getProviderId() const { - return state->getCardRef().providerId; - } - void setCardRef(const CardRef &_cardRef) - { - state->setCardRef(_cardRef); + return cardRef.providerId; } + void setCardRef(const CardRef &_cardRef); CardRef getCardRef() const { - return state->getCardRef(); + return cardRef; } qreal getRealZValue() const { @@ -120,33 +116,27 @@ public: { return isHovered; } - [[nodiscard]] QString getColor() const + QString getColor() const { - return state->getColor(); + return color; } void setColor(const QString &_color); bool getTapped() const { - return state->getTapped(); - } - void setTapped(bool _tapped, bool canAnimate = false) - { - state->setTapped(_tapped, canAnimate); + return tapped; } + void setTapped(bool _tapped, bool canAnimate = false); bool getFaceDown() const { - return state->getFaceDown(); - } - void setFaceDown(bool _facedown) - { - state->setFaceDown(_facedown); + return facedown; } + void setFaceDown(bool _facedown); void processHoverEvent(); /** @brief Drop a pending collapse and disarm any deferred collapse timer. */ void cancelPendingSelectionCollapse(); void deleteCardInfoPopup() { - emit deleteCardInfoPopup(state->getCardRef().name); + emit deleteCardInfoPopup(cardRef.name); } /** @brief Default: no per-tick animation. Subclasses override to animate. */ diff --git a/cockatrice/src/game_graphics/board/abstract_counter.cpp b/cockatrice/src/game_graphics/board/abstract_counter.cpp index 30ffde320..8500b9c51 100644 --- a/cockatrice/src/game_graphics/board/abstract_counter.cpp +++ b/cockatrice/src/game_graphics/board/abstract_counter.cpp @@ -2,6 +2,10 @@ #include "../../client/settings/cache_settings.h" #include "../../client/settings/shortcuts_settings.h" +#include "../../game/board/counter_state.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_info.h" +#include "../../game/player/player_logic.h" #include "../../game_graphics/board/translate_counter_name.h" #include "../../interface/widgets/menus/tearoff_menu.h" @@ -19,10 +23,8 @@ #include #include #include -#include -#include -#include -#include +#include +#include #include #include @@ -149,7 +151,10 @@ void AbstractCounter::mousePressEvent(QGraphicsSceneMouseEvent *event) menu->exec(event->screenPos()); } } else { - player->getPlayerActions()->actIncrementCounter(id, event->button() == Qt::LeftButton ? 1 : -1); + Command_IncCounter cmd; + cmd.set_counter_id(id); + cmd.set_delta(event->button() == Qt::LeftButton ? 1 : -1); + player->getPlayerActions()->sendGameCommand(cmd); } event->accept(); } @@ -167,7 +172,10 @@ void AbstractCounter::hoverLeaveEvent(QGraphicsSceneHoverEvent *) void AbstractCounter::incrementCounter() { - player->getPlayerActions()->actIncrementCounter(id, static_cast(sender())->data().toInt()); + Command_IncCounter cmd; + cmd.set_counter_id(id); + cmd.set_delta(static_cast(sender())->data().toInt()); + player->getPlayerActions()->sendGameCommand(cmd); } void AbstractCounter::setCounter() @@ -191,7 +199,10 @@ void AbstractCounter::setCounter() } Expression exp(value); - player->getPlayerActions()->actSetCounter(id, static_cast(exp.parse(dlg.textValue()))); + Command_SetCounter cmd; + cmd.set_counter_id(id); + cmd.set_value(static_cast(exp.parse(dlg.textValue()))); + player->getPlayerActions()->sendGameCommand(cmd); } AbstractCounterDialog::AbstractCounterDialog(const QString &name, const QString &value, QWidget *parent) diff --git a/cockatrice/src/game_graphics/board/arrow_item.cpp b/cockatrice/src/game_graphics/board/arrow_item.cpp index f7df98ab5..c5a9b9c5e 100644 --- a/cockatrice/src/game_graphics/board/arrow_item.cpp +++ b/cockatrice/src/game_graphics/board/arrow_item.cpp @@ -2,6 +2,11 @@ #include "arrow_item.h" #include "../../client/settings/cache_settings.h" +#include "../../game/board/arrow_data.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_info.h" +#include "../../game/player/player_logic.h" +#include "../../game/zones/card_zone_logic.h" #include "../game_scene.h" #include "../player/player_target.h" #include "../z_values.h" @@ -22,12 +27,12 @@ #include #include #include -#include -#include -#include -#include -#include +#include +#include +#include #include +#include +#include #include #include #include @@ -421,19 +426,19 @@ void ArrowDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) CardZoneLogic *startZone = startCard->getZone(); - ArrowData arrow; - arrow.color = data->color; - arrow.startPlayerId = startZone->getPlayer()->getPlayerInfo()->getId(); - arrow.startZone = startZone->getName(); - arrow.startCardId = startCard->getId(); + Command_CreateArrow cmd; + cmd.mutable_arrow_color()->CopyFrom(convertQColorToColor(data->color)); + cmd.set_start_player_id(startZone->getPlayer()->getPlayerInfo()->getId()); + cmd.set_start_zone(startZone->getName().toStdString()); + cmd.set_start_card_id(startCard->getId()); if (auto *targetCard = qgraphicsitem_cast(targetItem)) { CardZoneLogic *targetZone = targetCard->getZone(); - arrow.targetPlayerId = targetZone->getPlayer()->getPlayerInfo()->getId(); - arrow.targetZone = targetZone->getName(); - arrow.targetCardId = targetCard->getId(); + cmd.set_target_player_id(targetZone->getPlayer()->getPlayerInfo()->getId()); + cmd.set_target_zone(targetZone->getName().toStdString()); + cmd.set_target_card_id(targetCard->getId()); } else if (auto *targetPlayer = qgraphicsitem_cast(targetItem)) { - arrow.targetPlayerId = targetPlayer->getOwner()->getPlayerInfo()->getId(); + cmd.set_target_player_id(targetPlayer->getOwner()->getPlayerInfo()->getId()); } else { delArrow(); return; @@ -442,9 +447,22 @@ void ArrowDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) // if the card is in hand then we will move the card to stack or table as part of drawing the arrow if (startZone->getName() == ZoneNames::HAND) { startCard->playCard(false); + CardInfoPtr ci = startCard->getCard().getCardPtr(); + bool playToStack = SettingsCache::instance().userInterface().getPlayToStack(); + if (ci && ((!playToStack && ci->getUiAttributes().tableRow == 3) || + (playToStack && ci->getUiAttributes().tableRow != 0 && + startCard->getZone()->getName() != ZoneNames::STACK))) { + cmd.set_start_zone(ZoneNames::STACK); + } else { + cmd.set_start_zone(playToStack ? ZoneNames::STACK : ZoneNames::TABLE); + } } - player->getPlayerActions()->createArrow(startCard->getState(), arrow, deleteInPhase); + if (deleteInPhase != 0) { + cmd.set_delete_in_phase(deleteInPhase); + } + + player->getPlayerActions()->sendGameCommand(cmd); } delArrow(); @@ -536,7 +554,7 @@ void ArrowAttachItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) auto *startCard = qgraphicsitem_cast(startItem); auto *targetCard = qgraphicsitem_cast(targetItem); if (startCard && targetCard) { - player->getPlayerActions()->attachCards(startCard->getState(), targetCard->getState()); + attachCards(startCard, targetCard); } } @@ -545,3 +563,23 @@ void ArrowAttachItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) child->mouseReleaseEvent(event); } } + +void ArrowAttachItem::attachCards(CardItem *startCard, const CardItem *targetCard) +{ + if (targetCard->getAttachedTo() || targetCard->getZone()->getName() != ZoneNames::TABLE) { + return; + } + + // move card onto table first if attaching from some other zone + if (startCard->getZone()->getName() != ZoneNames::TABLE) { + player->getPlayerActions()->playCardToTable(startCard, false); + } + + Command_AttachCard cmd; + cmd.set_start_zone(ZoneNames::TABLE); + cmd.set_card_id(startCard->getId()); + cmd.set_target_player_id(targetCard->getZone()->getPlayer()->getPlayerInfo()->getId()); + cmd.set_target_zone(targetCard->getZone()->getName().toStdString()); + cmd.set_target_card_id(targetCard->getId()); + player->getPlayerActions()->sendGameCommand(cmd); +} \ No newline at end of file diff --git a/cockatrice/src/game_graphics/board/arrow_item.h b/cockatrice/src/game_graphics/board/arrow_item.h index 64a48cd14..2395165fc 100644 --- a/cockatrice/src/game_graphics/board/arrow_item.h +++ b/cockatrice/src/game_graphics/board/arrow_item.h @@ -1,6 +1,7 @@ #ifndef ARROWITEM_H #define ARROWITEM_H +#include "../../game/board/arrow_data.h" #include "../animated_item.h" #include "arrow_target.h" #include "graphics_item_type.h" @@ -17,7 +18,6 @@ #include #include #include -#include #include class CardItem; @@ -131,6 +131,7 @@ private: PlayerLogic *player; QList childArrows; QMetaObject::Connection positionConnection; + void attachCards(CardItem *startCard, const CardItem *targetCard); public: explicit ArrowAttachItem(ArrowTarget *_startItem); diff --git a/cockatrice/src/game_graphics/board/card_item.cpp b/cockatrice/src/game_graphics/board/card_item.cpp index acd0764c9..1901a634f 100644 --- a/cockatrice/src/game_graphics/board/card_item.cpp +++ b/cockatrice/src/game_graphics/board/card_item.cpp @@ -1,6 +1,17 @@ #include "card_item.h" #include "../../client/settings/cache_settings.h" +#include "../../game/abstract_game.h" +#include "../../game/board/card_state.h" +#include "../../game/game_state.h" +#include "../../game/phase.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_info.h" +#include "../../game/player/player_logic.h" +#include "../../game/player/player_manager.h" +#include "../../game/zones/card_zone_logic.h" +#include "../../game/zones/table_zone_logic.h" +#include "../../game/zones/view_zone_logic.h" #include "../card_dimensions.h" #include "../game_scene.h" #include "arrow_item.h" @@ -30,17 +41,6 @@ #include #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include #include #include @@ -53,9 +53,8 @@ CardItem::CardItem(PlayerLogic *_owner, const CardRef &cardRef, int _cardid, CardZoneLogic *_zone) - : AbstractCardItem(parent, cardRef, _owner, _cardid), dragItem(nullptr) + : AbstractCardItem(parent, cardRef, _owner, _cardid), state(new CardState(this, _zone)), dragItem(nullptr) { - state->setZone(_zone); owner->addCard(this); connect(&SettingsCache::instance().cardCounters(), &CardCounterSettings::colorChanged, this, [this](int counterId) { @@ -63,44 +62,25 @@ CardItem::CardItem(PlayerLogic *_owner, update(); } }); - - connect(state, &CardState::stateReset, this, [this] { - if (scene()) { - static_cast(scene())->unregisterAnimationItem(this); - } - update(); - }); - - // The retyped logic layer drives CardState setters directly (bypassing the CardItem wrappers that called - // update()), so repaint on every state change: attacking, annotation, doesntUntap, P/T and counters. - connect(state, &CardState::stateChanged, this, [this] { update(); }); - - connect(state, &CardState::viewDeleteRequested, this, &CardItem::deleteLater); } void CardItem::prepareDelete() { if (owner != nullptr) { - auto *gameScene = qobject_cast(scene()); - if (gameScene && gameScene->getActiveCard() == this) { + if (owner->getGame()->getActiveCard() == this) { emit owner->requestCardMenuUpdate(nullptr); - gameScene->setActiveCard(nullptr); + owner->getGame()->setActiveCard(nullptr); } owner = nullptr; } - const QList &attachedStates = state->getAttachedCards(); - while (!attachedStates.isEmpty()) { - auto *attachedItem = qobject_cast(attachedStates.first()->parent()); - if (attachedItem == nullptr) { - break; - } - attachedItem->setZone(nullptr); // so that it won't try to call reorganizeCards() - attachedItem->setAttachedTo(nullptr); + while (!attachedCards.isEmpty()) { + attachedCards.first()->setZone(nullptr); // so that it won't try to call reorganizeCards() + attachedCards.first()->setAttachedTo(nullptr); } if (state->getAttachedTo() != nullptr) { - state->getAttachedTo()->removeAttachedCard(state); + state->getAttachedTo()->removeAttachedCard(this); state->setAttachedTo(nullptr); } } @@ -147,7 +127,7 @@ void CardItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, painter->save(); transformPainter(painter, translatedSize, tapAngle); - if (!getFaceDown() && state->getPT() == state->getCardInfo().getPowTough()) { + if (!getFaceDown() && state->getPT() == exactCard.getInfo().getPowTough()) { painter->setPen(Qt::white); } else { painter->setPen(QColor(255, 150, 0)); // dark orange @@ -229,13 +209,36 @@ void CardItem::setPT(const QString &_pt) void CardItem::setAttachedTo(CardItem *_attachedTo) { - CardState *newParent = _attachedTo == nullptr ? nullptr : _attachedTo->getState(); - state->setAttachedTo(newParent); + if (state->getAttachedTo() != nullptr) { + state->getAttachedTo()->removeAttachedCard(this); + } - // If the zone is being torn down, it might already be null by the time a card tries to un-attach all its attached - // cards - if (newParent != nullptr ? newParent->getZone() == nullptr : state->getZone() == nullptr) { - deleteLater(); + gridPoint.setX(-1); + state->setAttachedTo(_attachedTo); + if (state->getAttachedTo() != nullptr) { + // If the zone is being torn down, it might already be null by the time a card tries to un-attach all its + // attached cards + if (state->getAttachedTo()->getZone() == nullptr) { + deleteLater(); + } else { + emit state->getAttachedTo()->getZone()->cardAdded(this); + state->getAttachedTo()->addAttachedCard(this); + if (state->getZone() != state->getAttachedTo()->getZone()) { + state->getAttachedTo()->getZone()->reorganizeCards(); + } + } + } else { + // If the zone is being torn down, it might already be null by the time a card tries to un-attach all its + // attached cards + if (state->getZone() == nullptr) { + deleteLater(); + } else { + emit state->getZone()->cardAdded(this); + } + } + + if (state->getZone() != nullptr) { + state->getZone()->reorganizeCards(); } } @@ -245,6 +248,13 @@ void CardItem::setAttachedTo(CardItem *_attachedTo) void CardItem::resetState(bool keepAnnotations) { state->resetState(keepAnnotations); + attachedCards.clear(); + setTapped(false, false); + setDoesntUntap(false); + if (scene()) { + static_cast(scene())->unregisterAnimationItem(this); + } + update(); } void CardItem::processCardInfo(const ServerInfo_Card &_info) @@ -380,7 +390,7 @@ void CardItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) // Use the buttonDownPos to align the hot spot with the position when // the user originally clicked - createDragItem(state->getId(), event->buttonDownPos(Qt::LeftButton), event->scenePos(), forceFaceDown); + createDragItem(id, event->buttonDownPos(Qt::LeftButton), event->scenePos(), forceFaceDown); dragItem->grabMouse(); int childIndex = 0; @@ -423,11 +433,42 @@ void CardItem::playCard(bool faceDown) emit playSelected(this); } } else { - state->getZone()->getPlayer()->getPlayerActions()->playCard(state, faceDown); + state->getZone()->getPlayer()->getPlayerActions()->playCard(this, faceDown); } } } +QVariantList CardItem::parsePT(const QString &pt) +{ + QVariantList ptList = QVariantList(); + if (!pt.isEmpty()) { + int sep = pt.indexOf('/'); + if (sep == 0) { + ptList.append(QVariant(pt.mid(1))); // cut off starting '/' and take full string + } else { + int start = 0; + for (;;) { + QString item = pt.mid(start, sep - start); + if (item.isEmpty()) { + ptList.append(QVariant(QString())); + } else if (item[0] == '+') { + ptList.append(QVariant(item.mid(1).toInt())); // add as int + } else if (item[0] == '-') { + ptList.append(QVariant(item.toInt())); // add as int + } else { + ptList.append(QVariant(item)); // add as qstring + } + if (sep == -1) { + break; + } + start = sep + 1; + sep = pt.indexOf('/', start); + } + } + } + return ptList; +} + /** * @brief returns true if the zone is a unwritable reveal zone view (eg a card reveal window). Will return false if zone * is nullptr. @@ -452,7 +493,7 @@ void CardItem::handleClickedToPlay(bool shiftHeld) if (SettingsCache::instance().userInterface().getClickPlaysAllSelected()) { emit hideSelected(this); } else { - state->getZone()->removeCard(state); + state->getZone()->removeCard(this); } } else { playCard(shiftHeld); @@ -496,16 +537,16 @@ bool CardItem::animationEvent() { int rotation = ROTATION_DEGREES_PER_FRAME; bool animationIncomplete = true; - if (!state->getTapped()) { + if (!tapped) { rotation *= -1; } tapAngle += rotation; - if (state->getTapped() && (tapAngle > 90)) { + if (tapped && (tapAngle > 90)) { tapAngle = 90; animationIncomplete = false; } - if (!state->getTapped() && (tapAngle < 0)) { + if (!tapped && (tapAngle < 0)) { tapAngle = 0; animationIncomplete = false; } @@ -526,9 +567,7 @@ QVariant CardItem::itemChange(GraphicsItemChange change, const QVariant &value) bool selected = value.toBool(); if (selected) { - if (auto *gameScene = qobject_cast(scene())) { - gameScene->setActiveCard(this); - } + owner->getGame()->setActiveCard(this); } emit selectionChanged(this, selected); diff --git a/cockatrice/src/game_graphics/board/card_item.h b/cockatrice/src/game_graphics/board/card_item.h index 01dacc021..0737d7f9c 100644 --- a/cockatrice/src/game_graphics/board/card_item.h +++ b/cockatrice/src/game_graphics/board/card_item.h @@ -7,6 +7,7 @@ #ifndef CARDITEM_H #define CARDITEM_H +#include "../../game/board/card_state.h" #include "abstract_card_item.h" #include "graphics_item_type.h" @@ -15,7 +16,6 @@ #include #include #include -#include #include class CardDragItem; @@ -33,7 +33,11 @@ class CardItem : public AbstractCardItem { Q_OBJECT private: + CardState *state; + + QPoint gridPoint; CardDragItem *dragItem; + QList attachedCards; void prepareDelete(); void handleClickedToPlay(bool shiftHeld); @@ -56,6 +60,10 @@ public: CardZoneLogic *_zone = nullptr); void retranslateUi(); + [[nodiscard]] CardState *getState() const + { + return state; + } [[nodiscard]] CardZoneLogic *getZone() const { return state->getZone(); @@ -64,15 +72,15 @@ public: void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; [[nodiscard]] QPoint getGridPoint() const { - return state->getGridPoint(); + return gridPoint; } void setGridPoint(const QPoint &_gridPoint) { - state->setGridPoint(_gridPoint); + gridPoint = _gridPoint; } [[nodiscard]] QPoint getGridPos() const { - return state->getGridPoint(); + return gridPoint; } [[nodiscard]] PlayerLogic *getOwner() const { @@ -115,22 +123,22 @@ public: { state->setDestroyOnZoneChange(_destroy); } - [[nodiscard]] CardState *getAttachedTo() const + [[nodiscard]] CardItem *getAttachedTo() const { return state->getAttachedTo(); } void setAttachedTo(CardItem *_attachedTo); - void addAttachedCard(CardState *card) + void addAttachedCard(CardItem *card) { - state->addAttachedCard(card); + attachedCards.append(card); } - void removeAttachedCard(CardState *card) + void removeAttachedCard(CardItem *card) { - state->removeAttachedCard(card); + attachedCards.removeOne(card); } - [[nodiscard]] const QList &getAttachedCards() const + [[nodiscard]] const QList &getAttachedCards() const { - return state->getAttachedCards(); + return attachedCards; } void resetState(bool keepAnnotations = false); void processCardInfo(const ServerInfo_Card &_info); @@ -142,6 +150,26 @@ public: void drawAttachArrow(); void playCard(bool faceDown); + /** + * @brief Parses a string representing a p/t in order to extract the values from it. + * + * If the string contains '/', the string will be split at the '/' and each side will be parsed separately, + * which means the result list will have two elements. + * + * If '/' is not found, then the entire string is parsed together, which means the result list will + * have a single element. + * + * If either side of the split is empty, there will also only be a single element in the result list. + * + * This function will attempt to parse each substring as an int first, handling plus and minus prefixes. + * If successful, it will put the parsed value into the QVariant as an int. + * If failed, it will just put the substring into the QVariant as a QString. + * + * @param pt The p/t string + * @return A QVariantList that can contain one or two elements, where each QVariant can be either int or QString + */ + static QVariantList parsePT(const QString &pt); + protected: void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; diff --git a/cockatrice/src/game_graphics/deckview/deck_view_container.cpp b/cockatrice/src/game_graphics/deckview/deck_view_container.cpp index 3bef9e59e..90b3bea11 100644 --- a/cockatrice/src/game_graphics/deckview/deck_view_container.cpp +++ b/cockatrice/src/game_graphics/deckview/deck_view_container.cpp @@ -2,8 +2,13 @@ #include "../../client/settings/cache_settings.h" #include "../../client/settings/shortcuts_settings.h" +#include "../../game/abstract_game.h" +#include "../../game/game_event_handler.h" +#include "../../game/game_state.h" #include "../../interface/card_picture_loader/card_picture_loader.h" +#include "../../interface/deck_loader/deck_file_format.h" #include "../../interface/deck_loader/deck_loader.h" +#include "../../interface/deck_loader/loaded_deck.h" #include "../../interface/widgets/dialogs/dlg_load_deck.h" #include "../../interface/widgets/dialogs/dlg_load_deck_from_clipboard.h" #include "../../interface/widgets/dialogs/dlg_load_deck_from_website.h" @@ -28,12 +33,12 @@ #include #include #include -#include -#include #include -#include -#include -#include +#include +#include +#include +#include +#include #include #include #include @@ -309,7 +314,9 @@ void DeckViewContainer::loadDeckFromDeckList(const DeckList &deck) return; } - PendingCommand *pend = parentGame->getGame()->getGameEventHandler()->prepareDeckSelect(deckString); + Command_DeckSelect cmd; + cmd.set_deck(deckString.toStdString()); + PendingCommand *pend = parentGame->getGame()->getGameEventHandler()->prepareGameCommand(cmd); connect(pend, &PendingCommand::finished, this, &DeckViewContainer::deckSelectFinished); parentGame->getGame()->getGameEventHandler()->sendGameCommand(pend, playerId); @@ -338,7 +345,15 @@ void DeckViewContainer::resolveAndSendPlaymat() lastResolvedPlaymat = resolved; - PendingCommand *playmatPend = parentGame->getGame()->getGameEventHandler()->prepareSetPlaymat(resolved); + Command_SetPlaymat playmatCmd; + auto *pp = playmatCmd.mutable_playmat_params(); + pp->set_card_name(resolved.card.name.toStdString()); + pp->set_card_provider_id(resolved.card.providerId.toStdString()); + pp->set_margin_pct_l(resolved.params.marginPctL); + pp->set_margin_pct_r(resolved.params.marginPctR); + pp->set_vertical_offset(resolved.params.verticalOffset); + pp->set_zoom(resolved.params.zoom); + PendingCommand *playmatPend = parentGame->getGame()->getGameEventHandler()->prepareGameCommand(playmatCmd); const QString sentName = resolved.card.name; connect(playmatPend, &PendingCommand::finished, this, [this, sentName](const Response &r) { // Only RespFunctionNotAllowed signals a card art DENY rule. Older servers that predate @@ -369,7 +384,9 @@ void DeckViewContainer::loadRemoteDeck() { DlgLoadRemoteDeck dlg(parentGame->getGame()->getClientForPlayer(playerId), this); if (dlg.exec()) { - PendingCommand *pend = parentGame->getGame()->getGameEventHandler()->prepareDeckSelectId(dlg.getDeckId()); + Command_DeckSelect cmd; + cmd.set_deck_id(dlg.getDeckId()); + PendingCommand *pend = parentGame->getGame()->getGameEventHandler()->prepareGameCommand(cmd); connect(pend, &PendingCommand::finished, this, &DeckViewContainer::deckSelectFinished); parentGame->getGame()->getGameEventHandler()->sendGameCommand(pend, playerId); } @@ -429,17 +446,28 @@ void DeckViewContainer::forceStart() return; } - parentGame->getGame()->getGameEventHandler()->sendForceStart(playerId); + Command_ReadyStart cmd; + cmd.set_force_start(true); + cmd.set_ready(true); + parentGame->getGame()->getGameEventHandler()->sendGameCommand(cmd, playerId); } void DeckViewContainer::sideboardLockButtonClicked() { - parentGame->getGame()->getGameEventHandler()->sendSetSideboardLock(sideboardLockButton->getState(), playerId); + Command_SetSideboardLock cmd; + cmd.set_locked(sideboardLockButton->getState()); + + parentGame->getGame()->getGameEventHandler()->sendGameCommand(cmd, playerId); } void DeckViewContainer::sideboardPlanChanged() { - parentGame->getGame()->getGameEventHandler()->sendSetSideboardPlan(deckView->getSideboardPlan(), playerId); + Command_SetSideboardPlan cmd; + const QList &newPlan = deckView->getSideboardPlan(); + for (const auto &i : newPlan) { + cmd.add_move_list()->CopyFrom(i); + } + parentGame->getGame()->getGameEventHandler()->sendGameCommand(cmd, playerId); } /** @@ -451,7 +479,9 @@ void DeckViewContainer::sendReadyStartCommand(bool ready) resolveAndSendPlaymat(); } - parentGame->getGame()->getGameEventHandler()->sendReadyStart(ready, playerId); + Command_ReadyStart cmd; + cmd.set_ready(ready); + parentGame->getGame()->getGameEventHandler()->sendGameCommand(cmd, playerId); } /** diff --git a/cockatrice/src/game_graphics/dialogs/dlg_annotation.cpp b/cockatrice/src/game_graphics/dialogs/dlg_annotation.cpp deleted file mode 100644 index c812f9c74..000000000 --- a/cockatrice/src/game_graphics/dialogs/dlg_annotation.cpp +++ /dev/null @@ -1,13 +0,0 @@ -#include "dlg_annotation.h" - -#include - -void AnnotationDialog::keyPressEvent(QKeyEvent *event) -{ - if (event->key() == Qt::Key_Return && event->modifiers() & Qt::ControlModifier) { - event->accept(); - accept(); - return; - } - QInputDialog::keyPressEvent(event); -} diff --git a/cockatrice/src/game_graphics/dialogs/dlg_annotation.h b/cockatrice/src/game_graphics/dialogs/dlg_annotation.h deleted file mode 100644 index 686377d3c..000000000 --- a/cockatrice/src/game_graphics/dialogs/dlg_annotation.h +++ /dev/null @@ -1,22 +0,0 @@ -/** - * @file dlg_annotation.h - * @ingroup GameDialogs - */ - -#ifndef COCKATRICE_DLG_ANNOTATION_H -#define COCKATRICE_DLG_ANNOTATION_H - -#include - -class AnnotationDialog : public QInputDialog -{ - Q_OBJECT - void keyPressEvent(QKeyEvent *e) override; - -public: - explicit AnnotationDialog(QWidget *parent = nullptr) : QInputDialog(parent) - { - } -}; - -#endif // COCKATRICE_DLG_ANNOTATION_H diff --git a/cockatrice/src/game_graphics/dialogs/dlg_create_token.h b/cockatrice/src/game_graphics/dialogs/dlg_create_token.h index 323e61829..a03a1046b 100644 --- a/cockatrice/src/game_graphics/dialogs/dlg_create_token.h +++ b/cockatrice/src/game_graphics/dialogs/dlg_create_token.h @@ -11,7 +11,6 @@ #include #include #include -#include #include class QLabel; @@ -26,6 +25,17 @@ class CardInfoPictureWidget; class QModelIndex; class QWidget; +struct TokenInfo +{ + QString name; + QString color; + QString pt; + QString annotation; + bool destroy = true; + bool faceDown = false; + QString providerId; +}; + class DlgCreateToken : public QDialog { Q_OBJECT diff --git a/cockatrice/src/game_graphics/dialogs/dlg_move_top_cards_until.h b/cockatrice/src/game_graphics/dialogs/dlg_move_top_cards_until.h index da2e70b0a..b9080bcc1 100644 --- a/cockatrice/src/game_graphics/dialogs/dlg_move_top_cards_until.h +++ b/cockatrice/src/game_graphics/dialogs/dlg_move_top_cards_until.h @@ -11,7 +11,6 @@ #include #include #include -#include #include class FilterString; @@ -22,6 +21,13 @@ class QLabel; class QSpinBox; class QWidget; +struct MoveTopCardsUntilOptions +{ + QStringList exprs = {}; + int numberOfHits = 1; + bool autoPlay = false; +}; + class DlgMoveTopCardsUntil : public QDialog { Q_OBJECT diff --git a/cockatrice/src/game_graphics/game_scene.cpp b/cockatrice/src/game_graphics/game_scene.cpp index db1040006..cc3f9f1d3 100644 --- a/cockatrice/src/game_graphics/game_scene.cpp +++ b/cockatrice/src/game_graphics/game_scene.cpp @@ -1,10 +1,18 @@ #include "game_scene.h" #include "../client/settings/cache_settings.h" +#include "../game/abstract_game.h" +#include "../game/arrow_registry.h" +#include "../game/board/arrow_data.h" +#include "../game/player/player_actions.h" +#include "../game/player/player_event_handler.h" +#include "../game/player/player_info.h" +#include "../game/player/player_logic.h" +#include "../game/zones/card_zone_logic.h" +#include "../game/zones/view_zone_logic.h" #include "animated_item.h" #include "board/abstract_card_item.h" #include "board/arrow_item.h" -#include "board/arrow_registry.h" #include "board/card_item.h" #include "phases_toolbar.h" #include "player/menu/player_menu.h" @@ -32,15 +40,6 @@ #include #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include #include #include @@ -113,15 +112,6 @@ void GameScene::retranslateUi() } } -QList GameScene::selectedCardStates() const -{ - QList selectedStates; - for (auto *card : selectedCards()) { - selectedStates.append(card->getState()); - } - return selectedStates; -} - QList GameScene::selectedCards() const { QList selectedCards; @@ -134,67 +124,6 @@ QList GameScene::selectedCards() const return selectedCards; } -/** - * @brief Selects every card of @p zone for which @p filter holds. - * @param filter A predicate to filter which cards are selected. - */ -static void selectCardsInZone( - const CardZoneLogic *zone, - std::function filter = [](const CardItem *) { return true; }) -{ - if (!zone) { - return; - } - - for (auto *cardState : zone->getCards()) { - auto *cardItem = cardState == nullptr ? nullptr : qobject_cast(cardState->parent()); - if (cardItem && filter(cardItem)) { - cardItem->setSelected(true); - } - } -} - -void GameScene::selectAllInZone(const CardItem *card) -{ - if (!card) { - return; - } - - selectCardsInZone(card->getZone()); -} - -void GameScene::selectSameRow(const CardItem *card) -{ - if (!card) { - return; - } - - auto isSameRow = [card](const CardItem *cardItem) { - return qAbs(card->scenePos().y() - cardItem->scenePos().y()) < 50; - }; - selectCardsInZone(card->getZone(), isSameRow); -} - -void GameScene::selectSameColumn(const CardItem *card) -{ - if (!card) { - return; - } - - auto isSameColumn = [card](const CardItem *cardItem) { return cardItem->x() == card->x(); }; - selectCardsInZone(card->getZone(), isSameColumn); -} - -CardItem *GameScene::getActiveCard() const -{ - return activeCard; -} - -void GameScene::setActiveCard(CardItem *card) -{ - activeCard = card; -} - void GameScene::onCardSelectionChanged(AbstractCardItem *abstractCard, bool selected) { CardItem *card = qobject_cast(abstractCard); @@ -205,12 +134,12 @@ void GameScene::onCardSelectionChanged(AbstractCardItem *abstractCard, bool sele auto *owner = card->getOwner(); if (selected) { - owner->requestCardMenuUpdate(card->getState()); + owner->requestCardMenuUpdate(card); return; } if (selectedItems().isEmpty()) { - setActiveCard(nullptr); + owner->getGame()->setActiveCard(nullptr); owner->requestCardMenuUpdate(nullptr); } } @@ -229,9 +158,9 @@ void GameScene::onCardRightClicked(AbstractCardItem *abstractCard, QPoint screen return; } - setActiveCard(card); + card->getOwner()->getGame()->setActiveCard(card); - if (auto *menu = view->getPlayerMenu()->updateCardMenu(card->getState())) { + if (auto *menu = view->getPlayerMenu()->updateCardMenu(card)) { menu->popup(screenPos); } } @@ -244,7 +173,7 @@ void GameScene::playSelected(AbstractCardItem *card) if (!card->getOwner()) { return; } - card->getOwner()->getPlayerActions()->actPlay(selectedCardStates()); + card->getOwner()->getPlayerActions()->actPlay(selectedCards()); } void GameScene::playSelectedFaceDown(AbstractCardItem *card) @@ -255,7 +184,7 @@ void GameScene::playSelectedFaceDown(AbstractCardItem *card) if (!card->getOwner()) { return; } - card->getOwner()->getPlayerActions()->actPlayFacedown(selectedCardStates()); + card->getOwner()->getPlayerActions()->actPlayFacedown(selectedCards()); } void GameScene::hideSelected(AbstractCardItem *card) @@ -266,7 +195,7 @@ void GameScene::hideSelected(AbstractCardItem *card) if (!card->getOwner()) { return; } - card->getOwner()->getPlayerActions()->actHide(selectedCardStates()); + card->getOwner()->getPlayerActions()->actHide(selectedCards()); } /** @@ -301,8 +230,6 @@ void GameScene::addPlayer(PlayerLogic *player) [this, id = player->getPlayerInfo()->getId()]() { clearArrowsForPlayerLocally(id); }); connect(player->getPlayerEventHandler(), &PlayerEventHandler::cardZoneChanged, this, &GameScene::onCardZoneChanged); - connect(player->getPlayerEventHandler(), &PlayerEventHandler::cardViewRefreshRequested, this, - &GameScene::onCardViewRefreshRequested); rearrange(); } @@ -606,11 +533,7 @@ void GameScene::addArrow(QSharedPointer data) return; } - CardState *startCardState = startZone->getCard(data->startCardId); - if (!startCardState) { - return; - } - CardItem *startCard = qobject_cast(startCardState->parent()); + CardItem *startCard = startZone->getCard(data->startCardId); if (!startCard) { return; } @@ -621,8 +544,7 @@ void GameScene::addArrow(QSharedPointer data) } else { auto *zone = targetView->getLogic()->getZones().value(data->targetZone); if (zone) { - CardState *targetCardState = zone->getCard(data->targetCardId); - targetItem = targetCardState == nullptr ? nullptr : qobject_cast(targetCardState->parent()); + targetItem = zone->getCard(data->targetCardId); } } if (!targetItem) { @@ -650,15 +572,11 @@ void GameScene::requestArrowDeletion(int playerId, int arrowId) } } -void GameScene::onCardZoneChanged(CardState *card, bool sameZone) +void GameScene::onCardZoneChanged(CardItem *card, bool sameZone) { - auto *item = qobject_cast(card->parent()); - if (!item) { - return; - } QList toDelete; for (auto *arrow : arrowRegistry.all()) { - if (arrow->getStartItem() == item || arrow->getTargetItem() == item) { + if (arrow->getStartItem() == card || arrow->getTargetItem() == card) { if (sameZone) { arrow->updatePath(); } else { @@ -671,25 +589,6 @@ void GameScene::onCardZoneChanged(CardState *card, bool sameZone) } } -void GameScene::onCardViewRefreshRequested(CardState *card, bool zoneChanged, PlayerLogic *newOwner) -{ - auto *item = qobject_cast(card->parent()); - if (!item) { - return; - } - if (zoneChanged) { - item->deleteCardInfoPopup(); - } - item->deleteDragItem(); - if (zoneChanged) { - item->setBeingPointedAt(false); - item->setHovered(false); - } - if (newOwner) { - item->setOwner(newOwner); - } -} - void GameScene::clearArrowsForPlayer(int playerId) { for (int arrowId : arrowRegistry.idsForPlayer(playerId)) { diff --git a/cockatrice/src/game_graphics/game_scene.h b/cockatrice/src/game_graphics/game_scene.h index 0d4db70a8..c7cae1e76 100644 --- a/cockatrice/src/game_graphics/game_scene.h +++ b/cockatrice/src/game_graphics/game_scene.h @@ -1,9 +1,7 @@ #ifndef GAMESCENE_H #define GAMESCENE_H -#include "animated_item.h" -#include "board/arrow_item.h" -#include "board/arrow_registry.h" +#include "../game/arrow_registry.h" #include #include @@ -18,8 +16,6 @@ #include #include #include -#include -#include #include // IWYU pragma: keep @@ -43,7 +39,6 @@ class ZoneViewWidget; class CardZone; class AbstractCardItem; class CardItem; -class CardState; class ServerInfo_Card; class PhasesToolbar; class QBasicTimer; @@ -74,7 +69,6 @@ private: QList zoneViews; ///< Active zone view widgets QSize viewSize; ///< Current view size QPointer hoveredCard; ///< Currently hovered card - QPointer activeCard; ///< Card the currently open context menu targets QBasicTimer *animationTimer; ///< Timer for scene animations QHash animatedItems; ///< Items currently animating QHash @@ -113,22 +107,6 @@ public: /** @brief Gets all selected CardItems. */ QList selectedCards() const; - QList selectedCardStates() const; - - /** @brief Selects every card sharing the zone of @p card. */ - void selectAllInZone(const CardItem *card); - - /** @brief Selects the cards laid out in the same visual row as @p card. */ - void selectSameRow(const CardItem *card); - - /** @brief Selects the cards laid out in the same visual column as @p card. */ - void selectSameColumn(const CardItem *card); - - /** @brief Card the currently open context menu targets, or null. */ - CardItem *getActiveCard() const; - - /** @brief Sets the card the currently open context menu targets. */ - void setActiveCard(CardItem *card); /** * @brief Adds a player to the scene and stores their graphics item. @@ -283,15 +261,7 @@ public slots: /// Queues up arrow deletion but doesn't directly modify the scene void requestArrowDeletion(int playerId, int arrowId); - void onCardZoneChanged(CardState *card, bool sameZone); - /** - * @brief Refreshes the view of a card that the server just moved. - * - * @param card The card that moved. - * @param zoneChanged Whether the card crossed a zone boundary. - * @param newOwner The player the card now belongs to, or nullptr to leave the owner alone. - */ - void onCardViewRefreshRequested(CardState *card, bool zoneChanged, PlayerLogic *newOwner); + void onCardZoneChanged(CardItem *card, bool sameZone); protected: /** @brief Handles hover updates. */ diff --git a/cockatrice/src/game_graphics/hand_counter.cpp b/cockatrice/src/game_graphics/hand_counter.cpp index b9ea2d554..9320c1295 100644 --- a/cockatrice/src/game_graphics/hand_counter.cpp +++ b/cockatrice/src/game_graphics/hand_counter.cpp @@ -1,5 +1,7 @@ #include "hand_counter.h" +#include "../game/board/card_list.h" +#include "../game/zones/card_zone_logic.h" #include "../interface/pixel_map_generator.h" #include @@ -12,8 +14,6 @@ #include #include #include -#include -#include #include class QGraphicsItem; diff --git a/cockatrice/src/game_graphics/log/message_log_widget.cpp b/cockatrice/src/game_graphics/log/message_log_widget.cpp index 046a1ae90..4a9750fca 100644 --- a/cockatrice/src/game_graphics/log/message_log_widget.cpp +++ b/cockatrice/src/game_graphics/log/message_log_widget.cpp @@ -3,6 +3,16 @@ #include "../../client/settings/cache_settings.h" #include "../../client/settings/card_counter_settings.h" #include "../../client/sound_engine.h" +#include "../../client/translation.h" +#include "../../game/abstract_game.h" +#include "../../game/board/card_list.h" +#include "../../game/game_state.h" +#include "../../game/phase.h" +#include "../../game/player/player_event_handler.h" +#include "../../game/player/player_info.h" +#include "../../game/player/player_logic.h" +#include "../../game/player/player_manager.h" +#include "../../game/zones/card_zone_logic.h" #include "../../interface/card_localization.h" #include "../../interface/widgets/server/chat_view/chat_view.h" #include "../board/card_item.h" @@ -14,16 +24,6 @@ #include #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include #include #include @@ -283,7 +283,7 @@ void MessageLogWidget::logDestroyCard(PlayerLogic *player, QString cardName) } void MessageLogWidget::logMoveCard(PlayerLogic *player, - CardState *card, + CardItem *card, CardZoneLogic *startZone, int oldX, CardZoneLogic *targetZone, @@ -662,7 +662,7 @@ void MessageLogWidget::logSetActivePlayer(PlayerLogic *player) QString(tr("%1's turn.")).arg(player->getPlayerInfo()->getName()) + "
"); } -void MessageLogWidget::logSetAnnotation(PlayerLogic *player, CardState *card, QString newAnnotation) +void MessageLogWidget::logSetAnnotation(PlayerLogic *player, CardItem *card, QString newAnnotation) { appendHtmlServerMessage( QString(tr("%1 sets annotation of %2 to %3.")) @@ -706,7 +706,7 @@ void MessageLogWidget::logSetCounter(PlayerLogic *player, QString counterName, i .arg(value - oldValue)); } -void MessageLogWidget::logSetDoesntUntap(PlayerLogic *player, CardState *card, bool doesntUntap) +void MessageLogWidget::logSetDoesntUntap(PlayerLogic *player, CardItem *card, bool doesntUntap) { QString str; if (doesntUntap) { @@ -717,7 +717,7 @@ void MessageLogWidget::logSetDoesntUntap(PlayerLogic *player, CardState *card, b appendHtmlServerMessage(str.arg(sanitizeHtml(player->getPlayerInfo()->getName())).arg(cardLink(card->getName()))); } -void MessageLogWidget::logSetPT(PlayerLogic *player, CardState *card, QString newPT) +void MessageLogWidget::logSetPT(PlayerLogic *player, CardItem *card, QString newPT) { if (currentContext == MessageContext_MoveCard) { return; @@ -755,7 +755,7 @@ void MessageLogWidget::logSetSideboardLock(PlayerLogic *player, bool locked) } } -void MessageLogWidget::logSetTapped(PlayerLogic *player, CardState *card, bool tapped) +void MessageLogWidget::logSetTapped(PlayerLogic *player, CardItem *card, bool tapped) { if (currentContext == MessageContext_MoveCard) { return; diff --git a/cockatrice/src/game_graphics/log/message_log_widget.h b/cockatrice/src/game_graphics/log/message_log_widget.h index 5642a76e9..6800ba8ed 100644 --- a/cockatrice/src/game_graphics/log/message_log_widget.h +++ b/cockatrice/src/game_graphics/log/message_log_widget.h @@ -14,7 +14,6 @@ #include #include -class CardState; class AbstractGame; class CardItem; class GameEventContext; @@ -77,7 +76,7 @@ public slots: void logLeaveSpectator(QString name, QString reason); void logNotReadyStart(PlayerLogic *player); void logMoveCard(PlayerLogic *player, - CardState *card, + CardItem *card, CardZoneLogic *startZone, int oldX, CardZoneLogic *targetZone, @@ -98,13 +97,13 @@ public slots: void logSay(PlayerLogic *player, QString message); void logSetActivePhase(int phase); void logSetActivePlayer(PlayerLogic *player); - void logSetAnnotation(PlayerLogic *player, CardState *card, QString newAnnotation); + void logSetAnnotation(PlayerLogic *player, CardItem *card, QString newAnnotation); void logSetCardCounter(PlayerLogic *player, QString cardName, int counterId, int value, int oldValue); void logSetCounter(PlayerLogic *player, QString counterName, int value, int oldValue); - void logSetDoesntUntap(PlayerLogic *player, CardState *card, bool doesntUntap); - void logSetPT(PlayerLogic *player, CardState *card, QString newPT); + void logSetDoesntUntap(PlayerLogic *player, CardItem *card, bool doesntUntap); + void logSetPT(PlayerLogic *player, CardItem *card, QString newPT); void logSetSideboardLock(PlayerLogic *player, bool locked); - void logSetTapped(PlayerLogic *player, CardState *card, bool tapped); + void logSetTapped(PlayerLogic *player, CardItem *card, bool tapped); void logShuffle(PlayerLogic *player, CardZoneLogic *zone, int start, int end); void logSpectatorSay(const ServerInfo_User &spectator, QString message); void logUnattachCard(PlayerLogic *player, QString cardName); diff --git a/cockatrice/src/game_graphics/phases_toolbar.cpp b/cockatrice/src/game_graphics/phases_toolbar.cpp index bc05e8901..d0a3f74fd 100644 --- a/cockatrice/src/game_graphics/phases_toolbar.cpp +++ b/cockatrice/src/game_graphics/phases_toolbar.cpp @@ -14,6 +14,11 @@ #include #include #include +#include +#include +#include +#include +#include #include #include @@ -115,9 +120,9 @@ PhasesToolbar::PhasesToolbar(QGraphicsItem *parent) : QGraphicsItem(parent), width(100), height(100), ySpacing(1), symbolSize(8) { auto *aUntapAll = new QAction(this); - connect(aUntapAll, &QAction::triggered, this, &PhasesToolbar::untapAllRequested); + connect(aUntapAll, &QAction::triggered, this, &PhasesToolbar::actUntapAll); auto *aDrawCard = new QAction(this); - connect(aDrawCard, &QAction::triggered, this, &PhasesToolbar::drawCardRequested); + connect(aDrawCard, &QAction::triggered, this, &PhasesToolbar::actDrawCard); PhaseButton *untapButton = new PhaseButton("untap", this, aUntapAll); PhaseButton *upkeepButton = new PhaseButton("upkeep", this); @@ -139,7 +144,7 @@ PhasesToolbar::PhasesToolbar(QGraphicsItem *parent) } nextTurnButton = new PhaseButton("nextturn", this, nullptr, false); - connect(nextTurnButton, &PhaseButton::clicked, this, &PhasesToolbar::nextTurnRequested); + connect(nextTurnButton, &PhaseButton::clicked, this, &PhasesToolbar::actNextTurn); rearrangeButtons(); @@ -260,5 +265,31 @@ void PhasesToolbar::phaseButtonClicked() button->triggerDoubleClickAction(); } - emit setActivePhaseRequested(buttonList.indexOf(button)); + Command_SetActivePhase cmd; + cmd.set_phase(static_cast(buttonList.indexOf(button))); + + emit sendGameCommand(cmd, -1); +} + +void PhasesToolbar::actNextTurn() +{ + emit sendGameCommand(Command_NextTurn(), -1); +} + +void PhasesToolbar::actUntapAll() +{ + Command_SetCardAttr cmd; + cmd.set_zone(ZoneNames::TABLE); + cmd.set_attribute(AttrTapped); + cmd.set_attr_value("0"); + + emit sendGameCommand(cmd, -1); +} + +void PhasesToolbar::actDrawCard() +{ + Command_DrawCards cmd; + cmd.set_number(1); + + emit sendGameCommand(cmd, -1); } diff --git a/cockatrice/src/game_graphics/phases_toolbar.h b/cockatrice/src/game_graphics/phases_toolbar.h index 0565f1351..376775284 100644 --- a/cockatrice/src/game_graphics/phases_toolbar.h +++ b/cockatrice/src/game_graphics/phases_toolbar.h @@ -17,6 +17,14 @@ class QAction; class QTimer; +namespace google +{ +namespace protobuf +{ +class Message; +} +} // namespace google + class PhaseButton : public QObject, public QGraphicsItem { Q_OBJECT @@ -87,16 +95,11 @@ public slots: void triggerPhaseAction(int phase); private slots: void phaseButtonClicked(); + void actNextTurn(); + void actUntapAll(); + void actDrawCard(); signals: - /** @name Intent signals - * The view reports what the user asked for; GameEventHandler builds the commands. - * @{ - */ - void setActivePhaseRequested(int phase); - void nextTurnRequested(); - void untapAllRequested(); - void drawCardRequested(); - /** @} */ + void sendGameCommand(const ::google::protobuf::Message &command, int playerId); protected: void paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) override; diff --git a/libcockatrice_game/libcockatrice/game/player/card_menu_action_type.h b/cockatrice/src/game_graphics/player/card_menu_action_type.h similarity index 95% rename from libcockatrice_game/libcockatrice/game/player/card_menu_action_type.h rename to cockatrice/src/game_graphics/player/card_menu_action_type.h index e5c4cd84e..4cae22716 100644 --- a/libcockatrice_game/libcockatrice/game/player/card_menu_action_type.h +++ b/cockatrice/src/game_graphics/player/card_menu_action_type.h @@ -1,6 +1,6 @@ /** * @file card_menu_action_type.h - * @ingroup GameLogicPlayers + * @ingroup GameMenusPlayers */ //! \todo Document this file. diff --git a/cockatrice/src/game_graphics/player/menu/card_menu.cpp b/cockatrice/src/game_graphics/player/menu/card_menu.cpp index 9f00c0cba..137ec2ed1 100644 --- a/cockatrice/src/game_graphics/player/menu/card_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/card_menu.cpp @@ -3,8 +3,16 @@ #include "../../../client/settings/cache_settings.h" #include "../../../client/settings/card_counter_settings.h" #include "../../../client/settings/shortcuts_settings.h" +#include "../../../game/abstract_game.h" +#include "../../../game/player/player_info.h" +#include "../../../game/player/player_manager.h" +#include "../../../game/zones/card_zone_logic.h" #include "../../board/card_item.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_logic.h" +#include "../../game/zones/view_zone_logic.h" #include "../../game_scene.h" +#include "../card_menu_action_type.h" #include "../player_graphics_item.h" #include "libcockatrice/card/card_info.h" #include "libcockatrice/card/database/card_database_querier.h" @@ -27,14 +35,6 @@ #include #include #include -#include -#include -#include -#include -#include -#include -#include -#include #include #include #include @@ -67,7 +67,7 @@ static QAction *makeAction(QObject *parent, Slot &&slot, bool checkable = false, return a; } -CardMenu::CardMenu(PlayerGraphicsItem *_player, CardItem *_card, bool _shortcutsActive) +CardMenu::CardMenu(PlayerGraphicsItem *_player, const CardItem *_card, bool _shortcutsActive) : player(_player), card(_card), shortcutsActive(_shortcutsActive) { const QList &players = player->getLogic()->getGame()->getPlayerManager()->getPlayers().values(); @@ -86,7 +86,7 @@ CardMenu::CardMenu(PlayerGraphicsItem *_player, CardItem *_card, bool _shortcuts auto *gameScene = player->getGameScene(); // Single selection resolver used by all lambdas — called at trigger time - auto sel = [gameScene]() { return gameScene->selectedCardStates(); }; + auto sel = [gameScene]() { return gameScene->selectedCards(); }; // Unified dispatcher for card menu actions auto invoke = [actions, sel](CardMenuActionType type) { @@ -108,18 +108,18 @@ CardMenu::CardMenu(PlayerGraphicsItem *_player, CardItem *_card, bool _shortcuts aHide = makeAction(this, [actions, sel]() { actions->actHide(sel()); }); aReduceLifeByPower = makeAction(this, [actions, sel]() { actions->actReduceLifeByPower(sel()); }); - // Actions bound to the card this menu was opened for, not to the current selection + // Actions that use activeCard, not selection — direct connection aAttach = new QAction(this); aDrawArrow = new QAction(this); aSelectAll = new QAction(this); aSelectRow = new QAction(this); aSelectColumn = new QAction(this); - connect(aAttach, &QAction::triggered, this, &CardMenu::attachCard); - connect(aDrawArrow, &QAction::triggered, this, &CardMenu::drawArrow); - connect(aSelectAll, &QAction::triggered, this, [this, gameScene]() { gameScene->selectAllInZone(card); }); - connect(aSelectRow, &QAction::triggered, this, [this, gameScene]() { gameScene->selectSameRow(card); }); - connect(aSelectColumn, &QAction::triggered, this, [this, gameScene]() { gameScene->selectSameColumn(card); }); + connect(aAttach, &QAction::triggered, actions, &PlayerActions::actAttach); + connect(aDrawArrow, &QAction::triggered, actions, &PlayerActions::actDrawArrow); + connect(aSelectAll, &QAction::triggered, actions, &PlayerActions::actSelectAll); + connect(aSelectRow, &QAction::triggered, actions, &PlayerActions::actSelectRow); + connect(aSelectColumn, &QAction::triggered, actions, &PlayerActions::actSelectColumn); aRevealToAll = new QAction(this); @@ -337,7 +337,7 @@ void CardMenu::createHandOrCustomZoneMenu(bool canModifyCard) initContextualPlayersMenu(revealMenu, aRevealToAll); connect(revealMenu, &QMenu::triggered, this, [this](QAction *action) { - player->getLogic()->getPlayerActions()->actReveal(player->getGameScene()->selectedCardStates(), action); + player->getLogic()->getPlayerActions()->actReveal(player->getGameScene()->selectedCards(), action); }); addSeparator(); @@ -441,7 +441,6 @@ void CardMenu::addRelatedCardActions() if (relatedCards.isEmpty()) { return; } - auto *playerActions = player->getLogic()->getPlayerActions(); addSeparator(); int index = 0; @@ -485,10 +484,9 @@ void CardMenu::addRelatedCardActions() } auto *createRelated = new QAction(text, this); - const int relatedIndex = index++; - connect(createRelated, &QAction::triggered, this, [this, playerActions, relatedIndex] { - playerActions->actCreateRelatedCard(card->getState(), relatedIndex); - }); + createRelated->setData(QVariant(index++)); + connect(createRelated, &QAction::triggered, player->getLogic()->getPlayerActions(), + &PlayerActions::actCreateRelatedCard); addAction(createRelated); } @@ -497,26 +495,12 @@ void CardMenu::addRelatedCardActions() createRelatedCards->setShortcuts( SettingsCache::instance().shortcuts().getShortcut("Player/aCreateRelatedTokens")); } - connect(createRelatedCards, &QAction::triggered, this, - [this, playerActions] { playerActions->actCreateAllRelatedCards(card->getState()); }); + connect(createRelatedCards, &QAction::triggered, player->getLogic()->getPlayerActions(), + &PlayerActions::actCreateAllRelatedCards); addAction(createRelatedCards); } } -void CardMenu::attachCard() -{ - if (card) { - card->drawAttachArrow(); - } -} - -void CardMenu::drawArrow() -{ - if (card) { - card->drawArrow(Qt::red); - } -} - void CardMenu::retranslateUi() { aSelectAll->setText(tr("&Select All")); diff --git a/cockatrice/src/game_graphics/player/menu/card_menu.h b/cockatrice/src/game_graphics/player/menu/card_menu.h index 2c80483b1..337d77b3e 100644 --- a/cockatrice/src/game_graphics/player/menu/card_menu.h +++ b/cockatrice/src/game_graphics/player/menu/card_menu.h @@ -27,7 +27,7 @@ signals: void cardInfoRequested(const CardRef &cardRef); public: - explicit CardMenu(PlayerGraphicsItem *player, CardItem *card, bool shortcutsActive); + explicit CardMenu(PlayerGraphicsItem *player, const CardItem *card, bool shortcutsActive); void removePlayer(PlayerLogic *playerToRemove); void createTableMenu(bool canModifyCard); void createStackMenu(bool canModifyCard); @@ -53,13 +53,11 @@ public: private: PlayerGraphicsItem *player; - CardItem *card; + const CardItem *card; QList> playersInfo; bool shortcutsActive; void addRelatedCardActions(); - void attachCard(); - void drawArrow(); void retranslateUi(); void initContextualPlayersMenu(QMenu *menu, QAction *allPlayersAction); void setShortcutsActive(); diff --git a/cockatrice/src/game_graphics/player/menu/custom_zone_menu.cpp b/cockatrice/src/game_graphics/player/menu/custom_zone_menu.cpp index 87a0d1540..e16ebc112 100644 --- a/cockatrice/src/game_graphics/player/menu/custom_zone_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/custom_zone_menu.cpp @@ -1,13 +1,13 @@ #include "custom_zone_menu.h" +#include "../../../game/player/player_info.h" +#include "../../game/player/player_logic.h" #include "../../game_scene.h" #include "../player_graphics_item.h" #include #include #include -#include -#include CustomZoneMenu::CustomZoneMenu(PlayerGraphicsItem *_player) : player(_player) { diff --git a/cockatrice/src/game_graphics/player/menu/grave_menu.cpp b/cockatrice/src/game_graphics/player/menu/grave_menu.cpp index 001a4d0cf..580d2642e 100644 --- a/cockatrice/src/game_graphics/player/menu/grave_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/grave_menu.cpp @@ -2,7 +2,13 @@ #include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcuts_settings.h" +#include "../../../game/player/player_info.h" +#include "../../../game/player/player_manager.h" +#include "../../../game/zones/pile_zone_logic.h" #include "../../../interface/widgets/menus/tearoff_menu.h" +#include "../../game/abstract_game.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_logic.h" #include "../player_graphics_item.h" #include @@ -13,12 +19,6 @@ #include #include #include -#include -#include -#include -#include -#include -#include #include class QWidget; diff --git a/cockatrice/src/game_graphics/player/menu/hand_menu.cpp b/cockatrice/src/game_graphics/player/menu/hand_menu.cpp index e992f3ff5..141542cb4 100644 --- a/cockatrice/src/game_graphics/player/menu/hand_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/hand_menu.cpp @@ -2,7 +2,14 @@ #include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcuts_settings.h" +#include "../../../game/board/card_list.h" +#include "../../../game/player/player_info.h" +#include "../../../game/player/player_manager.h" +#include "../../../game/zones/hand_zone_logic.h" #include "../../../interface/widgets/menus/tearoff_menu.h" +#include "../../game/abstract_game.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_logic.h" #include "../player_graphics_item.h" #include @@ -13,13 +20,6 @@ #include #include #include -#include -#include -#include -#include -#include -#include -#include #include class QWidget; diff --git a/cockatrice/src/game_graphics/player/menu/library_menu.cpp b/cockatrice/src/game_graphics/player/menu/library_menu.cpp index 5f90c3760..aa389006e 100644 --- a/cockatrice/src/game_graphics/player/menu/library_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/library_menu.cpp @@ -2,7 +2,14 @@ #include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcuts_settings.h" +#include "../../../game/board/card_list.h" +#include "../../../game/player/player_info.h" +#include "../../../game/player/player_manager.h" +#include "../../../game/zones/pile_zone_logic.h" #include "../../../interface/widgets/menus/tearoff_menu.h" +#include "../../game/abstract_game.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_logic.h" #include "../player_graphics_item.h" #include @@ -16,13 +23,6 @@ #include #include #include -#include -#include -#include -#include -#include -#include -#include class QWidget; diff --git a/cockatrice/src/game_graphics/player/menu/move_menu.cpp b/cockatrice/src/game_graphics/player/menu/move_menu.cpp index b81c4619b..2991100ec 100644 --- a/cockatrice/src/game_graphics/player/menu/move_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/move_menu.cpp @@ -2,13 +2,13 @@ #include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcuts_settings.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_logic.h" #include "../../game_scene.h" +#include "../card_menu_action_type.h" #include "../player_graphics_item.h" #include -#include -#include -#include MoveMenu::MoveMenu(PlayerGraphicsItem *player) : QMenu(tr("Move to")) { @@ -30,7 +30,7 @@ MoveMenu::MoveMenu(PlayerGraphicsItem *player) : QMenu(tr("Move to")) auto invoke = [player](CardMenuActionType type) { return [type, player]() { - player->getLogic()->getPlayerActions()->cardMenuAction(player->getGameScene()->selectedCardStates(), type); + player->getLogic()->getPlayerActions()->cardMenuAction(player->getGameScene()->selectedCards(), type); }; }; diff --git a/cockatrice/src/game_graphics/player/menu/player_menu.cpp b/cockatrice/src/game_graphics/player/menu/player_menu.cpp index 582586ac2..44b184cfe 100644 --- a/cockatrice/src/game_graphics/player/menu/player_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/player_menu.cpp @@ -2,6 +2,10 @@ #include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcuts_settings.h" +#include "../../../game/abstract_game.h" +#include "../../../game/player/player_info.h" +#include "../../../game/player/player_logic.h" +#include "../../../game/player/player_manager.h" #include "../../../game_graphics/zones/hand_zone.h" #include "../../../game_graphics/zones/pile_zone.h" #include "../../../game_graphics/zones/table_zone.h" @@ -22,10 +26,6 @@ #include #include -#include -#include -#include -#include PlayerMenu::PlayerMenu(PlayerGraphicsItem *_player) : QObject(_player), player(_player) { @@ -87,24 +87,22 @@ void PlayerMenu::setMenusForGraphicItems() } } -QMenu *PlayerMenu::updateCardMenu(CardState *card) +QMenu *PlayerMenu::updateCardMenu(const CardItem *card) { if (!card) { emit cardMenuUpdated(nullptr); return nullptr; } - auto *cardItem = qobject_cast(card->parent()); - // If is spectator (as spectators don't need card menus), return // only update the menu if the card is actually selected if ((player->getLogic()->getGame()->getPlayerManager()->isSpectator() && !player->getLogic()->getGame()->getPlayerManager()->isJudge()) || - cardItem == nullptr || player->getGameScene()->getActiveCard() != cardItem) { + player->getLogic()->getGame()->getActiveCard() != card) { return nullptr; } - CardMenu *menu = new CardMenu(player, cardItem, shortcutsActive); + CardMenu *menu = new CardMenu(player, card, shortcutsActive); connect(menu, &CardMenu::cardInfoRequested, this, &PlayerMenu::cardInfoRequested); emit cardMenuUpdated(menu); diff --git a/cockatrice/src/game_graphics/player/menu/player_menu.h b/cockatrice/src/game_graphics/player/menu/player_menu.h index e21a689de..97dd2fc56 100644 --- a/cockatrice/src/game_graphics/player/menu/player_menu.h +++ b/cockatrice/src/game_graphics/player/menu/player_menu.h @@ -14,7 +14,7 @@ #include #include -class CardState; +class CardItem; class CardMenu; class PlayerGraphicsItem; class AbstractPlayerComponent; @@ -43,7 +43,7 @@ signals: public slots: void setMenusForGraphicItems(); - QMenu *updateCardMenu(CardState *card); + QMenu *updateCardMenu(const CardItem *card); private slots: void refreshShortcuts(); diff --git a/cockatrice/src/game_graphics/player/menu/pt_menu.cpp b/cockatrice/src/game_graphics/player/menu/pt_menu.cpp index e6c65fcfb..b4f852b43 100644 --- a/cockatrice/src/game_graphics/player/menu/pt_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/pt_menu.cpp @@ -2,12 +2,12 @@ #include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcuts_settings.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_logic.h" #include "../../game_scene.h" #include "../player_graphics_item.h" #include -#include -#include PtMenu::PtMenu(PlayerGraphicsItem *player) : QMenu(tr("Power / toughness")) { @@ -15,35 +15,34 @@ PtMenu::PtMenu(PlayerGraphicsItem *player) : QMenu(tr("Power / toughness")) aIncP = new QAction(this); connect(aIncP, &QAction::triggered, playerActions, - [player, playerActions] { playerActions->actIncP(player->getGameScene()->selectedCardStates()); }); + [player, playerActions] { playerActions->actIncP(player->getGameScene()->selectedCards()); }); aDecP = new QAction(this); connect(aDecP, &QAction::triggered, playerActions, - [player, playerActions] { playerActions->actDecP(player->getGameScene()->selectedCardStates()); }); + [player, playerActions] { playerActions->actDecP(player->getGameScene()->selectedCards()); }); aIncT = new QAction(this); connect(aIncT, &QAction::triggered, playerActions, - [player, playerActions] { playerActions->actIncT(player->getGameScene()->selectedCardStates()); }); + [player, playerActions] { playerActions->actIncT(player->getGameScene()->selectedCards()); }); aDecT = new QAction(this); connect(aDecT, &QAction::triggered, playerActions, - [player, playerActions] { playerActions->actDecT(player->getGameScene()->selectedCardStates()); }); + [player, playerActions] { playerActions->actDecT(player->getGameScene()->selectedCards()); }); aIncPT = new QAction(this); connect(aIncPT, &QAction::triggered, playerActions, - [player, playerActions] { playerActions->actIncPT(player->getGameScene()->selectedCardStates()); }); + [player, playerActions] { playerActions->actIncPT(player->getGameScene()->selectedCards()); }); aDecPT = new QAction(this); connect(aDecPT, &QAction::triggered, playerActions, - [player, playerActions] { playerActions->actDecPT(player->getGameScene()->selectedCardStates()); }); + [player, playerActions] { playerActions->actDecPT(player->getGameScene()->selectedCards()); }); aFlowP = new QAction(this); connect(aFlowP, &QAction::triggered, playerActions, - [player, playerActions] { playerActions->actFlowP(player->getGameScene()->selectedCardStates()); }); + [player, playerActions] { playerActions->actFlowP(player->getGameScene()->selectedCards()); }); aFlowT = new QAction(this); connect(aFlowT, &QAction::triggered, playerActions, - [player, playerActions] { playerActions->actFlowT(player->getGameScene()->selectedCardStates()); }); + [player, playerActions] { playerActions->actFlowT(player->getGameScene()->selectedCards()); }); aSetPT = new QAction(this); - connect(aSetPT, &QAction::triggered, playerActions, [player, playerActions] { - playerActions->actRequestSetPTDialog(player->getGameScene()->selectedCardStates()); - }); + connect(aSetPT, &QAction::triggered, playerActions, + [player, playerActions] { playerActions->actRequestSetPTDialog(player->getGameScene()->selectedCards()); }); aResetPT = new QAction(this); connect(aResetPT, &QAction::triggered, playerActions, - [player, playerActions] { playerActions->actResetPT(player->getGameScene()->selectedCardStates()); }); + [player, playerActions] { playerActions->actResetPT(player->getGameScene()->selectedCards()); }); addAction(aIncP); addAction(aDecP); diff --git a/cockatrice/src/game_graphics/player/menu/rfg_menu.cpp b/cockatrice/src/game_graphics/player/menu/rfg_menu.cpp index 415d4a2ef..98514280f 100644 --- a/cockatrice/src/game_graphics/player/menu/rfg_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/rfg_menu.cpp @@ -1,6 +1,10 @@ #include "rfg_menu.h" +#include "../../../game/player/player_info.h" +#include "../../../game/zones/pile_zone_logic.h" #include "../../../interface/widgets/menus/tearoff_menu.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_logic.h" #include "../player_graphics_item.h" #include @@ -8,10 +12,6 @@ #include #include #include -#include -#include -#include -#include #include class QWidget; diff --git a/cockatrice/src/game_graphics/player/menu/say_menu.cpp b/cockatrice/src/game_graphics/player/menu/say_menu.cpp index 486508da9..d467c6469 100644 --- a/cockatrice/src/game_graphics/player/menu/say_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/say_menu.cpp @@ -1,14 +1,14 @@ #include "say_menu.h" #include "../../../client/settings/cache_settings.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_logic.h" #include "../player_graphics_item.h" #include #include #include #include -#include -#include #include SayMenu::SayMenu(PlayerGraphicsItem *_player) : player(_player) diff --git a/cockatrice/src/game_graphics/player/menu/sideboard_menu.cpp b/cockatrice/src/game_graphics/player/menu/sideboard_menu.cpp index e601a0e09..1eef3604f 100644 --- a/cockatrice/src/game_graphics/player/menu/sideboard_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/sideboard_menu.cpp @@ -2,13 +2,13 @@ #include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcuts_settings.h" +#include "../../../game/player/player_info.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_logic.h" #include "../player_graphics_item.h" #include #include -#include -#include -#include SideboardMenu::SideboardMenu(PlayerGraphicsItem *player, QMenu *playerMenu) : QMenu(playerMenu) { diff --git a/cockatrice/src/game_graphics/player/menu/utility_menu.cpp b/cockatrice/src/game_graphics/player/menu/utility_menu.cpp index 3b8affff5..7c18b3f4c 100644 --- a/cockatrice/src/game_graphics/player/menu/utility_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/utility_menu.cpp @@ -2,6 +2,9 @@ #include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcuts_settings.h" +#include "../../../game/player/player_info.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_logic.h" #include "../../game_scene.h" #include "../player_graphics_item.h" #include "libcockatrice/card/card_info.h" @@ -11,9 +14,6 @@ #include #include #include -#include -#include -#include UtilityMenu::UtilityMenu(PlayerGraphicsItem *_player, QMenu *playerMenu) : QMenu(playerMenu), player(_player) { @@ -44,7 +44,7 @@ UtilityMenu::UtilityMenu(PlayerGraphicsItem *_player, QMenu *playerMenu) : QMenu aIncrementAllCardCounters = new QAction(this); connect(aIncrementAllCardCounters, &QAction::triggered, playerActions, [this]() { player->getLogic()->getPlayerActions()->actIncrementAllCardCounters( - player->getGameScene()->selectedCardStates()); + player->getGameScene()->selectedCards()); }); createPredefinedTokenMenu = new QMenu(QString()); diff --git a/cockatrice/src/game_graphics/player/player_dialogs.cpp b/cockatrice/src/game_graphics/player/player_dialogs.cpp index a5baa8ca8..74f5de601 100644 --- a/cockatrice/src/game_graphics/player/player_dialogs.cpp +++ b/cockatrice/src/game_graphics/player/player_dialogs.cpp @@ -2,10 +2,11 @@ #include "../../client/settings/cache_settings.h" #include "../../client/settings/card_counter_settings.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_logic.h" #include "../../interface/widgets/utility/get_text_with_max.h" #include "../board/abstract_counter.h" #include "../board/card_item.h" -#include "../dialogs/dlg_annotation.h" #include "../dialogs/dlg_create_token.h" #include "../dialogs/dlg_move_top_cards_until.h" #include "../dialogs/dlg_roll_dice.h" @@ -16,8 +17,6 @@ #include #include #include -#include -#include #include PlayerDialogs::PlayerDialogs(PlayerGraphicsItem *_player, PlayerActions *_playerActions) @@ -35,7 +34,7 @@ PlayerDialogs::PlayerDialogs(PlayerGraphicsItem *_player, PlayerActions *_player &PlayerDialogs::onShuffleBottomDialogRequested); connect(playerActions, &PlayerActions::requestMulliganDialog, this, &PlayerDialogs::onMulliganDialogRequested); - connect(playerActions, &PlayerActions::startingHandSizeChosen, this, &PlayerDialogs::onStartingHandSizeChosen); + connect(playerActions, &PlayerActions::requestDrawCardsDialog, this, &PlayerDialogs::onDrawCardsDialogRequested); connect(playerActions, &PlayerActions::requestMoveTopCardsToDialog, this, @@ -127,11 +126,6 @@ void PlayerDialogs::onMulliganDialogRequested(int startSize, int handSize, int d } } -void PlayerDialogs::onStartingHandSizeChosen(int number) -{ - SettingsCache::instance().userInterface().setStartingHandSize(number); -} - void PlayerDialogs::onDrawCardsDialogRequested(int defaultNumberTopCards, int deckSize) { bool ok; @@ -202,7 +196,7 @@ void PlayerDialogs::onRollDieDialogRequested() playerActions->actRollDie(dlg.getDieSideCount(), dlg.getDiceToRollCount()); } -void PlayerDialogs::onCreateRelatedFromRelationDialogRequested(const CardState *sourceCard, +void PlayerDialogs::onCreateRelatedFromRelationDialogRequested(const CardItem *sourceCard, const CardRelation *cardRelation) { if (sourceCard == nullptr || cardRelation == nullptr) { @@ -257,14 +251,14 @@ void PlayerDialogs::onMoveCardXCardsFromTopDialogRequested(int defaultNumberTopC number -= 1; // indexes start at 0 if (ok) { - playerActions->actMoveCardXCardsFromTop(player->getGameScene()->selectedCardStates(), number); + playerActions->actMoveCardXCardsFromTop(player->getGameScene()->selectedCards(), number); } } void PlayerDialogs::onSetPTDialogRequested(const QString &oldPT) { bool ok; - auto cards = player->getGameScene()->selectedCardStates(); + auto cards = player->getGameScene()->selectedCards(); emit requestDialogSemaphore(true); QString pt = getTextWithMax(dialogParent(), tr("Change power/toughness"), tr("Change stats to:"), QLineEdit::Normal, oldPT, &ok); @@ -279,7 +273,7 @@ void PlayerDialogs::onSetPTDialogRequested(const QString &oldPT) void PlayerDialogs::onSetAnnotationDialogRequested(const QString &oldAnnotation) { - auto cards = player->getGameScene()->selectedCardStates(); + auto cards = player->getGameScene()->selectedCards(); emit requestDialogSemaphore(true); AnnotationDialog *dialog = new AnnotationDialog(dialogParent()); dialog->setOptions(QInputDialog::UsePlainTextEditForTextInput); @@ -297,7 +291,7 @@ void PlayerDialogs::onSetAnnotationDialogRequested(const QString &oldAnnotation) void PlayerDialogs::onSetCardCounterDialogRequested(int counterId, const QString &oldValueForDlg) { - auto cards = player->getGameScene()->selectedCardStates(); + auto cards = player->getGameScene()->selectedCards(); emit requestDialogSemaphore(true); auto &cardCounterSettings = SettingsCache::instance().cardCounters(); diff --git a/cockatrice/src/game_graphics/player/player_dialogs.h b/cockatrice/src/game_graphics/player/player_dialogs.h index 4ac443f3c..86d7282a2 100644 --- a/cockatrice/src/game_graphics/player/player_dialogs.h +++ b/cockatrice/src/game_graphics/player/player_dialogs.h @@ -17,7 +17,6 @@ class CardItem; class CardRelation; -class CardState; class PlayerActions; class QWidget; struct MoveTopCardsUntilOptions; @@ -53,13 +52,12 @@ public slots: bool faceDown); void onDrawBottomCardsDialogRequested(int defaultNumberBottomCards, int maxCards); void onRollDieDialogRequested(); - void onCreateRelatedFromRelationDialogRequested(const CardState *sourceCard, const CardRelation *cardRelation); + void onCreateRelatedFromRelationDialogRequested(const CardItem *sourceCard, const CardRelation *cardRelation); void onCreateTokenDialogRequested(const QStringList &predefinedTokens); void onMoveCardXCardsFromTopDialogRequested(int defaultNumberTopCardsToPlaceBelow, int deckSize); void onSetPTDialogRequested(const QString &oldPT); void onSetAnnotationDialogRequested(const QString &oldAnnotation); void onSetCardCounterDialogRequested(int counterId, const QString &oldValueForDlg); - void onStartingHandSizeChosen(int number); private: PlayerGraphicsItem *player; diff --git a/cockatrice/src/game_graphics/player/player_graphics_item.cpp b/cockatrice/src/game_graphics/player/player_graphics_item.cpp index 0805dbb5b..419816087 100644 --- a/cockatrice/src/game_graphics/player/player_graphics_item.cpp +++ b/cockatrice/src/game_graphics/player/player_graphics_item.cpp @@ -1,6 +1,15 @@ #include "player_graphics_item.h" #include "../../client/settings/cache_settings.h" +#include "../../game/board/counter_state.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_info.h" +#include "../../game/player/player_logic.h" +#include "../../game/zones/card_zone_logic.h" +#include "../../game/zones/hand_zone_logic.h" +#include "../../game/zones/pile_zone_logic.h" +#include "../../game/zones/stack_zone_logic.h" +#include "../../game/zones/table_zone_logic.h" #include "../../interface/card_picture_loader/card_picture_loader.h" #include "../../interface/widgets/cards/art_crop_attribution.h" #include "../../interface/widgets/cards/card_art_utils.h" @@ -36,15 +45,6 @@ #include #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include #include #include @@ -177,22 +177,6 @@ void PlayerGraphicsItem::initializeZones() void PlayerGraphicsItem::onCustomZoneAdded(QString customZoneName) { - // Custom zones have no board view, but their cards still need a graphics owner. - // Without one, PileZone::onCardAdded never runs and cards moved from the table - // or hand into a custom zone stay drawn at their old spot. Give them a hidden - // pile zone so added cards are reparented to (0, 0) and hidden. - CardZoneLogic *logic = player->getZone(customZoneName); - if (auto *pileLogic = qobject_cast(logic)) { - auto *hiddenZone = new PileZone(pileLogic, this); - hiddenZone->setVisible(false); - hiddenZone->setPos(0, 0); - zoneGraphicsItems.insert(customZoneName, hiddenZone); - // Hide any cards that arrived before the graphics item existed. - for (CardState *state : pileLogic->getCards()) { - hiddenZone->onCardAdded(state); - } - return; - } zoneGraphicsItems.insert(customZoneName, nullptr); // Custom zone view goes here, if we ever implement it. } diff --git a/cockatrice/src/game_graphics/player/player_target.cpp b/cockatrice/src/game_graphics/player/player_target.cpp index 1cd97e7b5..24734cd42 100644 --- a/cockatrice/src/game_graphics/player/player_target.cpp +++ b/cockatrice/src/game_graphics/player/player_target.cpp @@ -1,6 +1,8 @@ #include "player_target.h" #include "../../client/settings/cache_settings.h" +#include "../../game/player/player_info.h" +#include "../../game/player/player_logic.h" #include "../../interface/pixel_map_generator.h" #include "../board/abstract_counter.h" #include "../board/abstract_graphics_item.h" @@ -28,8 +30,6 @@ #include #include #include -#include -#include #include #include #include diff --git a/cockatrice/src/game_graphics/tally/stats_tally.cpp b/cockatrice/src/game_graphics/tally/stats_tally.cpp index 2bce9fdfd..28e280c02 100644 --- a/cockatrice/src/game_graphics/tally/stats_tally.cpp +++ b/cockatrice/src/game_graphics/tally/stats_tally.cpp @@ -16,7 +16,7 @@ static int sumPowers(const QList &cards) // calculate total power; int total = 0; for (auto card : cards) { - 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); @@ -44,7 +44,7 @@ static int sumToughness(const QList &cards) { int total = 0; for (auto card : cards) { - QVariantList parsed = CardState::parsePT(card->getPT()); + QVariantList parsed = CardItem::parsePT(card->getPT()); if (parsed.size() == 2) { int toughness = parsed.at(1).toInt(); // toInt will default to 0 if it's not an int total += qMax(toughness, 0); diff --git a/cockatrice/src/game_graphics/zones/card_zone.cpp b/cockatrice/src/game_graphics/zones/card_zone.cpp index fd3b5849f..d09bb588c 100644 --- a/cockatrice/src/game_graphics/zones/card_zone.cpp +++ b/cockatrice/src/game_graphics/zones/card_zone.cpp @@ -1,13 +1,13 @@ #include "card_zone.h" +#include "../../game/board/card_list.h" +#include "../../game/zones/card_zone_logic.h" #include "../board/abstract_graphics_item.h" #include "../board/card_item.h" #include #include #include -#include -#include #include class QGraphicsItem; @@ -18,60 +18,23 @@ CardZone::CardZone(CardZoneLogic *_logic, QGraphicsItem *parent) : AbstractGraphicsItem(parent), menu(nullptr), doubleClickAction(0), logic(_logic) { connect(logic, &CardZoneLogic::retranslateUi, this, &CardZone::retranslateUi); - // Invalidation is connected before any consumer so a handler that reads cardItems() - // during the same signal emission always sees a stale flag and rebuilds. - connect(logic, &CardZoneLogic::cardAdded, this, &CardZone::invalidateItems); - connect(logic, &CardZoneLogic::cardCountChanged, this, &CardZone::invalidateItems); - connect(logic, &CardZoneLogic::reorganizeCards, this, &CardZone::invalidateItems); connect(logic, &CardZoneLogic::cardAdded, this, &CardZone::onCardAdded); - connect(logic, &CardZoneLogic::createCardRequested, this, &CardZone::onCreateCardRequested); connect(logic, &CardZoneLogic::setGraphicsVisibility, this, [this](bool v) { this->setVisible(v); }); connect(logic, &CardZoneLogic::updateGraphics, this, [this]() { update(); }); connect(logic, &CardZoneLogic::reorganizeCards, this, &CardZone::reorganizeCards); } -void CardZone::invalidateItems() +void CardZone::onCardAdded(CardItem *addedCard) { - itemsDirty = true; -} - -const QList &CardZone::cardItems() const -{ - if (itemsDirty) { - items.clear(); - for (auto *cardState : getLogic()->getCards()) { - if (auto *cardItem = qobject_cast(cardState->parent())) { - items.append(cardItem); - } - } - itemsDirty = false; - } - return items; -} - -void CardZone::onCardAdded(CardState *addedCard) -{ - auto *addedItem = addedCard == nullptr ? nullptr : qobject_cast(addedCard->parent()); - if (addedItem == nullptr) { - return; - } - addedItem->setParentItem(this); - addedItem->setVisible(true); - addedItem->update(); -} - -void CardZone::onCreateCardRequested(const ServerInfo_Card &cardInfo, bool reorganize) -{ - CardRef cardRef = {QString::fromStdString(cardInfo.name()), QString::fromStdString(cardInfo.provider_id())}; - auto *card = new CardItem(getLogic()->getPlayer(), nullptr, cardRef, cardInfo.id()); - card->processCardInfo(cardInfo); - getLogic()->addCard(card->getState(), reorganize, cardInfo.x(), cardInfo.y()); + addedCard->setParentItem(this); + addedCard->setVisible(true); + addedCard->update(); } void CardZone::retranslateUi() { - for (auto *card : cardItems()) { - card->retranslateUi(); + for (int i = 0; i < getLogic()->getCards().size(); ++i) { + getLogic()->getCards()[i]->retranslateUi(); } } diff --git a/cockatrice/src/game_graphics/zones/card_zone.h b/cockatrice/src/game_graphics/zones/card_zone.h index ea77cb057..def3eee14 100644 --- a/cockatrice/src/game_graphics/zones/card_zone.h +++ b/cockatrice/src/game_graphics/zones/card_zone.h @@ -16,17 +16,13 @@ class CardDragItem; class CardItem; -class CardState; class CardZoneLogic; class QAction; class QGraphicsItem; class QMenu; class QPoint; -class ServerInfo_Card; template class QList; -class CardItem; - inline Q_LOGGING_CATEGORY(CardZoneLog, "card_zone"); /** @@ -59,9 +55,7 @@ public slots: * Virtual so subclasses (e.g. SelectZone) can override parenting behavior — the Qt signal * connection in CardZone's constructor dispatches through the vtable. */ - virtual void onCardAdded(CardState *addedCard); - /** @brief Builds the card item for @p cardInfo and hands it to this zone's logic. */ - void onCreateCardRequested(const ServerInfo_Card &cardInfo, bool reorganize); + virtual void onCardAdded(CardItem *addedCard); public: enum @@ -82,15 +76,6 @@ public: return logic; } - /** - * @brief The CardItems backing getLogic()->getCards(), in zone order. - * - * Game logic talks about cards as states; this is the graphics side's view of the same list. - * It is rebuilt on demand after any content change, so views never reach into the logic's - * container themselves and never observe a stale one. - */ - [[nodiscard]] const QList &cardItems() const; - void setMenu(QMenu *_menu, QAction *_doubleClickAction = 0) { menu = _menu; @@ -98,12 +83,7 @@ public: } private: - /** @brief Marks the cached card items stale. Connected to every content-changing signal. */ - void invalidateItems(); - CardZoneLogic *logic; - mutable QList items; - mutable bool itemsDirty = true; }; #endif diff --git a/cockatrice/src/game_graphics/zones/hand_zone.cpp b/cockatrice/src/game_graphics/zones/hand_zone.cpp index 31758c455..3e527c081 100644 --- a/cockatrice/src/game_graphics/zones/hand_zone.cpp +++ b/cockatrice/src/game_graphics/zones/hand_zone.cpp @@ -1,6 +1,11 @@ #include "hand_zone.h" #include "../../client/settings/cache_settings.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_info.h" +#include "../../game/player/player_logic.h" +#include "../../game/zones/card_zone_logic.h" +#include "../../game/zones/hand_zone_logic.h" #include "../../interface/theme_manager.h" #include "../board/card_drag_item.h" #include "../board/card_item.h" @@ -13,11 +18,7 @@ #include #include #include -#include -#include -#include -#include -#include +#include #include #include @@ -47,8 +48,8 @@ void HandZone::handleDropEvent(const QList &dragItems, QPoint point = dropPoint + scenePos().toPoint(); int x = -1; if (SettingsCache::instance().userInterface().getHorizontalHand()) { - for (x = 0; x < cardItems().size(); x++) { - if (point.x() < cardItems().at(x)->scenePos().x()) { + for (x = 0; x < getLogic()->getCards().size(); x++) { + if (point.x() < static_cast(getLogic()->getCards().at(x))->scenePos().x()) { break; } } @@ -57,13 +58,19 @@ void HandZone::handleDropEvent(const QList &dragItems, x = calcDropIndexFromY(dropPoint.y(), !sameZone); } - QList cards; - cards.reserve(dragItems.size()); - for (const CardDragItem *item : dragItems) { - cards.append(CardMoveRequest{item->getId()}); + Command_MoveCard cmd; + cmd.set_start_player_id(startZone->getPlayer()->getPlayerInfo()->getId()); + cmd.set_start_zone(startZone->getName().toStdString()); + cmd.set_target_player_id(getLogic()->getPlayer()->getPlayerInfo()->getId()); + cmd.set_target_zone(getLogic()->getName().toStdString()); + cmd.set_x(x); + cmd.set_y(-1); + + for (int i = 0; i < dragItems.size(); ++i) { + cmd.mutable_cards_to_move()->add_card()->set_card_id(dragItems[i]->getId()); } - getLogic()->getPlayer()->getPlayerActions()->moveCards(startZone, getLogic(), x, -1, cards); + getLogic()->getPlayer()->getPlayerActions()->sendGameCommand(cmd); } QRectF HandZone::boundingRect() const @@ -83,23 +90,23 @@ void HandZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*optio void HandZone::reorganizeCards() { - if (!cardItems().isEmpty()) { - const int cardCount = cardItems().size(); + if (!getLogic()->getCards().isEmpty()) { + const int cardCount = getLogic()->getCards().size(); if (SettingsCache::instance().userInterface().getHorizontalHand()) { bool leftJustified = SettingsCache::instance().userInterface().getLeftJustified(); - qreal cardWidth = cardItems().at(0)->boundingRect().width(); + qreal cardWidth = getLogic()->getCards().at(0)->boundingRect().width(); const int xPadding = leftJustified ? cardWidth * 1.4 : 5; qreal totalWidth = leftJustified ? boundingRect().width() - (1 * xPadding) - 5 : boundingRect().width() - 2 * xPadding; if (cardCount == 1) { - CardItem *c = cardItems().at(0); + CardItem *c = getLogic()->getCards().at(0); qreal xPosition = leftJustified ? xPadding : xPadding + (totalWidth - cardWidth) / 2; c->setPos(xPosition, 5); c->setRealZValue(0); } else { for (int i = 0; i < cardCount; i++) { - CardItem *c = cardItems().at(i); + CardItem *c = getLogic()->getCards().at(i); // If the total width of the cards is smaller than the available width, // the cards do not need to overlap and are displayed in the center of the area. if (cardWidth * cardCount > totalWidth) { @@ -124,7 +131,7 @@ void HandZone::reorganizeCards() void HandZone::sortHand(const QList &options) { - if (cardItems().isEmpty()) { + if (getLogic()->getCards().isEmpty()) { return; } diff --git a/cockatrice/src/game_graphics/zones/hand_zone.h b/cockatrice/src/game_graphics/zones/hand_zone.h index ed8f1e8d5..5c91d684d 100644 --- a/cockatrice/src/game_graphics/zones/hand_zone.h +++ b/cockatrice/src/game_graphics/zones/hand_zone.h @@ -7,10 +7,10 @@ #ifndef HANDZONE_H #define HANDZONE_H +#include "../../game/board/card_list.h" #include "select_zone.h" #include -#include #include class HandZoneLogic; diff --git a/cockatrice/src/game_graphics/zones/pile_zone.cpp b/cockatrice/src/game_graphics/zones/pile_zone.cpp index 08af241b6..77a87a4af 100644 --- a/cockatrice/src/game_graphics/zones/pile_zone.cpp +++ b/cockatrice/src/game_graphics/zones/pile_zone.cpp @@ -1,6 +1,12 @@ #include "pile_zone.h" #include "../../client/settings/cache_settings.h" +#include "../../game/board/card_list.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_info.h" +#include "../../game/player/player_logic.h" +#include "../../game/zones/card_zone_logic.h" +#include "../../game/zones/pile_zone_logic.h" #include "../board/card_drag_item.h" #include "../board/card_item.h" #include "../card_dimensions.h" @@ -18,12 +24,7 @@ #include #include #include -#include -#include -#include -#include -#include -#include +#include #include #include #include @@ -66,35 +67,36 @@ void PileZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*optio { painter->drawPath(shape()); - if (!cardItems().isEmpty()) { - cardItems().at(0)->paintPicture(painter, cardItems().at(0)->getTranslatedSize(painter), 90); + if (!getLogic()->getCards().isEmpty()) { + getLogic()->getCards().at(0)->paintPicture(painter, getLogic()->getCards().at(0)->getTranslatedSize(painter), + 90); } painter->translate(CardDimensions::WIDTH_HALF_F, CardDimensions::HEIGHT_HALF_F); painter->rotate(-90); painter->translate(-CardDimensions::WIDTH_HALF_F, -CardDimensions::HEIGHT_HALF_F); - paintNumberEllipse(cardItems().size(), 28, Qt::white, -1, -1, painter); + paintNumberEllipse(getLogic()->getCards().size(), 28, Qt::white, -1, -1, painter); } void PileZone::handleDropEvent(const QList &dragItems, CardZoneLogic *startZone, const QPoint &) { - QList cards; - cards.reserve(dragItems.size()); - for (const CardDragItem *item : dragItems) { - cards.append(CardMoveRequest{item->getId(), item->isForceFaceDown()}); + Command_MoveCard cmd; + cmd.set_start_player_id(startZone->getPlayer()->getPlayerInfo()->getId()); + cmd.set_start_zone(startZone->getName().toStdString()); + cmd.set_target_player_id(getLogic()->getPlayer()->getPlayerInfo()->getId()); + cmd.set_target_zone(getLogic()->getName().toStdString()); + cmd.set_x(0); + cmd.set_y(0); + + for (int i = 0; i < dragItems.size(); ++i) { + auto cardToMove = cmd.mutable_cards_to_move()->add_card(); + cardToMove->set_card_id(dragItems[i]->getId()); + if (dragItems[i]->isForceFaceDown()) { + cardToMove->set_face_down(true); + } } - getLogic()->getPlayer()->getPlayerActions()->moveCards(startZone, getLogic(), 0, 0, cards); -} - -void PileZone::onCardAdded(CardState *addedCard) -{ - CardZone::onCardAdded(addedCard); - auto *addedItem = addedCard == nullptr ? nullptr : qobject_cast(addedCard->parent()); - if (addedItem) { - addedItem->setPos(0, 0); - addedItem->setVisible(false); - } + getLogic()->getPlayer()->getPlayerActions()->sendGameCommand(cmd); } void PileZone::reorganizeCards() @@ -124,14 +126,15 @@ void PileZone::mouseMoveEvent(QGraphicsSceneMouseEvent *event) return; } - if (cardItems().isEmpty()) { + if (getLogic()->getCards().isEmpty()) { return; } bool forceFaceDown = event->modifiers().testFlag(Qt::ShiftModifier); bool bottomCard = event->modifiers().testFlag(Qt::ControlModifier); - CardItem *card = bottomCard ? cardItems().last() : cardItems().first(); - const int cardid = getLogic()->contentsKnown() ? card->getId() : (bottomCard ? cardItems().size() - 1 : 0); + CardItem *card = bottomCard ? getLogic()->getCards().last() : getLogic()->getCards().first(); + const int cardid = + getLogic()->contentsKnown() ? card->getId() : (bottomCard ? getLogic()->getCards().size() - 1 : 0); CardDragItem *drag = card->createDragItem(cardid, event->pos(), event->scenePos(), forceFaceDown); drag->grabMouse(); setCursor(Qt::OpenHandCursor); @@ -144,8 +147,8 @@ void PileZone::mouseReleaseEvent(QGraphicsSceneMouseEvent * /*event*/) void PileZone::hoverEnterEvent(QGraphicsSceneHoverEvent *event) { - if (!cardItems().isEmpty()) { - cardItems()[0]->processHoverEvent(); + if (!getLogic()->getCards().isEmpty()) { + getLogic()->getCards()[0]->processHoverEvent(); } QGraphicsItem::hoverEnterEvent(event); } diff --git a/cockatrice/src/game_graphics/zones/pile_zone.h b/cockatrice/src/game_graphics/zones/pile_zone.h index fa510ad52..b2d8d0cd2 100644 --- a/cockatrice/src/game_graphics/zones/pile_zone.h +++ b/cockatrice/src/game_graphics/zones/pile_zone.h @@ -33,7 +33,6 @@ public: [[nodiscard]] QPainterPath shape() const override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; void reorganizeCards() override; - void onCardAdded(CardState *addedCard) override; void handleDropEvent(const QList &dragItems, CardZoneLogic *startZone, const QPoint &dropPoint) override; diff --git a/cockatrice/src/game_graphics/zones/select_zone.cpp b/cockatrice/src/game_graphics/zones/select_zone.cpp index ccac9decc..d69ec32f5 100644 --- a/cockatrice/src/game_graphics/zones/select_zone.cpp +++ b/cockatrice/src/game_graphics/zones/select_zone.cpp @@ -1,6 +1,8 @@ #include "select_zone.h" #include "../../client/settings/cache_settings.h" +#include "../../game/board/card_list.h" +#include "../../game/zones/card_zone_logic.h" #include "../board/card_item.h" #include "../game_scene.h" @@ -15,8 +17,6 @@ #include #include #include -#include -#include #include #include #include @@ -85,7 +85,7 @@ SelectZone *SelectZone::findOwningSelectZone(const QGraphicsItem *card) SelectZone::StackLayoutParams SelectZone::buildStackParams(qreal minOffset) const { - const auto &cards = cardItems(); + const auto &cards = getLogic()->getCards(); if (cards.isEmpty()) { return {0, boundingRect().height(), 0.0, 0.0, minOffset}; } @@ -97,7 +97,7 @@ SelectZone::StackLayoutParams SelectZone::buildStackParams(qreal minOffset) cons int SelectZone::calcDropIndexFromY(qreal dropY, bool allowCountExpand, qreal minOffset) const { - const auto &cards = cardItems(); + const auto &cards = getLogic()->getCards(); if (cards.isEmpty()) { return 0; } @@ -115,7 +115,7 @@ void SelectZone::restoreStaleEscapedCards() if (!cardClipContainer) { return; } - for (auto *card : cardItems()) { + for (auto *card : getLogic()->getCards()) { // A card parented to the zone (instead of the clip container) should // only occur while it is actively hovered. If hover cleanup was // missed, reparent it back so clipping resumes. @@ -127,7 +127,7 @@ void SelectZone::restoreStaleEscapedCards() void SelectZone::layoutCardsVertically(const StackLayoutParams ¶ms) { - const auto &cards = cardItems(); + const auto &cards = getLogic()->getCards(); if (cards.isEmpty() || params.cardCount <= 0) { return; } @@ -162,9 +162,9 @@ SelectZone::~SelectZone() if (cardClipContainer) { // Reparent any hover-escaped cards back to the clip container so Qt's // parent-child tree is consistent for destruction. setParentItem() does - // not invalidate cardItems() — it only moves the item in the graphics - // tree, it never changes the zone's contents. - for (auto *card : cardItems()) { + // not invalidate getLogic()->getCards() (it modifies the graphics tree, + // not the zone's logical card list). + for (auto *card : getLogic()->getCards()) { if (card && card->parentItem() == this) { card->setParentItem(cardClipContainer); } @@ -172,13 +172,12 @@ SelectZone::~SelectZone() } } -void SelectZone::onCardAdded(CardState *addedCard) +void SelectZone::onCardAdded(CardItem *addedCard) { - auto *addedItem = addedCard == nullptr ? nullptr : qobject_cast(addedCard->parent()); - if (cardClipContainer && addedItem) { - addedItem->setParentItem(cardClipContainer); - addedItem->setVisible(true); - addedItem->update(); + if (cardClipContainer && addedCard) { + addedCard->setParentItem(cardClipContainer); + addedCard->setVisible(true); + addedCard->update(); } else { CardZone::onCardAdded(addedCard); } @@ -248,7 +247,7 @@ void SelectZone::mouseMoveEvent(QGraphicsSceneMouseEvent *event) } QRectF selectionRect = QRectF(selectionOrigin, pos).normalized(); - for (auto card : cardItems()) { + for (auto card : getLogic()->getCards()) { if (card->getAttachedTo() && card->getAttachedTo()->getZone() != getLogic()) { continue; } diff --git a/cockatrice/src/game_graphics/zones/select_zone.h b/cockatrice/src/game_graphics/zones/select_zone.h index 8c81f0dbf..8a56949e3 100644 --- a/cockatrice/src/game_graphics/zones/select_zone.h +++ b/cockatrice/src/game_graphics/zones/select_zone.h @@ -35,7 +35,7 @@ public: SelectZone(CardZoneLogic *logic, QGraphicsItem *parent = nullptr); ~SelectZone() override; - void onCardAdded(CardState *addedCard) override; + void onCardAdded(CardItem *addedCard) override; /** * @brief Temporarily reparents a card from the clip container to this zone so hover scaling is visible beyond clip diff --git a/cockatrice/src/game_graphics/zones/stack_zone.cpp b/cockatrice/src/game_graphics/zones/stack_zone.cpp index d46a4bd94..de1ff8ba9 100644 --- a/cockatrice/src/game_graphics/zones/stack_zone.cpp +++ b/cockatrice/src/game_graphics/zones/stack_zone.cpp @@ -1,5 +1,11 @@ #include "stack_zone.h" +#include "../../game/board/card_list.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_info.h" +#include "../../game/player/player_logic.h" +#include "../../game/zones/card_zone_logic.h" +#include "../../game/zones/stack_zone_logic.h" #include "../../interface/theme_manager.h" #include "../board/card_drag_item.h" #include "../board/card_item.h" @@ -13,12 +19,7 @@ #include #include #include -#include -#include -#include -#include -#include -#include +#include #include #include @@ -72,21 +73,31 @@ void StackZone::handleDropEvent(const QList &dragItems, int index = calcDropIndexFromY(dropPoint.y(), !sameZone, MIN_CARD_VISIBLE); if (sameZone) { // Same-zone no-op: don't move a card onto itself - const auto &cards = cardItems(); + const auto &cards = getLogic()->getCards(); if (!cards.isEmpty() && cards.at(index)->getId() == dragItems.at(0)->getId()) { return; } } - QList cards; - cards.reserve(dragItems.size()); + Command_MoveCard cmd; + cmd.set_start_player_id(startZone->getPlayer()->getPlayerInfo()->getId()); + cmd.set_start_zone(startZone->getName().toStdString()); + cmd.set_target_player_id(getLogic()->getPlayer()->getPlayerInfo()->getId()); + cmd.set_target_zone(getLogic()->getName().toStdString()); + cmd.set_x(index); + cmd.set_y(0); + for (const CardDragItem *item : dragItems) { if (item) { - cards.append(CardMoveRequest{item->getId(), item->isForceFaceDown()}); + auto *cardToMove = cmd.mutable_cards_to_move()->add_card(); + cardToMove->set_card_id(item->getId()); + if (item->isForceFaceDown()) { + cardToMove->set_face_down(true); + } } } - getLogic()->getPlayer()->getPlayerActions()->moveCards(startZone, getLogic(), index, 0, cards); + getLogic()->getPlayer()->getPlayerActions()->sendGameCommand(cmd); } void StackZone::setHeight(qreal newHeight) @@ -102,7 +113,7 @@ void StackZone::setHeight(qreal newHeight) void StackZone::reorganizeCards() { - if (!cardItems().isEmpty()) { + if (!getLogic()->getCards().isEmpty()) { const auto params = buildStackParams(MIN_CARD_VISIBLE); layoutCardsVertically(params); } diff --git a/cockatrice/src/game_graphics/zones/table_zone.cpp b/cockatrice/src/game_graphics/zones/table_zone.cpp index 0dffab353..b704b79e0 100644 --- a/cockatrice/src/game_graphics/zones/table_zone.cpp +++ b/cockatrice/src/game_graphics/zones/table_zone.cpp @@ -1,6 +1,12 @@ #include "table_zone.h" #include "../../client/settings/cache_settings.h" +#include "../../game/board/card_list.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_info.h" +#include "../../game/player/player_logic.h" +#include "../../game/zones/card_zone_logic.h" +#include "../../game/zones/table_zone_logic.h" #include "../../interface/theme_manager.h" #include "../board/abstract_card_item.h" #include "../board/card_drag_item.h" @@ -26,12 +32,8 @@ #include #include #include -#include -#include -#include -#include -#include -#include +#include +#include #include #include #include @@ -60,8 +62,7 @@ TableZone::TableZone(TableZoneLogic *_logic, bool _mirrored, QGraphicsItem *pare updateBg(); - height = MARGIN_TOP + MARGIN_BOTTOM + TableZoneLogic::ROW_COUNT * CardDimensions::HEIGHT + - (TableZoneLogic::ROW_COUNT - 1) * PADDING_Y; + height = MARGIN_TOP + MARGIN_BOTTOM + TABLEROWS * CardDimensions::HEIGHT + (TABLEROWS - 1) * PADDING_Y; width = MIN_WIDTH; currentMinimumWidth = width; @@ -207,20 +208,29 @@ void TableZone::handleDropEventByGrid(const QList &dragItems, CardZoneLogic *startZone, const QPoint &gridPoint) { - QList cards; - cards.reserve(dragItems.size()); - for (const CardDragItem *item : dragItems) { - CardMoveRequest card{item->getId(), item->isForceFaceDown()}; + Command_MoveCard cmd; + cmd.set_start_player_id(startZone->getPlayer()->getPlayerInfo()->getId()); + cmd.set_start_zone(startZone->getName().toStdString()); + cmd.set_target_player_id(getLogic()->getPlayer()->getPlayerInfo()->getId()); + cmd.set_target_zone(getLogic()->getName().toStdString()); + cmd.set_x(gridPoint.x()); + cmd.set_y(gridPoint.y()); + + for (const auto &item : dragItems) { + CardToMove *ctm = cmd.mutable_cards_to_move()->add_card(); + ctm->set_card_id(item->getId()); + if (item->isForceFaceDown()) { + ctm->set_face_down(true); + } if (startZone->getName() != getLogic()->getName() && !item->isForceFaceDown()) { - const auto &cardInfo = item->getItem()->getCard(); - if (cardInfo) { - card.pt = cardInfo.getInfo().getPowTough(); + const auto &card = item->getItem()->getCard(); + if (card) { + ctm->set_pt(card.getInfo().getPowTough().toStdString()); } } - cards.append(card); } - startZone->getPlayer()->getPlayerActions()->moveCards(startZone, getLogic(), gridPoint.x(), gridPoint.y(), cards); + startZone->getPlayer()->getPlayerActions()->sendGameCommand(cmd); } void TableZone::reorganizeCards() @@ -228,8 +238,8 @@ void TableZone::reorganizeCards() // Calculate card stack widths so mapping functions work properly computeCardStackWidths(); - for (int i = 0; i < cardItems().size(); ++i) { - QPoint gridPoint = cardItems()[i]->getGridPos(); + for (int i = 0; i < getLogic()->getCards().size(); ++i) { + QPoint gridPoint = getLogic()->getCards()[i]->getGridPos(); if (gridPoint.x() == -1) { continue; } @@ -238,24 +248,21 @@ void TableZone::reorganizeCards() qreal x = mapPoint.x(); qreal y = mapPoint.y(); - int numberAttachedCards = cardItems()[i]->getAttachedCards().size(); + int numberAttachedCards = getLogic()->getCards()[i]->getAttachedCards().size(); qreal actualX = x + numberAttachedCards * STACKED_CARD_OFFSET_X; qreal actualY = y; if (numberAttachedCards) { actualY += 15; } - cardItems()[i]->setPos(actualX, actualY); - cardItems()[i]->setRealZValue(ZValues::tableCardZValue(actualX, actualY)); + getLogic()->getCards()[i]->setPos(actualX, actualY); + getLogic()->getCards()[i]->setRealZValue(ZValues::tableCardZValue(actualX, actualY)); - QListIterator attachedCardIterator(cardItems()[i]->getAttachedCards()); + QListIterator attachedCardIterator(getLogic()->getCards()[i]->getAttachedCards()); int j = 0; while (attachedCardIterator.hasNext()) { ++j; - CardItem *attachedCard = qobject_cast(attachedCardIterator.next()->parent()); - if (attachedCard == nullptr) { - continue; - } + CardItem *attachedCard = attachedCardIterator.next(); qreal childX = actualX - j * STACKED_CARD_OFFSET_X; qreal childY = y + 5; attachedCard->setPos(childX, childY); @@ -269,18 +276,35 @@ void TableZone::reorganizeCards() void TableZone::toggleTapped() { - QList selectedCards; - for (auto *item : scene()->selectedItems()) { - auto *card = qgraphicsitem_cast(item); - if (card && card->getZone()->getName() == ZoneNames::TABLE) { - selectedCards.append(card->getState()); + QList selectedItemsRaw = scene()->selectedItems(); + QList selectedItems; + + auto isCardOnTable = [](const QGraphicsItem *item) { + if (auto card = qgraphicsitem_cast(item)) { + return card->getZone()->getName() == ZoneNames::TABLE; + } + return false; + }; + + std::copy_if(selectedItemsRaw.begin(), selectedItemsRaw.end(), std::back_inserter(selectedItems), isCardOnTable); + + bool tapAll = std::any_of(selectedItems.begin(), selectedItems.end(), [](const QGraphicsItem *item) { + return !qgraphicsitem_cast(item)->getTapped(); + }); + QList cmdList; + for (const auto &selectedItem : selectedItems) { + CardItem *temp = qgraphicsitem_cast(selectedItem); + if (temp->getTapped() != tapAll) { + Command_SetCardAttr *cmd = new Command_SetCardAttr; + cmd->set_zone(getLogic()->getName().toStdString()); + cmd->set_card_id(temp->getId()); + cmd->set_attribute(AttrTapped); + cmd->set_attr_value(tapAll ? "1" : "0"); + cmdList.append(cmd); } } - - const bool tapAll = std::any_of(selectedCards.begin(), selectedCards.end(), - [](const CardState *card) { return !card->getTapped(); }); - - getLogic()->getPlayer()->getPlayerActions()->actToggleTapped(getLogic()->getName(), selectedCards, tapAll); + getLogic()->getPlayer()->getPlayerActions()->sendGameCommand( + getLogic()->getPlayer()->getPlayerActions()->prepareGameCommand(cmdList)); } void TableZone::resizeToContents() @@ -288,9 +312,9 @@ void TableZone::resizeToContents() int xMax = 0; // Find rightmost card position, which includes the left margin amount. - for (int i = 0; i < cardItems().size(); ++i) { - if (cardItems()[i]->pos().x() > xMax) { - xMax = (int)cardItems()[i]->pos().x(); + for (int i = 0; i < getLogic()->getCards().size(); ++i) { + if (getLogic()->getCards()[i]->pos().x() > xMax) { + xMax = (int)getLogic()->getCards()[i]->pos().x(); } } @@ -311,9 +335,9 @@ void TableZone::resizeToContents() CardItem *TableZone::getCardFromGrid(const QPoint &gridPoint) const { - for (int i = 0; i < cardItems().size(); i++) { - if (cardItems().at(i)->getGridPoint() == gridPoint) { - return cardItems().at(i); + for (int i = 0; i < getLogic()->getCards().size(); i++) { + if (getLogic()->getCards().at(i)->getGridPoint() == gridPoint) { + return getLogic()->getCards().at(i); } } return 0; @@ -330,8 +354,8 @@ void TableZone::computeCardStackWidths() // Each card stack is three grid points worth of card locations. // First pass: compute the number of cards at each card stack. QMap cardStackCount; - for (int i = 0; i < cardItems().size(); ++i) { - const QPoint &gridPoint = cardItems()[i]->getGridPos(); + for (int i = 0; i < getLogic()->getCards().size(); ++i) { + const QPoint &gridPoint = getLogic()->getCards()[i]->getGridPos(); if (gridPoint.x() == -1) { continue; } @@ -342,8 +366,8 @@ void TableZone::computeCardStackWidths() // Second pass: compute the width at each card stack. cardStackWidth.clear(); - for (int i = 0; i < cardItems().size(); ++i) { - const QPoint &gridPoint = cardItems()[i]->getGridPos(); + for (int i = 0; i < getLogic()->getCards().size(); ++i) { + const QPoint &gridPoint = getLogic()->getCards()[i]->getGridPos(); if (gridPoint.x() == -1) { continue; } @@ -351,8 +375,8 @@ void TableZone::computeCardStackWidths() const int key = getCardStackMapKey(gridPoint.x() / 3, gridPoint.y()); const int stackCount = cardStackCount.value(key, 0); if (stackCount == 1) { - cardStackWidth.insert(key, CardDimensions::WIDTH + - cardItems()[i]->getAttachedCards().size() * STACKED_CARD_OFFSET_X); + cardStackWidth.insert(key, CardDimensions::WIDTH + getLogic()->getCards()[i]->getAttachedCards().size() * + STACKED_CARD_OFFSET_X); } else { cardStackWidth.insert(key, CardDimensions::WIDTH + (stackCount - 1) * STACKED_CARD_OFFSET_X); } @@ -373,7 +397,7 @@ QPointF TableZone::mapFromGrid(QPoint gridPoint) const } if (isInverted()) { - gridPoint.setY(TableZoneLogic::ROW_COUNT - 1 - gridPoint.y()); + gridPoint.setY(TABLEROWS - 1 - gridPoint.y()); } // Start with margin plus stacked card offset @@ -399,10 +423,10 @@ QPoint TableZone::mapToGrid(const QPointF &mapPoint) const const int gridPointHeight = CardDimensions::HEIGHT + PADDING_Y; int gridPointY = (y + PADDING_Y / 2) / gridPointHeight; - gridPointY = TableZoneLogic::clampValidTableRow(gridPointY); + gridPointY = clampValidTableRow(gridPointY); if (isInverted()) { - gridPointY = TableZoneLogic::ROW_COUNT - 1 - gridPointY; + gridPointY = TABLEROWS - 1 - gridPointY; } // Calculating the x-coordinate of the grid space requires adding up the @@ -447,3 +471,22 @@ QPointF TableZone::closestGridPoint(const QPointF &point) } return mapFromGrid(gridPoint); } + +int TableZone::clampValidTableRow(const int row) +{ + if (row < 0) { + return 0; + } + if (row >= TABLEROWS) { + return TABLEROWS - 1; + } + return row; +} + +int TableZone::tableRowToGridY(int tableRow) +{ + if (tableRow > 2) { + tableRow = 1; + } + return clampValidTableRow(2 - tableRow); +} diff --git a/cockatrice/src/game_graphics/zones/table_zone.h b/cockatrice/src/game_graphics/zones/table_zone.h index 8aa4936ad..ccd1d8e81 100644 --- a/cockatrice/src/game_graphics/zones/table_zone.h +++ b/cockatrice/src/game_graphics/zones/table_zone.h @@ -17,7 +17,6 @@ #include #include #include -#include #include class CardDragItem; @@ -26,6 +25,7 @@ class CardZoneLogic; class QColor; class QGraphicsItem; class QPainter; +class TableZoneLogic; template class QList; /** @@ -42,6 +42,8 @@ signals: void sizeChanged(); private: + static const int TABLEROWS = 3; + /* Margins between table edges and cards, paddings between cards */ @@ -179,6 +181,15 @@ public: QPointF closestGridPoint(const QPointF &point) override; + static int clampValidTableRow(const int row); + + /** + * Converts a card's logical table row (0=creatures, 1=noncreatures, 2=lands) + * to the corresponding grid Y coordinate. Cards with tableRow > 2 (e.g., + * instants/sorceries) default to the noncreatures row. + */ + static int tableRowToGridY(int tableRow); + /** Resizes the TableZone in case CardItems are within or outside of the TableZone constraints. diff --git a/cockatrice/src/game_graphics/zones/view_zone.cpp b/cockatrice/src/game_graphics/zones/view_zone.cpp index be2e471d4..a2aa124dc 100644 --- a/cockatrice/src/game_graphics/zones/view_zone.cpp +++ b/cockatrice/src/game_graphics/zones/view_zone.cpp @@ -1,6 +1,11 @@ #include "view_zone.h" #include "../../client/settings/cache_settings.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_info.h" +#include "../../game/player/player_logic.h" +#include "../../game/zones/card_zone_logic.h" +#include "../../game/zones/view_zone_logic.h" #include "../board/card_drag_item.h" #include "../board/card_item.h" #include "../card_dimensions.h" @@ -21,11 +26,8 @@ #include #include #include -#include -#include -#include -#include -#include +#include +#include #include #include #include @@ -40,23 +42,21 @@ class QGraphicsItem; ZoneViewZone::ZoneViewZone(ZoneViewZoneLogic *_logic, QGraphicsItem *parent) : SelectZone(_logic, parent), bRect(QRectF()), minRows(0), groupBy(CardList::NoSort), sortBy(CardList::NoSort) { - auto *viewLogic = qobject_cast(getLogic()); - if (!(viewLogic->getRevealZone() && !viewLogic->getWriteableRevealZone())) { - viewLogic->getOriginalZone()->getViews().append(viewLogic); + if (!(qobject_cast(getLogic())->getRevealZone() && + !qobject_cast(getLogic())->getWriteableRevealZone())) { + qobject_cast(getLogic())->getOriginalZone()->getViews().append(this); } connect(_logic, &ZoneViewZoneLogic::closeView, this, &ZoneViewZone::close); } void ZoneViewZone::addToViews() { - auto *viewLogic = qobject_cast(getLogic()); - viewLogic->getOriginalZone()->getViews().append(viewLogic); + qobject_cast(getLogic())->getOriginalZone()->getViews().append(this); } void ZoneViewZone::removeFromViews() { - auto *viewLogic = qobject_cast(getLogic()); - viewLogic->getOriginalZone()->getViews().removeOne(viewLogic); + qobject_cast(getLogic())->getOriginalZone()->getViews().removeOne(this); } /** @@ -66,9 +66,9 @@ void ZoneViewZone::removeFromViews() void ZoneViewZone::close() { emit closed(); - auto *viewLogic = qobject_cast(getLogic()); - if (!(viewLogic->getRevealZone() && !viewLogic->getWriteableRevealZone())) { - viewLogic->getOriginalZone()->getViews().removeOne(viewLogic); + if (!(qobject_cast(getLogic())->getRevealZone() && + !qobject_cast(getLogic())->getWriteableRevealZone())) { + qobject_cast(getLogic())->getOriginalZone()->getViews().removeOne(this); } deleteLater(); } @@ -95,27 +95,28 @@ void ZoneViewZone::initializeCards(const QList &cardLis auto copy = new CardItem(getLogic()->getPlayer(), this, cardRef, card->id()); copy->setFaceDown(card->face_down()); - getLogic()->addCard(copy->getState(), false, i); + getLogic()->addCard(copy, false, i); } reorganizeCards(); } else if (!qobject_cast(getLogic())->getOriginalZone()->contentsKnown()) { - auto *viewLogic = qobject_cast(getLogic()); - PendingCommand *pend = viewLogic->prepareZoneDump(); - if (pend != nullptr) { - // A local game answers synchronously, so the handler has to be connected before the - // command is sent or the response would be delivered to nobody. - connect(pend, &PendingCommand::finished, this, &ZoneViewZone::zoneDumpReceived); - viewLogic->sendZoneDump(pend); - } + Command_DumpZone cmd; + cmd.set_player_id(getLogic()->getPlayer()->getPlayerInfo()->getId()); + cmd.set_zone_name(getLogic()->getName().toStdString()); + cmd.set_number_cards(numberCards); + cmd.set_is_reversed(qobject_cast(getLogic())->getIsReversed()); + + PendingCommand *pend = getLogic()->getPlayer()->getPlayerActions()->prepareGameCommand(cmd); + connect(pend, &PendingCommand::finished, this, &ZoneViewZone::zoneDumpReceived); + getLogic()->getPlayer()->getPlayerActions()->sendGameCommand(pend); } else { const CardList &c = qobject_cast(getLogic())->getOriginalZone()->getCards(); int number = numberCards == -1 ? c.size() : (numberCards < c.size() ? numberCards : c.size()); for (int i = 0; i < number; i++) { - CardState *card = c.at(i); + CardItem *card = c.at(i); auto copy = new CardItem(getLogic()->getPlayer(), this, card->getCardRef(), card->getId()); copy->setFaceDown(card->getFaceDown()); - getLogic()->addCard(copy->getState(), false, i); + getLogic()->addCard(copy, false, i); } reorganizeCards(); } @@ -131,7 +132,7 @@ void ZoneViewZone::zoneDumpReceived(const Response &r) auto cardProviderId = QString::fromStdString(cardInfo.provider_id()); auto card = new CardItem(getLogic()->getPlayer(), this, {cardName, cardProviderId}, cardInfo.id(), getLogic()); card->setFaceDown(cardInfo.face_down()); - getLogic()->rawInsertCard(card->getState(), i); + getLogic()->rawInsertCard(card, i); } qobject_cast(getLogic())->updateCardIds(ZoneViewZoneLogic::INITIALIZE); @@ -145,11 +146,11 @@ void ZoneViewZone::zoneDumpReceived(const Response &r) void ZoneViewZone::reorganizeCards() { // filter cards - CardList cardsToDisplay = CardList(getLogic()->contentsKnown()); - for (auto card : cardItems()) { + CardList cardsToDisplay = CardList(getLogic()->getCards().getContentsKnown()); + for (auto card : getLogic()->getCards()) { if (filterString.check(card->getCard().getCardPtr())) { card->show(); - cardsToDisplay.append(card->getState()); + cardsToDisplay.append(card); } else { card->hide(); } @@ -217,8 +218,7 @@ ZoneViewZone::GridSize ZoneViewZone::positionCardsForDisplay(CardList &cards, Ca const auto extractor = CardList::getExtractorFor(pileOption); for (int i = 0; i < cardCount; i++) { - CardState *c = cards.at(i); - CardItem *cItem = qobject_cast(c->parent()); + CardItem *c = cards.at(i); QString columnProp = extractor(c); if (i) { // if not the first card @@ -233,8 +233,8 @@ ZoneViewZone::GridSize ZoneViewZone::positionCardsForDisplay(CardList &cards, Ca lastColumnProp = columnProp; qreal x = col * CardDimensions::WIDTH_F; qreal y = row * CardDimensions::HEIGHT_F / 3; - cItem->setPos(HORIZONTAL_PADDING + x, VERTICAL_PADDING + y); - cItem->setRealZValue(i); + c->setPos(HORIZONTAL_PADDING + x, VERTICAL_PADDING + y); + c->setRealZValue(i); longestRow = qMax(row, longestRow); } @@ -259,11 +259,11 @@ ZoneViewZone::GridSize ZoneViewZone::positionCardsForDisplay(CardList &cards, Ca qCDebug(ViewZoneLog) << "reorganizeCards: rows=" << rows << "cols=" << cols; for (int i = 0; i < cardCount; i++) { - auto *cItem = qobject_cast(cards.at(i)->parent()); + CardItem *c = cards.at(i); qreal x = (i / rows) * CardDimensions::WIDTH_F; qreal y = (i % rows) * CardDimensions::HEIGHT_F / 3; - cItem->setPos(HORIZONTAL_PADDING + x, VERTICAL_PADDING + y); - cItem->setRealZValue(i); + c->setPos(HORIZONTAL_PADDING + x, VERTICAL_PADDING + y); + c->setRealZValue(i); } return GridSize{rows, qMax(cols, 1)}; @@ -301,13 +301,24 @@ void ZoneViewZone::handleDropEvent(const QList &dragItems, CardZoneLogic *startZone, const QPoint & /*dropPoint*/) { - QList cards; - cards.reserve(dragItems.size()); + Command_MoveCard cmd; + cmd.set_start_player_id(startZone->getPlayer()->getPlayerInfo()->getId()); + cmd.set_start_zone(startZone->getName().toStdString()); + cmd.set_target_player_id(getLogic()->getPlayer()->getPlayerInfo()->getId()); + cmd.set_target_zone(getLogic()->getName().toStdString()); + cmd.set_x(0); + cmd.set_y(0); + cmd.set_is_reversed(qobject_cast(getLogic())->getIsReversed()); + for (int i = 0; i < dragItems.size(); ++i) { - cards.append(CardMoveRequest{dragItems[i]->getId(), dragItems[i]->isForceFaceDown()}); + auto cardToMove = cmd.mutable_cards_to_move()->add_card(); + cardToMove->set_card_id(dragItems[i]->getId()); + if (dragItems[i]->isForceFaceDown()) { + cardToMove->set_face_down(true); + } } - getLogic()->getPlayer()->getPlayerActions()->moveCards(startZone, getLogic(), 0, 0, cards); + getLogic()->getPlayer()->getPlayerActions()->sendGameCommand(cmd); } void ZoneViewZone::setGeometry(const QRectF &rect) diff --git a/cockatrice/src/game_graphics/zones/view_zone.h b/cockatrice/src/game_graphics/zones/view_zone.h index f91b24834..4032da760 100644 --- a/cockatrice/src/game_graphics/zones/view_zone.h +++ b/cockatrice/src/game_graphics/zones/view_zone.h @@ -7,6 +7,7 @@ #ifndef ZONEVIEWERZONE_H #define ZONEVIEWERZONE_H +#include "../../game/board/card_list.h" #include "select_zone.h" #include @@ -16,7 +17,6 @@ #include #include #include -#include #include #include diff --git a/cockatrice/src/game_graphics/zones/view_zone_widget.cpp b/cockatrice/src/game_graphics/zones/view_zone_widget.cpp index b1ca42ee1..8c82d6401 100644 --- a/cockatrice/src/game_graphics/zones/view_zone_widget.cpp +++ b/cockatrice/src/game_graphics/zones/view_zone_widget.cpp @@ -1,7 +1,13 @@ #include "view_zone_widget.h" #include "../../client/settings/cache_settings.h" +#include "../../client/translation.h" #include "../../filters/syntax_help.h" +#include "../../game/board/card_list.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_logic.h" +#include "../../game/zones/card_zone_logic.h" +#include "../../game/zones/view_zone_logic.h" #include "../../interface/pixel_map_generator.h" #include "../card_dimensions.h" #include "../z_values.h" @@ -39,12 +45,7 @@ #include #include #include -#include -#include -#include -#include -#include -#include +#include #include #include #include @@ -177,10 +178,9 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player, vbox->addItem(zoneHBox); - zone = - new ZoneViewZone(new ZoneViewZoneLogic(player, _origZone, numberCards, _revealZone, _writeableRevealZone, - _isReversed, &SettingsCache::instance().userInterface(), zoneContainer), - zoneContainer); + zone = new ZoneViewZone(new ZoneViewZoneLogic(player, _origZone, numberCards, _revealZone, _writeableRevealZone, + _isReversed, zoneContainer), + zoneContainer); connect(zone, &ZoneViewZone::wheelEventReceived, scrollBarProxy, &ScrollableGraphicsProxyWidget::recieveWheelEvent); retranslateUi(); @@ -570,7 +570,7 @@ void ZoneViewWidget::closeEvent(QCloseEvent *event) // manually call zone->close in order to remove it from the origZones views zone->close(); if (shuffleCheckBox.isChecked()) { - player->getPlayerActions()->actShuffle(); + player->getPlayerActions()->sendGameCommand(Command_Shuffle()); } zoneDeleted(); event->accept(); diff --git a/cockatrice/src/installer_close_server.cpp b/cockatrice/src/installer_close_server.cpp deleted file mode 100644 index 26f0b2c87..000000000 --- a/cockatrice/src/installer_close_server.cpp +++ /dev/null @@ -1,81 +0,0 @@ -#include "installer_close_server.h" - -#include -#include -#include -#include -#include -#include - -namespace -{ -/// The one line the probe writes, from the header both ends include. A QByteArray view of -/// the shared literal, so the client cannot drift from the probe's copy. -const QByteArray CLOSE_COMMAND = QByteArray::fromRawData(INSTALLER_CLOSE_COMMAND, sizeof(INSTALLER_CLOSE_COMMAND) - 1); - -/// How long a connection that never sends the command may stay open. The probe writes and -/// closes at once, so this only reaps a stray client; it must not run while a close request -/// is being handled, which is why it is a timer on the socket and not a disconnected slot. -constexpr int STRAY_CONNECTION_MILLISECONDS = 5000; -} // namespace - -InstallerCloseServer::InstallerCloseServer(QObject *parent) : QObject(parent) -{ -} - -QString InstallerCloseServer::socketNameForUser(qint64 processId, const QString &userName) -{ - return QStringLiteral("CockatriceInstallerClose-%1-%2").arg(processId).arg(userName); -} - -QString InstallerCloseServer::socketName(qint64 processId) -{ - // The same scoping helper the single-instance socket uses, so the probe only has to - // reproduce one rule (USER, then USERNAME, then the home directory name). - return scopedLocalServerName(QStringLiteral("CockatriceInstallerClose-%1").arg(processId)); -} - -bool InstallerCloseServer::listen() -{ - if (server != nullptr) { - return server->isListening(); - } - name = socketName(QCoreApplication::applicationPid()); - server = new QLocalServer(this); - // A crashed instance can leave a Unix socket file behind; on Windows this is a no-op. - QLocalServer::removeServer(name); - if (!server->listen(name)) { - delete server; - server = nullptr; - return false; - } - - connect(server, &QLocalServer::newConnection, this, [this]() { - while (QLocalSocket *socket = server->nextPendingConnection()) { - // No disconnected -> deleteLater() slot: the close request can spin a nested - // event loop, the probe disconnects while it runs, and a deferred delete would - // then free the socket from inside its own readyRead emission. The reaper covers - // a client that never sends the line; the handler below deletes after the emit. - auto *reaper = new QTimer(socket); - reaper->setSingleShot(true); - connect(reaper, &QTimer::timeout, socket, &QObject::deleteLater); - reaper->start(STRAY_CONNECTION_MILLISECONDS); - connect(socket, &QLocalSocket::readyRead, this, [this, socket, reaper]() { - if (!socket->canReadLine()) { - return; - } - if (socket->readLine() != CLOSE_COMMAND) { - return; - } - // Stop the reaper before the emit: the receiver may close the window and run - // a nested event loop for as long as the user takes, and the reaper must not - // free the socket from inside that loop while this handler still holds it. - reaper->stop(); - emit closeRequested(); - socket->disconnectFromServer(); - socket->deleteLater(); - }); - } - }); - return true; -} diff --git a/cockatrice/src/installer_close_server.h b/cockatrice/src/installer_close_server.h deleted file mode 100644 index 847c35523..000000000 --- a/cockatrice/src/installer_close_server.h +++ /dev/null @@ -1,50 +0,0 @@ -#ifndef COCKATRICE_INSTALLER_CLOSE_SERVER_H -#define COCKATRICE_INSTALLER_CLOSE_SERVER_H - -#include -#include - -class QLocalServer; - -/** - * @brief Listens for the installer's request to close this instance gracefully. - * - * The Windows installer cannot reach an instance whose window is hidden in the notification - * area with WM_CLOSE, and it must not kill a process that may hold an unsaved deck. It - * connects to this local socket instead and writes one line; the instance then closes the - * same way its own close button does - saving settings and answering its own confirmation, - * never bypassing it. - * - * The socket name carries the process id, so every instance has its own and an installer - * that found several can ask each one. It is built by installerCloseSocketName() in - * tools/installer-holder-probe/installer_close_name.h; the two must stay identical. - */ -class InstallerCloseServer : public QObject -{ - Q_OBJECT -public: - explicit InstallerCloseServer(QObject *parent = nullptr); - - /** - * @brief Starts listening. A failure is not fatal: the installer falls back to - * WM_CLOSE, which is what older clients answer. - */ - bool listen(); - - /// The socket name this process listens on. The installer's probe builds the same - /// string from the process id and the account name. - static QString socketName(qint64 processId); - - /// The name for an explicit account, exposed so the probe's name helper and this one - /// can be tested against each other. - static QString socketNameForUser(qint64 processId, const QString &userName); - -signals: - void closeRequested(); - -private: - QLocalServer *server = nullptr; - QString name; -}; - -#endif // COCKATRICE_INSTALLER_CLOSE_SERVER_H diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_file_format.cpp b/cockatrice/src/interface/deck_loader/deck_file_format.cpp similarity index 100% rename from libcockatrice_deck_list/libcockatrice/deck_list/deck_file_format.cpp rename to cockatrice/src/interface/deck_loader/deck_file_format.cpp diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_file_format.h b/cockatrice/src/interface/deck_loader/deck_file_format.h similarity index 100% rename from libcockatrice_deck_list/libcockatrice/deck_list/deck_file_format.h rename to cockatrice/src/interface/deck_loader/deck_file_format.h diff --git a/cockatrice/src/interface/deck_loader/deck_loader.h b/cockatrice/src/interface/deck_loader/deck_loader.h index 39a12bed4..16402b14b 100644 --- a/cockatrice/src/interface/deck_loader/deck_loader.h +++ b/cockatrice/src/interface/deck_loader/deck_loader.h @@ -7,14 +7,13 @@ #ifndef DECK_LOADER_H #define DECK_LOADER_H +#include "deck_file_format.h" +#include "loaded_deck.h" + #include #include -#include #include #include -#include -#include -#include #include #include diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/loaded_deck.cpp b/cockatrice/src/interface/deck_loader/loaded_deck.cpp similarity index 100% rename from libcockatrice_deck_list/libcockatrice/deck_list/loaded_deck.cpp rename to cockatrice/src/interface/deck_loader/loaded_deck.cpp diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/loaded_deck.h b/cockatrice/src/interface/deck_loader/loaded_deck.h similarity index 100% rename from libcockatrice_deck_list/libcockatrice/deck_list/loaded_deck.h rename to cockatrice/src/interface/deck_loader/loaded_deck.h diff --git a/cockatrice/src/interface/intents/intent_open_local_deck.cpp b/cockatrice/src/interface/intents/intent_open_local_deck.cpp index 20704a551..5b673d106 100644 --- a/cockatrice/src/interface/intents/intent_open_local_deck.cpp +++ b/cockatrice/src/interface/intents/intent_open_local_deck.cpp @@ -1,14 +1,14 @@ #include "intent_open_local_deck.h" +#include "../deck_loader/deck_file_format.h" #include "../deck_loader/deck_loader.h" +#include "../deck_loader/loaded_deck.h" #include "../widgets/tabs/tab_supervisor.h" #include "intent_wait_for_database_load.h" #include "libcockatrice/card/database/card_database.h" #include "libcockatrice/card/database/card_database_loader.h" #include -#include -#include #include IntentOpenLocalDeck::IntentOpenLocalDeck(TabSupervisor *_tabSupervisor, const QString &_file) diff --git a/cockatrice/src/interface/intents/intent_open_shared_deck.h b/cockatrice/src/interface/intents/intent_open_shared_deck.h index 2d9dc5528..afce2370f 100644 --- a/cockatrice/src/interface/intents/intent_open_shared_deck.h +++ b/cockatrice/src/interface/intents/intent_open_shared_deck.h @@ -1,6 +1,7 @@ #ifndef COCKATRICE_INTENT_OPEN_SHARED_DECK_H #define COCKATRICE_INTENT_OPEN_SHARED_DECK_H +#include "../deck_loader/loaded_deck.h" #include "contexts/context_open_deck.h" #include "intent.h" @@ -8,7 +9,6 @@ #include #include #include -#include #include #include diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp index a7e231ecf..d4715c8d5 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp @@ -1,6 +1,7 @@ #include "deck_state_manager.h" #include "../../../client/settings/cache_settings.h" +#include "../../deck_loader/loaded_deck.h" #include "deck_list_model.h" #include "libcockatrice/card/card_info.h" #include "libcockatrice/card/database/card_database_querier.h" @@ -17,7 +18,6 @@ #include #include #include -#include #include #include #include diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h index 54c59eb79..98876947b 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h @@ -1,7 +1,7 @@ #ifndef COCKATRICE_DECK_STATE_MANAGER_H #define COCKATRICE_DECK_STATE_MANAGER_H -#include "deck_list_model.h" +#include "../../deck_loader/loaded_deck.h" #include #include @@ -10,7 +10,6 @@ #include #include #include -#include #include class DeckListHistoryManager; diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.cpp index 5659b3388..31841b9cd 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.cpp @@ -1,6 +1,7 @@ #include "dlg_convert_deck_to_cod_format.h" #include "../../../client/settings/cache_settings.h" +#include "../../deck_loader/deck_file_format.h" #include #include @@ -11,7 +12,6 @@ #include #include #include -#include #include #include diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.h b/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.h index 6cf6021b0..f3cbb7b62 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.h @@ -10,10 +10,8 @@ #include "libcockatrice/deck_list/deck_list.h" -#include #include #include -#include #include class QPlainTextEdit; diff --git a/cockatrice/src/interface/widgets/general/home_widget.cpp b/cockatrice/src/interface/widgets/general/home_widget.cpp index 5905b2c40..7555d1f45 100644 --- a/cockatrice/src/interface/widgets/general/home_widget.cpp +++ b/cockatrice/src/interface/widgets/general/home_widget.cpp @@ -2,7 +2,9 @@ #include "../../../client/settings/cache_settings.h" #include "../../../interface/widgets/tabs/tab_supervisor.h" +#include "../../deck_loader/deck_file_format.h" #include "../../deck_loader/deck_loader.h" +#include "../../deck_loader/loaded_deck.h" #include "../../pixel_map_generator.h" #include "../../theme_config.h" #include "../../theme_manager.h" @@ -55,8 +57,6 @@ #include #include #include -#include -#include #include #include #include diff --git a/cockatrice/src/interface/widgets/onboarding/pages/welcome_page.cpp b/cockatrice/src/interface/widgets/onboarding/pages/welcome_page.cpp index b2d5ab1e0..71d2fa409 100644 --- a/cockatrice/src/interface/widgets/onboarding/pages/welcome_page.cpp +++ b/cockatrice/src/interface/widgets/onboarding/pages/welcome_page.cpp @@ -5,7 +5,6 @@ #include "../../settings_page/general_settings_page.h" #include "../first_run_wizard_page.h" #include "libcockatrice/settings/personal_settings.h" -#include "libcockatrice/utility/translation_loader.h" #include #include @@ -31,7 +30,7 @@ WelcomePage::WelcomePage(QWidget *parent) : FirstRunWizardPage(parent) languageLabel = new QLabel(this); langCombo = new QComboBox(this); for (const QString &code : GeneralSettingsPage::findQmFiles()) { - langCombo->addItem(TranslationLoader::languageDisplayName(code), code); + langCombo->addItem(GeneralSettingsPage::languageName(code), code); } QString current = SettingsCache::instance().personal().getLang(); diff --git a/cockatrice/src/interface/widgets/replay/replay_manager.cpp b/cockatrice/src/interface/widgets/replay/replay_manager.cpp index 8ba0cb653..f3f1da2c4 100644 --- a/cockatrice/src/interface/widgets/replay/replay_manager.cpp +++ b/cockatrice/src/interface/widgets/replay/replay_manager.cpp @@ -1,13 +1,13 @@ #include "replay_manager.h" #include "../../../client/settings/cache_settings.h" +#include "../../../game/player/event_processing_options.h" #include "libcockatrice/protocol/pb/game_event.pb.h" #include "libcockatrice/protocol/pb/game_event_container.pb.h" #include "libcockatrice/protocol/pb/game_replay.pb.h" #include #include -#include #include #include #include diff --git a/cockatrice/src/interface/widgets/replay/replay_manager.h b/cockatrice/src/interface/widgets/replay/replay_manager.h index 3849b86ae..3b33deada 100644 --- a/cockatrice/src/interface/widgets/replay/replay_manager.h +++ b/cockatrice/src/interface/widgets/replay/replay_manager.h @@ -1,10 +1,11 @@ #ifndef COCKATRICE_REPLAY_MANAGER_H #define COCKATRICE_REPLAY_MANAGER_H +#include "../../../game/player/event_processing_options.h" + #include #include #include -#include #include class GameReplay; diff --git a/cockatrice/src/interface/widgets/replay/replay_widget.h b/cockatrice/src/interface/widgets/replay/replay_widget.h index 0e78442ce..74aace570 100644 --- a/cockatrice/src/interface/widgets/replay/replay_widget.h +++ b/cockatrice/src/interface/widgets/replay/replay_widget.h @@ -8,8 +8,9 @@ #ifndef REPLAY_WIDGET_H #define REPLAY_WIDGET_H +#include "../../../game/player/event_processing_options.h" + #include -#include #include class ReplayManager; diff --git a/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp b/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp index 447e63673..0b5523f67 100644 --- a/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp @@ -1,5 +1,8 @@ #include "user_context_menu.h" +#include "../../../../game/abstract_game.h" +#include "../../../../game/game_event_handler.h" +#include "../../../../game/game_meta_info.h" #include "../../dialogs/dlg_report_user.h" #include "../../interface/widgets/tabs/tab_supervisor.h" #include "../chat_view/chat_view.h" @@ -34,9 +37,6 @@ #include #include #include -#include -#include -#include #include #include #include diff --git a/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp index 7b89b744d..2b885237b 100644 --- a/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp @@ -8,6 +8,7 @@ #include "update/client/release_channel.h" #include +#include #include #include #include @@ -21,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -49,24 +51,18 @@ enum startupCardUpdateCheckBehaviorIndex GeneralSettingsPage::GeneralSettingsPage() { // language settings - for (const QString &code : findQmFiles()) { - languageBox.addItem(TranslationLoader::languageDisplayName(code), code); + QStringList languageCodes = findQmFiles(); + for (const QString &code : languageCodes) { + QString langName = languageName(code); + languageBox.addItem(langName, code); } - // Select the language the client runs with: its own entry, the base language for a variant without a - // file of its own (de_CH -> de), and the default language for an unset setting, which names no file. - const QString configuredLanguage = SettingsCache::instance().personal().getLang(); - int index = languageBox.findData(configuredLanguage); - if (index < 0) { - index = languageBox.findData(configuredLanguage.section('_', 0, 0)); - } - if (index < 0) { - index = languageBox.findData(TranslationLoader::defaultLanguageCode()); - } - if (index >= 0) { - languageBox.setCurrentIndex(index); + QString setLanguage = QCoreApplication::translate("i18n", DEFAULT_LANG_NAME); + int index = languageBox.findText(setLanguage, Qt::MatchExactly); + if (index == -1) { + qWarning() << "could not find language" << setLanguage; } else { - qCWarning(GeneralSettingsPageLog) << "could not find language" << configuredLanguage; + languageBox.setCurrentIndex(index); } advertiseTranslationPageLabel.setTextInteractionFlags(Qt::LinksAccessibleByMouse); @@ -100,12 +96,12 @@ GeneralSettingsPage::GeneralSettingsPage() auto *languageGrid = new QGridLayout; languageGrid->addWidget(&languageLabel, 0, 0); languageGrid->addWidget(&languageBox, 0, 1); - languageGrid->addWidget(&advertiseTranslationPageLabel, 1, 1, Qt::AlignRight); - languageGrid->addWidget(&cardLanguageLabel, 2, 0); - languageGrid->addWidget(&cardLanguageBox, 2, 1); - languageGrid->addWidget(&cardLanguageNoteLabel, 3, 1); - languageGrid->addWidget(&cardSearchLanguageLabel, 4, 0); - languageGrid->addWidget(&cardSearchLanguageBox, 4, 1); + languageGrid->addWidget(&cardLanguageLabel, 1, 0); + languageGrid->addWidget(&cardLanguageBox, 1, 1); + languageGrid->addWidget(&cardLanguageNoteLabel, 2, 1); + languageGrid->addWidget(&cardSearchLanguageLabel, 3, 0); + languageGrid->addWidget(&cardSearchLanguageBox, 3, 1); + languageGrid->addWidget(&advertiseTranslationPageLabel, 4, 1, Qt::AlignRight); cardLanguageNoteLabel.setWordWrap(true); cardLanguageNoteLabel.setAlignment(Qt::AlignLeft | Qt::AlignVCenter); @@ -388,6 +384,18 @@ QStringList GeneralSettingsPage::findQmFiles() TranslationLoader::applicationTranslationPaths(translationPath)); } +QString GeneralSettingsPage::languageName(const QString &lang) +{ + QTranslator qTranslator; + + // Only the language name is needed here, so the file is loaded without logging a line per + // language: findQmFiles() asked for languages whose files exist in the first place. + TranslationLoader::loadFrom(qTranslator, translationPrefix + "_" + lang, + TranslationLoader::applicationTranslationPaths(translationPath)); + + return qTranslator.translate("i18n", DEFAULT_LANG_NAME); +} + void GeneralSettingsPage::deckPathButtonClicked() { QString path = QFileDialog::getExistingDirectory(this, tr("Choose path"), deckPathEdit->text()); @@ -568,7 +576,7 @@ void GeneralSettingsPage::retranslateUi() const auto &settings = SettingsCache::instance(); languageGroupBox->setTitle(tr("Language settings")); - languageLabel.setText(tr("Interface language:")); + languageLabel.setText(tr("Language:")); cardLanguageBox.setItemText(0, tr("English")); cardLanguageLabel.setText(tr("Card text & images language:")); cardLanguageNoteLabel.setText( diff --git a/cockatrice/src/interface/widgets/settings_page/general_settings_page.h b/cockatrice/src/interface/widgets/settings_page/general_settings_page.h index 9451db434..20564c968 100644 --- a/cockatrice/src/interface/widgets/settings_page/general_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/general_settings_page.h @@ -26,6 +26,7 @@ public: void retranslateUi() override; static QStringList findQmFiles(); + static QString languageName(const QString &lang); signals: /// Request to re-import the card database with the newly selected card language diff --git a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp index 1e80b2715..4044f03db 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp @@ -11,7 +11,9 @@ #include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcuts_settings.h" +#include "../../deck_loader/deck_file_format.h" #include "../../deck_loader/deck_loader.h" +#include "../../deck_loader/loaded_deck.h" #include "../cards/additional_info/deck_color_identity.h" #include "../cards/card_info_frame_widget.h" #include "../client/network/interfaces/deck_stats_interface.h" @@ -53,8 +55,6 @@ #include #include #include -#include -#include #include #include #include diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp index 46d2466c6..3abf3f9aa 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp @@ -2,6 +2,7 @@ #include "../../../../../../client/settings/cache_settings.h" #include "../../../../../deck_loader/card_node_function.h" +#include "../../../../../deck_loader/loaded_deck.h" #include "../../../../cards/deck_card_zone_display_widget.h" #include "../../../../visual_deck_editor/visual_deck_display_options_widget.h" #include "../../../../visual_deck_editor/visual_deck_editor_widget.h" @@ -29,7 +30,6 @@ #include #include #include -#include #include #include #include diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/tab_edhrec_main.cpp b/cockatrice/src/interface/widgets/tabs/api/edhrec/tab_edhrec_main.cpp index c8822930e..741a1e85f 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/tab_edhrec_main.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/tab_edhrec_main.cpp @@ -1,6 +1,7 @@ #include "tab_edhrec_main.h" #include "../../../../../client/settings/cache_settings.h" +#include "../../../../deck_loader/loaded_deck.h" #include "../../../cards/card_size_widget.h" #include "../../../quick_settings/settings_button_widget.h" #include "../../../utility/completer_utils.h" @@ -27,7 +28,6 @@ #include #include #include -#include #include #include #include diff --git a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp index 3dfdc956b..9db1952f1 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp @@ -1,7 +1,9 @@ #include "tab_deck_storage.h" #include "../../../client/settings/cache_settings.h" +#include "../../deck_loader/deck_file_format.h" #include "../../deck_loader/deck_loader.h" +#include "../../deck_loader/loaded_deck.h" #include "../../pixel_map_generator.h" #include "../cards/additional_info/deck_color_identity.h" #include "../deck_share/deck_share_utils.h" @@ -47,9 +49,7 @@ #include #include #include -#include #include -#include #include #include #include diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.cpp b/cockatrice/src/interface/widgets/tabs/tab_game.cpp index 9e724727f..11813cdf4 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_game.cpp @@ -2,10 +2,16 @@ #include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcuts_settings.h" -#include "../../../database/interface/settings_card_preference_provider.h" +#include "../../../game/abstract_game.h" +#include "../../../game/game_event_handler.h" +#include "../../../game/game_meta_info.h" +#include "../../../game/game_state.h" +#include "../../../game/player/player_info.h" +#include "../../../game/player/player_manager.h" #include "../../../game_graphics/board/abstract_card_item.h" -#include "../game_graphics/board/arrow_item.h" -#include "../game_graphics/board/card_item.h" +#include "../game/game.h" +#include "../game/player/player_logic.h" +#include "../game/replay.h" #include "../game_graphics/deckview/deck_view_container.h" #include "../game_graphics/deckview/tabbed_deck_view_container.h" #include "../game_graphics/game_scene.h" @@ -65,15 +71,6 @@ #include #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include #include #include @@ -100,8 +97,7 @@ TabGame::TabGame(TabSupervisor *_tabSupervisor, GameReplay *_replay) : Tab(_tabSupervisor), sayLabel(nullptr), sayEdit(nullptr) { // THIS CTOR IS USED ON REPLAY - game = new Replay(this, _replay, tabSupervisor->getIsLocalGame(), &SettingsCache::instance().userInterface(), - new SettingsCardPreferenceProvider(this)); + game = new Replay(this, _replay, tabSupervisor->getIsLocalGame()); createCardInfoDock(true); createPlayerListDock(true); @@ -145,8 +141,7 @@ TabGame::TabGame(TabSupervisor *_tabSupervisor, : Tab(_tabSupervisor), userListProxy(_tabSupervisor->getUserListManager()) { // THIS CTOR IS USED ON GAMES - game = new Game(this, tabSupervisor->getIsLocalGame(), _clients, event, _roomGameTypes, - &SettingsCache::instance().userInterface(), new SettingsCardPreferenceProvider(this)); + game = new Game(this, tabSupervisor->getIsLocalGame(), _clients, event, _roomGameTypes); createCardInfoDock(); createPlayerListDock(); @@ -1281,14 +1276,8 @@ void TabGame::createPlayAreaWidget(bool bReplay) { phasesToolbar = new PhasesToolbar; if (!bReplay) { - connect(phasesToolbar, &PhasesToolbar::setActivePhaseRequested, game->getGameEventHandler(), - &GameEventHandler::handleActivePhaseChanged); - connect(phasesToolbar, &PhasesToolbar::nextTurnRequested, game->getGameEventHandler(), - &GameEventHandler::handleNextTurn); - connect(phasesToolbar, &PhasesToolbar::untapAllRequested, game->getGameEventHandler(), - &GameEventHandler::handleUntapAll); - connect(phasesToolbar, &PhasesToolbar::drawCardRequested, game->getGameEventHandler(), - &GameEventHandler::handleDrawCard); + connect(phasesToolbar, &PhasesToolbar::sendGameCommand, game->getGameEventHandler(), + qOverload(&GameEventHandler::sendGameCommand)); } scene = new GameScene(phasesToolbar, this); connect(game->getPlayerManager(), &PlayerManager::playerConceded, scene, &GameScene::rearrange); diff --git a/cockatrice/src/interface/widgets/tabs/tab_public_decks.cpp b/cockatrice/src/interface/widgets/tabs/tab_public_decks.cpp index ce37ce440..9c28abd0b 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_public_decks.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_public_decks.cpp @@ -2,6 +2,7 @@ #include "../../../client/settings/cache_settings.h" #include "../../deck_loader/deck_loader.h" +#include "../../deck_loader/loaded_deck.h" #include "../cards/card_size_widget.h" #include "../general/layout_containers/flow_widget.h" #include "../visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.h" @@ -27,7 +28,6 @@ #include #include #include -#include #include #include #include diff --git a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp index 3316232a4..c614fe7f6 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp @@ -2,6 +2,10 @@ #include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcuts_settings.h" +#include "../../../game/abstract_game.h" +#include "../../../game/game_event_handler.h" +#include "../../../game/game_meta_info.h" +#include "../../deck_loader/loaded_deck.h" #include "../../intents/contexts/context_connect_to_server.h" #include "../../intents/contexts/context_join_game.h" #include "../../intents/contexts/context_join_room.h" @@ -59,10 +63,6 @@ #include #include #include -#include -#include -#include -#include #include #include #include diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp index bf4e15aa6..be7255dc3 100644 --- a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp +++ b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp @@ -2,6 +2,7 @@ #include "../../../../client/settings/cache_settings.h" #include "../../../../client/settings/shortcuts_settings.h" +#include "../../../deck_loader/loaded_deck.h" #include "../../deck_editor/deck_editor_card_database_dock_widget.h" #include "../../deck_editor/deck_editor_card_info_dock_widget.h" #include "../../deck_editor/deck_editor_deck_dock_widget.h" @@ -43,7 +44,6 @@ #include #include #include -#include #include #include #include @@ -61,8 +61,8 @@ TabDeckEditorVisual::TabDeckEditorVisual(TabSupervisor *_tabSupervisor) : Abstra { setObjectName("TabDeckEditorVisual"); - createMenus(); createCentralFrame(); + createMenus(); installEventFilter(this); @@ -105,6 +105,7 @@ void TabDeckEditorVisual::createCentralFrame() centralFrame->addWidget(tabContainer); setCentralWidget(centralWidget); + setDockOptions(QMainWindow::AnimatedDocks | QMainWindow::AllowNestedDocks | QMainWindow::AllowTabbedDocks); } /** @brief Updates the visual deck, analytics, and sample hand after a deck change. */ diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.cpp b/cockatrice/src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.cpp index ece9881b4..74c149b2d 100644 --- a/cockatrice/src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.cpp +++ b/cockatrice/src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.cpp @@ -1,7 +1,9 @@ #include "tab_deck_storage_visual.h" #include "../../../../client/settings/cache_settings.h" +#include "../../../deck_loader/deck_file_format.h" #include "../../../deck_loader/deck_loader.h" +#include "../../../deck_loader/loaded_deck.h" #include "../../cards/additional_info/deck_color_identity.h" #include "../../deck_share/deck_share_utils.h" #include "../../deck_share/share_bar_widget.h" @@ -15,9 +17,7 @@ #include #include #include -#include #include -#include #include #include #include diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.cpp index cebe92495..4e6e7cc05 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.cpp @@ -1,7 +1,9 @@ #include "deck_preview_deck_tags_display_widget.h" #include "../../../../client/settings/cache_settings.h" +#include "../../../deck_loader/deck_file_format.h" #include "../../../deck_loader/deck_loader.h" +#include "../../../deck_loader/loaded_deck.h" #include "../../general/layout_containers/flow_widget.h" #include "deck_preview_tag_addition_widget.h" #include "deck_preview_tag_dialog.h" @@ -12,10 +14,7 @@ #include #include #include -#include #include -#include -#include #include #include #include @@ -118,13 +117,8 @@ void DeckPreviewDeckTagsDisplayWidget::openTagEditDlg() void DeckPreviewDeckTagsDisplayWidget::execTagDialog(const QStringList &knownTags) { - QPointer self(this); DeckPreviewTagDialog dialog(knownTags, currentTags); if (dialog.exec() == QDialog::Accepted) { - // Guard against the widget being destroyed during dialog.exec()'s nested event loop. - if (!self) { - return; - } QStringList updatedTags = dialog.getActiveTags(); if (updatedTags != currentTags) { setTags(updatedTags); diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp index f2002d4e4..220c55788 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp @@ -3,6 +3,7 @@ #include "../../../../client/settings/cache_settings.h" #include "../../../../interface/widgets/dialogs/dlg_convert_deck_to_cod_format.h" #include "../../../deck_loader/deck_loader.h" +#include "../../../deck_loader/loaded_deck.h" #include "../../cards/additional_info/color_identity_widget.h" #include "../../cards/deck_preview_card_picture_widget.h" #include "../visual_deck_storage_model.h" @@ -44,7 +45,6 @@ #include #include #include -#include #include #include #include diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.cpp index 3611a8c1f..e84631e52 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.cpp @@ -1,6 +1,8 @@ #include "visual_deck_storage_model.h" +#include "../../deck_loader/deck_file_format.h" #include "../../deck_loader/deck_loader.h" +#include "../../deck_loader/loaded_deck.h" #include "../cards/additional_info/deck_color_identity.h" #include "libcockatrice/deck_list/deck_list.h" @@ -19,8 +21,6 @@ #include #include #include -#include -#include #include #include #include diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.h b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.h index f9af6f318..57eaebacf 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.h @@ -13,6 +13,7 @@ #ifndef VISUAL_DECK_STORAGE_MODEL_H #define VISUAL_DECK_STORAGE_MODEL_H +#include "../../deck_loader/loaded_deck.h" #include "libcockatrice/utility/card_ref.h" #include @@ -23,9 +24,6 @@ #include #include #include -#include -#include -#include #include #include diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.cpp index c93e496ec..abe3327af 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.cpp @@ -1,6 +1,7 @@ #include "visual_deck_storage_sort_filter_proxy_model.h" #include "../../../client/settings/cache_settings.h" +#include "../../deck_loader/loaded_deck.h" #include "../../filters/deck_filter_string.h" #include "visual_deck_storage_model.h" @@ -15,7 +16,6 @@ #include #include #include -#include #include VisualDeckStorageSortFilterProxyModel::VisualDeckStorageSortFilterProxyModel(QObject *parent) diff --git a/cockatrice/src/interface/window_main.cpp b/cockatrice/src/interface/window_main.cpp index f8ab5d3f6..a3446e42b 100644 --- a/cockatrice/src/interface/window_main.cpp +++ b/cockatrice/src/interface/window_main.cpp @@ -923,28 +923,11 @@ void MainWindow::actShow() void MainWindow::closeEvent(QCloseEvent *event) { - switch (shutdownState) { - case ShutdownState::Accepted: - // A duplicate close for a shutdown that already went through (Qt delivers the event - // twice on some platforms). Accepting it again keeps the quit that is already under - // way; ignoring it would cancel it. - event->accept(); - return; - case ShutdownState::Confirming: - // A second close request arrived while one of our own confirmations is up. Another - // program asking us to close - an installer replacing our files, for one - looks - // exactly like this, and accepting it would be destructive: close() hides this - // window, hides the dialog with it, and leaves the process running inside the nested - // loop that dialog opened. Nothing can reach that loop afterwards, so the process - // stays alive with no window and every later close request is accepted into the same - // dead end. Refuse this one and leave the question in front of the user; the shutdown - // that is already running finishes as soon as they answer it. - event->ignore(); - return; - case ShutdownState::Idle: - break; + // workaround Qt bug where closeEvent gets called twice + if (bClosingDown) { + return; } - shutdownState = ShutdownState::Confirming; + bClosingDown = true; if (cardUpdateProcess && cardUpdateProcess->state() != QProcess::NotRunning) { const QString runningFor = cardUpdateStatusBar ? cardUpdateStatusBar->statusText() : QString(); @@ -956,20 +939,19 @@ void MainWindow::closeEvent(QCloseEvent *event) .arg(runningFor); if (!QtUtils::confirm(this, tr("Are you sure?"), message, QMessageBox::No)) { event->ignore(); - shutdownState = ShutdownState::Idle; + bClosingDown = false; return; } } if (!tabSupervisor->close()) { event->ignore(); - shutdownState = ShutdownState::Idle; + bClosingDown = false; return; } tip->close(); event->accept(); - shutdownState = ShutdownState::Accepted; SettingsCache::instance().layouts().setMainWindowGeometry(saveGeometry()); tabSupervisor->deleteLater(); } @@ -980,36 +962,12 @@ bool MainWindow::closeForUpdate() // closeEvent() is blocked on a user prompt). close() would hit the re-entrancy guard and return // true without any shutdown having happened, so report faithfully instead: the caller must keep // the process alive until the user resolves whatever is blocking the close. - if (shutdownState != ShutdownState::Idle) { + if (bClosingDown) { return false; } return close(); } -void MainWindow::requestInstallerClose() -{ - // The same shutdown the in-app updater asks for: close the window, flush settings and let - // the tabs answer their own prompts. Never a forced quit - ending a process that has not - // agreed is the installer's separate force-close answer, not this one. - if (closeForUpdate()) { - QTimer::singleShot(0, qApp, [] { QCoreApplication::exit(0); }); - return; - } - // A confirmation is already up, or a shutdown is in progress. Bring it forward: an - // installer prompt behind a hidden confirmation is the deadlock this request exists to - // break, and the user has to be able to answer it. showWindowIfHidden() also clears - // Qt::WindowMinimized, which raise() alone does not: a minimized window can keep the - // confirmation behind the installer. - showWindowIfHidden(); - if (QWidget *modal = QApplication::activeModalWidget()) { - modal->raise(); - modal->activateWindow(); - } - raise(); - activateWindow(); - QApplication::alert(this, 0); -} - void MainWindow::changeEvent(QEvent *event) { if (event->type() == QEvent::LanguageChange) { diff --git a/cockatrice/src/interface/window_main.h b/cockatrice/src/interface/window_main.h index 9e342e4b0..bcf709cb1 100644 --- a/cockatrice/src/interface/window_main.h +++ b/cockatrice/src/interface/window_main.h @@ -194,17 +194,7 @@ private: LagMonitor lagMonitor; ///< watches the main thread for event loop stalls LatencyStatusWidget *latencyStatus = nullptr; ///< status bar widget with live round-trip stats and history graph bool bHasActivated, askedForDbUpdater; - /** - * How far a shutdown that was asked for has got. A close request that arrives while one is - * already asking the user something must not be accepted - see closeEvent(). - */ - enum class ShutdownState - { - Idle, ///< no shutdown has been asked for - Confirming, ///< asking about a card database update, open games or an unsaved deck - Accepted, ///< the close was accepted and the window is hiding - }; - ShutdownState shutdownState = ShutdownState::Idle; + bool bClosingDown = false; ///< guards closeEvent() against re-entrancy bool skipStartupAutoConnect = false; bool startupAutoConnectAttempted = false; bool firstRunWizardActive = false; @@ -255,17 +245,6 @@ public: */ bool closeForUpdate(); - /** - * @brief Closes this instance because the installer asked it to over its local socket. - * - * The request reaches an instance hidden in the notification area or sitting on its own - * confirmation, which is exactly where a WM_CLOSE from the installer does not. The close - * is still the ordinary one: a clean client exits, one with unsaved work raises the same - * confirmation the close button does, and a confirmation already up is brought to the - * front instead of being left behind the installer. - */ - void requestInstallerClose(); - protected: void closeEvent(QCloseEvent *event) override; void changeEvent(QEvent *event) override; diff --git a/cockatrice/src/main.cpp b/cockatrice/src/main.cpp index 829a8ac42..7b8648d69 100644 --- a/cockatrice/src/main.cpp +++ b/cockatrice/src/main.cpp @@ -26,7 +26,6 @@ #include "client/sound_engine.h" #include "client/url_scheme_event_filter.h" #include "database/interface/settings_card_preference_provider.h" -#include "installer_close_server.h" #include "interface/intents/intent.h" #include "interface/intents/intent_open_local_deck.h" #include "interface/logger.h" @@ -432,15 +431,6 @@ int main(int argc, char *argv[]) MainWindow ui; - // The installer asks a running instance to close over this socket, which reaches a - // hidden window and one sitting on its own confirmation. Failure is not fatal: the - // installer falls back to WM_CLOSE for clients that have no socket. - InstallerCloseServer installerClose; - if (installerClose.listen()) { - QObject::connect(&installerClose, &InstallerCloseServer::closeRequested, &ui, - &MainWindow::requestInstallerClose); - } - // A URL launch must own the connection: the intent chain triggered by the // URL connects to the server named in the URL, so the window's own startup // auto-connect must not race against it (two connectToServer calls tear diff --git a/libcockatrice_card/CMakeLists.txt b/libcockatrice_card/CMakeLists.txt index e2d5029ed..b3583522c 100644 --- a/libcockatrice_card/CMakeLists.txt +++ b/libcockatrice_card/CMakeLists.txt @@ -21,9 +21,11 @@ set(HEADERS libcockatrice/card/relation/card_relation.h ) +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) + add_library( libcockatrice_card STATIC - ${HEADERS} + ${MOC_SOURCES} libcockatrice/card/card_info.cpp libcockatrice/card/card_info_comparator.cpp libcockatrice/card/card_localization.cpp diff --git a/libcockatrice_card/libcockatrice/card/card_info.cpp b/libcockatrice_card/libcockatrice/card/card_info.cpp index 90bfd3f44..388c9ab57 100644 --- a/libcockatrice_card/libcockatrice/card/card_info.cpp +++ b/libcockatrice_card/libcockatrice/card/card_info.cpp @@ -6,7 +6,6 @@ #include "relation/card_relation.h" #include "set/card_set.h" -#include #include #include #include @@ -20,18 +19,6 @@ const QHash &CardInfo::getPropertiesHash() const return properties.getProperties(); } -void CardInfo::notifyChanged() -{ - const CardInfoPtr self = sharedFromThis(); - Q_ASSERT_X(self, "CardInfo::notifyChanged", "every CardInfo is owned by a QSharedPointer"); - if (!self) { - qCWarning(CardInfoLog) << "notifyChanged() on an unowned CardInfo" << name << "- observers stay stale"; - return; - } - - emit cardInfoChanged(self); -} - void CardInfo::setProperty(const QString &_name, const QString &_value) { bool changed = properties.insert(_name, _value); @@ -39,7 +26,7 @@ void CardInfo::setProperty(const QString &_name, const QString &_value) return; } - notifyChanged(); + emit cardInfoChanged(smartThis); } CardInfo::CardInfo(const QString &_name, @@ -110,6 +97,7 @@ CardInfoPtr CardInfo::newInstance(const QString &_name, { CardInfoPtr ptr(new CardInfo(_name, _text, _isToken, _properties, _relatedCards, _reverseRelatedCards, _sets, _uiAttributes, std::move(_localizedNames), std::move(_localizedTexts))); + ptr->setSmartPointer(ptr); for (const auto &printings : _sets) { for (const PrintingInfo &printing : printings) { @@ -138,6 +126,7 @@ CardInfoPtr CardInfo::newInstance(const QString &_name, CardInfoPtr ptr(new CardInfo(_name, _text, _isToken, std::move(_propertiesBlob), _relatedCards, _reverseRelatedCards, _sets, _uiAttributes, std::move(_simpleName), std::move(_altNames), std::move(_localizedNames), std::move(_localizedTexts))); + ptr->setSmartPointer(ptr); if (_appendToSets) { for (const auto &printings : _sets) { @@ -180,15 +169,8 @@ bool CardInfo::isLegalInFormat(const QString &format) const void CardInfo::addToSet(const CardSetPtr &_set, const PrintingInfo &_info) { - const CardInfoPtr self = sharedFromThis(); - Q_ASSERT_X(self, "CardInfo::addToSet", "every CardInfo is owned by a QSharedPointer"); - if (!self) { - qCWarning(CardInfoLog) << "addToSet() on an unowned CardInfo" << name << "- the set will not list it"; - return; - } - - if (!_set->contains(self)) { - _set->append(self); + if (!_set->contains(smartThis)) { + _set->append(smartThis); } if (!setsToPrintings[_set->getShortName()].contains(_info)) { setsToPrintings[_set->getShortName()].append(_info); @@ -206,7 +188,7 @@ void CardInfo::combineLegalities(const QHash &props) properties.insert(it.key(), it.value()); } } - notifyChanged(); + emit cardInfoChanged(smartThis); } void CardInfo::refreshCachedSets() diff --git a/libcockatrice_card/libcockatrice/card/card_info.h b/libcockatrice_card/libcockatrice/card/card_info.h index 5452ef98b..eb1250a1c 100644 --- a/libcockatrice_card/libcockatrice/card/card_info.h +++ b/libcockatrice_card/libcockatrice/card/card_info.h @@ -7,7 +7,6 @@ #include #include -#include #include #include #include @@ -51,7 +50,7 @@ Q_DECLARE_METATYPE(CardInfoPtr) * Each CardInfo may belong to multiple sets through its printings, and can * be related to other cards through defined relationships. */ -class CardInfo : public QObject, public QEnableSharedFromThis +class CardInfo : public QObject { Q_OBJECT @@ -75,10 +74,11 @@ private: * @anchor PrivateCardProperties */ ///@{ - QString name; ///< Full name of the card. - QString simpleName; ///< Simplified name for fuzzy matching. - QString text; ///< Text description or rules text of the card. - bool isToken; ///< Whether this card is a token or not. + CardInfoPtr smartThis; ///< Smart pointer to self for safe cross-references. + QString name; ///< Full name of the card. + QString simpleName; ///< Simplified name for fuzzy matching. + QString text; ///< Text description or rules text of the card. + bool isToken; ///< Whether this card is a token or not. QMap localizedNames; ///< Localized card names, keyed by language code. QMap localizedTexts; ///< Localized rules text, keyed by language code. @@ -162,14 +162,13 @@ public: * @param other Another CardInfo to copy. */ CardInfo(const CardInfo &other) - : QObject(other.parent()), QEnableSharedFromThis(), name(other.name), simpleName(other.simpleName), - text(other.text), isToken(other.isToken), localizedNames(other.localizedNames), - localizedTexts(other.localizedTexts), properties(other.properties), relatedCards(other.relatedCards), + : QObject(other.parent()), name(other.name), simpleName(other.simpleName), text(other.text), + isToken(other.isToken), localizedNames(other.localizedNames), localizedTexts(other.localizedTexts), + properties(other.properties), relatedCards(other.relatedCards), reverseRelatedCards(other.reverseRelatedCards), reverseRelatedCardsToMe(other.reverseRelatedCardsToMe), setsToPrintings(other.setsToPrintings), uiAttributes(other.uiAttributes), setsNames(other.setsNames), altNames(other.altNames) { - // The copy deliberately starts unowned: the QSharedPointer that wraps it registers the self reference. } /** @@ -253,13 +252,27 @@ public: /** * @brief Clones the current CardInfo instance. * - * Uses the copy constructor; the copy is managed by its own shared pointer. + * Uses the copy constructor and ensures the smart pointer is properly set. * * @return Shared pointer to the cloned CardInfo. */ [[nodiscard]] CardInfoPtr clone() const { - return CardInfoPtr(new CardInfo(*this)); + auto newCardInfo = CardInfoPtr(new CardInfo(*this)); + newCardInfo->setSmartPointer(newCardInfo); // Set the smart pointer for the new instance + return newCardInfo; + } + + /** + * @brief Sets the internal smart pointer to self. + * + * Used internally to allow safe cross-references among CardInfo and CardSet. + * + * @param _ptr Shared pointer pointing to this instance. + */ + void setSmartPointer(CardInfoPtr _ptr) + { + smartThis = std::move(_ptr); } /** @name Basic Properties Accessors */ //@{ @@ -282,7 +295,7 @@ public: void setText(const QString &_text) { text = _text; - notifyChanged(); + emit cardInfoChanged(smartThis); } /** @@ -345,7 +358,7 @@ public: return; } localizedNames.insert(lang, _localizedName); - notifyChanged(); + emit cardInfoChanged(smartThis); } /** @@ -360,7 +373,7 @@ public: return; } localizedTexts.insert(lang, _localizedText); - notifyChanged(); + emit cardInfoChanged(smartThis); } /** @@ -539,14 +552,6 @@ public: static QString simplifyName(const QString &name); private: - /** - * @brief Emits cardInfoChanged() with a strong reference to this card. - * - * A CardInfo is always owned by a QSharedPointer, so the self reference is always available; a released - * build that somehow finds it missing warns rather than emitting a null card. - */ - void notifyChanged(); - /** * @brief Refreshes the cached, human-readable list of set names. * diff --git a/libcockatrice_card/libcockatrice/card/database/card_database.cpp b/libcockatrice_card/libcockatrice/card/database/card_database.cpp index ed097ef3b..946412606 100644 --- a/libcockatrice_card/libcockatrice/card/database/card_database.cpp +++ b/libcockatrice_card/libcockatrice/card/database/card_database.cpp @@ -53,26 +53,13 @@ CardDatabase::~CardDatabase() clear(); } -void CardDatabase::detachSetMembership() -{ - for (const auto &set : sets) { - if (set) { - set->clear(); - } - } -} - void CardDatabase::clear() { QMutexLocker locker(clearDatabaseMutex); - // The sets hold strong card references, so empty them before dropping the cards: this breaks the - // CardInfo <-> CardSet cycle and spares the per-card detach over the whole database. - detachSetMembership(); - for (const auto &card : cards.values()) { if (card) { - unindexCard(card); + removeCard(card); } } @@ -169,23 +156,6 @@ void CardDatabase::removeCard(const CardInfoPtr &card) return; } - // The set holds a strong reference back to the card, so detach it: leaving it there keeps both - // objects alive after the database drops the card. newInstance() appends through the first - // printing of every set, so detaching through that same printing mirrors the append. - for (const auto &printings : card->getSets()) { - if (printings.isEmpty()) { - continue; - } - if (const CardSetPtr set = printings.first().getSet()) { - set->removeAll(card); - } - } - - unindexCard(card); -} - -void CardDatabase::unindexCard(const CardInfoPtr &card) -{ for (auto *cardRelation : card->getRelatedCards()) { cardRelation->deleteLater(); } @@ -310,10 +280,6 @@ void CardDatabase::swapInDatabaseData(CardDatabaseData data) card->clearDirectRelations(); } - // Break the CardSet <-> CardInfo cycle before replacing the containers, otherwise the previous - // snapshot survives the reload through the sets' strong card lists. - detachSetMembership(); - cards = std::move(data.cards); simpleNameCards = std::move(data.simpleNameCards); sets = std::move(data.sets); diff --git a/libcockatrice_card/libcockatrice/card/database/card_database.h b/libcockatrice_card/libcockatrice/card/database/card_database.h index 5c1cb43c1..9f0d67b67 100644 --- a/libcockatrice_card/libcockatrice/card/database/card_database.h +++ b/libcockatrice_card/libcockatrice/card/database/card_database.h @@ -74,23 +74,6 @@ private: */ void refreshCachedReverseRelatedCards(CardNameMap &cardMap); - /** - * @brief Empties every set, dropping the strong card references they hold. - * - * A card keeps a CardSetPtr through its printings while its set keeps a strong CardInfoPtr, so the - * two form a reference cycle. Dropping a whole snapshot has to break that cycle once, up front, - * instead of relying on the per-card detach in removeCard(). - */ - void detachSetMembership(); - - /** - * @brief Drops a card from the database indexes and notifies observers. - * - * Callers are responsible for detaching the card from its sets first. - * @param card Pointer to the card to unindex. - */ - void unindexCard(const CardInfoPtr &card); - /** @brief Mutexes for thread safety. */ QBasicMutex *clearDatabaseMutex = new QBasicMutex(), *addCardMutex = new QBasicMutex(), *removeCardMutex = new QBasicMutex(); diff --git a/libcockatrice_card/libcockatrice/card/set/card_set.cpp b/libcockatrice_card/libcockatrice/card/set/card_set.cpp index a0f0f4578..e2b41a811 100644 --- a/libcockatrice_card/libcockatrice/card/set/card_set.cpp +++ b/libcockatrice_card/libcockatrice/card/set/card_set.cpp @@ -27,6 +27,7 @@ CardSetPtr CardSet::newInstance(ICardSetPriorityController *_priorityController, const Priority _priority) { CardSetPtr ptr(new CardSet(_priorityController, _shortName, _longName, _setType, _releaseDate, _priority)); + // ptr->setSmartPointer(ptr); return ptr; } diff --git a/libcockatrice_deck_list/CMakeLists.txt b/libcockatrice_deck_list/CMakeLists.txt index ffd4c28a1..e61a5702c 100644 --- a/libcockatrice_deck_list/CMakeLists.txt +++ b/libcockatrice_deck_list/CMakeLists.txt @@ -7,30 +7,28 @@ set(HEADERS libcockatrice/deck_list/tree/abstract_deck_list_node.h libcockatrice/deck_list/tree/deck_list_card_node.h libcockatrice/deck_list/tree/inner_deck_list_node.h - libcockatrice/deck_list/deck_file_format.h libcockatrice/deck_list/deck_list.h libcockatrice/deck_list/deck_list_history_manager.h libcockatrice/deck_list/deck_list_node_tree.h libcockatrice/deck_list/deck_list_memento.h libcockatrice/deck_list/deck_list_plain_text_parser.h - libcockatrice/deck_list/loaded_deck.h libcockatrice/deck_list/playmat_resolver.h libcockatrice/deck_list/sideboard_plan.h ) +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) + add_library( libcockatrice_deck_list STATIC - ${HEADERS} + ${MOC_SOURCES} libcockatrice/deck_list/tree/abstract_deck_list_card_node.cpp libcockatrice/deck_list/tree/abstract_deck_list_node.cpp libcockatrice/deck_list/tree/deck_list_card_node.cpp libcockatrice/deck_list/tree/inner_deck_list_node.cpp - libcockatrice/deck_list/deck_file_format.cpp libcockatrice/deck_list/deck_list.cpp libcockatrice/deck_list/deck_list_history_manager.cpp libcockatrice/deck_list/deck_list_node_tree.cpp libcockatrice/deck_list/deck_list_plain_text_parser.cpp - libcockatrice/deck_list/loaded_deck.cpp libcockatrice/deck_list/playmat_resolver.cpp libcockatrice/deck_list/sideboard_plan.cpp ) diff --git a/libcockatrice_filters/CMakeLists.txt b/libcockatrice_filters/CMakeLists.txt index df2ead83f..2d45b8e5b 100644 --- a/libcockatrice_filters/CMakeLists.txt +++ b/libcockatrice_filters/CMakeLists.txt @@ -6,9 +6,11 @@ set(HEADERS libcockatrice/filters/filter_card.h libcockatrice/filters/filter_str libcockatrice/filters/filter_tree.h ) +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) + add_library( - libcockatrice_filters STATIC ${HEADERS} libcockatrice/filters/filter_card.cpp libcockatrice/filters/filter_string.cpp - libcockatrice/filters/filter_tree.cpp + libcockatrice_filters STATIC ${MOC_SOURCES} libcockatrice/filters/filter_card.cpp + libcockatrice/filters/filter_string.cpp libcockatrice/filters/filter_tree.cpp ) add_dependencies(libcockatrice_filters libcockatrice_card) diff --git a/libcockatrice_game/CMakeLists.txt b/libcockatrice_game/CMakeLists.txt deleted file mode 100644 index e7d362ca2..000000000 --- a/libcockatrice_game/CMakeLists.txt +++ /dev/null @@ -1,81 +0,0 @@ -set(CMAKE_AUTOMOC ON) -set(CMAKE_AUTOUIC ON) -set(CMAKE_AUTORCC ON) - -set(HEADERS - libcockatrice/game/abstract_game.h - libcockatrice/game/board/arrow_data.h - libcockatrice/game/board/card_list.h - libcockatrice/game/board/card_state.h - libcockatrice/game/board/counter_state.h - libcockatrice/game/game.h - libcockatrice/game/game_event_handler.h - libcockatrice/game/game_meta_info.h - libcockatrice/game/game_state.h - libcockatrice/game/phase.h - libcockatrice/game/player/card_menu_action_type.h - libcockatrice/game/player/card_move_request.h - libcockatrice/game/player/event_processing_options.h - libcockatrice/game/player/move_top_cards_until_options.h - libcockatrice/game/player/player_actions.h - libcockatrice/game/player/player_event_handler.h - libcockatrice/game/player/player_info.h - libcockatrice/game/player/player_logic.h - libcockatrice/game/player/player_manager.h - libcockatrice/game/player/token_info.h - libcockatrice/game/replay.h - libcockatrice/game/translation.h - libcockatrice/game/zones/card_zone_algorithms.h - libcockatrice/game/zones/card_zone_logic.h - libcockatrice/game/zones/hand_zone_logic.h - libcockatrice/game/zones/pile_zone_logic.h - libcockatrice/game/zones/stack_zone_logic.h - libcockatrice/game/zones/table_zone_logic.h - libcockatrice/game/zones/view_zone_logic.h -) - -qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) - -add_library( - libcockatrice_game STATIC - ${MOC_SOURCES} - libcockatrice/game/abstract_game.cpp - libcockatrice/game/board/arrow_data.cpp - libcockatrice/game/board/card_list.cpp - libcockatrice/game/board/card_state.cpp - libcockatrice/game/board/counter_state.cpp - libcockatrice/game/game.cpp - libcockatrice/game/game_event_handler.cpp - libcockatrice/game/game_meta_info.cpp - libcockatrice/game/game_state.cpp - libcockatrice/game/phase.cpp - libcockatrice/game/player/player_actions.cpp - libcockatrice/game/player/player_event_handler.cpp - libcockatrice/game/player/player_info.cpp - libcockatrice/game/player/player_logic.cpp - libcockatrice/game/player/player_manager.cpp - libcockatrice/game/replay.cpp - libcockatrice/game/zones/card_zone_logic.cpp - libcockatrice/game/zones/hand_zone_logic.cpp - libcockatrice/game/zones/pile_zone_logic.cpp - libcockatrice/game/zones/stack_zone_logic.cpp - libcockatrice/game/zones/table_zone_logic.cpp - libcockatrice/game/zones/view_zone_logic.cpp -) - -target_include_directories(libcockatrice_game PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) - -target_link_libraries( - libcockatrice_game - PUBLIC libcockatrice_card - libcockatrice_deck_list - libcockatrice_filters - libcockatrice_interfaces - libcockatrice_network - libcockatrice_protocol - libcockatrice_settings - libcockatrice_utility - ${QT_CORE_MODULE} - ${COCKATRICE_QT_VERSION_NAME}::Concurrent - ${COCKATRICE_QT_VERSION_NAME}::Gui -) diff --git a/libcockatrice_game/libcockatrice/game/board/card_state.cpp b/libcockatrice_game/libcockatrice/game/board/card_state.cpp deleted file mode 100644 index 7e8c079a4..000000000 --- a/libcockatrice_game/libcockatrice/game/board/card_state.cpp +++ /dev/null @@ -1,240 +0,0 @@ -#include "card_state.h" - -#include "../zones/card_zone_logic.h" - -#include -#include -#include -#include - -void CardState::resetState(bool keepAnnotations) -{ - attacking = false; - counters.clear(); - pt.clear(); - if (!keepAnnotations) { - annotation.clear(); - } - attachedTo = nullptr; - attachedCards.clear(); - setTapped(false, false); - setDoesntUntap(false); - emit stateReset(); -} - -void CardState::setColor(const QString &_color) -{ - color = _color; - emit colorChanged(); -} - -void CardState::deleteView() -{ - emit viewDeleteRequested(); -} - -void CardState::setCardRef(const CardRef &_cardRef) -{ - if (cardRef == _cardRef) { - return; - } - - const CardRef oldCardRef = cardRef; - if (exactCard) { - disconnect(exactCard.getCardPtr().data(), nullptr, this, nullptr); - } - cardRef = _cardRef; - - emit cardRefChanged(oldCardRef, cardRef); - refreshCardInfo(); -} - -void CardState::refreshCardInfo() -{ - exactCard = CardDatabaseManager::query()->getCard(cardRef); - - if (!exactCard && !cardRef.name.isEmpty()) { - CardInfo::UiAttributes attributes = {.tableRow = -1}; - auto info = CardInfo::newInstance(cardRef.name, "", true, {}, {}, {}, {}, attributes); - exactCard = ExactCard(info); - } - if (exactCard) { - connect(exactCard.getCardPtr().data(), &CardInfo::pixmapUpdated, this, &CardState::cardPixmapUpdated); - } - - emit cardInfoChanged(); -} - -void CardState::setTapped(bool _tapped, bool canAnimate) -{ - if (tapped == _tapped) { - return; - } - - tapped = _tapped; - emit tappedChanged(tapped, canAnimate); - emit stateChanged(); -} - -void CardState::setFaceDown(bool _facedown) -{ - facedown = _facedown; - emit facedownChanged(facedown); - emit stateChanged(); -} - -QVariantList CardState::parsePT(const QString &pt) -{ - QVariantList ptList = QVariantList(); - if (!pt.isEmpty()) { - int sep = pt.indexOf('/'); - if (sep == 0) { - ptList.append(QVariant(pt.mid(1))); // cut off starting '/' and take full string - } else { - int start = 0; - for (;;) { - QString item = pt.mid(start, sep - start); - if (item.isEmpty()) { - ptList.append(QVariant(QString())); - } else if (item[0] == '+') { - ptList.append(QVariant(item.mid(1).toInt())); // add as int - } else if (item[0] == '-') { - ptList.append(QVariant(item.toInt())); // add as int - } else { - ptList.append(QVariant(item)); // add as qstring - } - if (sep == -1) { - break; - } - start = sep + 1; - sep = pt.indexOf('/', start); - } - } - } - return ptList; -} - -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(CardState *_attachedTo) -{ - if (attachedTo == _attachedTo) { - return; - } - - if (attachedTo != nullptr) { - attachedTo->removeAttachedCard(this); - } - - setGridPoint(QPoint(-1, getGridPoint().y())); - attachedTo = _attachedTo; - emit attachedToChanged(_attachedTo); - emit stateChanged(); - - if (attachedTo != nullptr) { - // If the zone is being torn down, it might already be null by the time a card tries to un-attach all its - // attached cards. The graphics item that owns this state handles the teardown in that case. - CardZoneLogic *parentZone = attachedTo->getZone(); - if (parentZone == nullptr) { - return; - } - emit parentZone->cardAdded(this); - attachedTo->addAttachedCard(this); - if (getZone() != parentZone) { - parentZone->reorganizeCards(); - } - } else if (getZone() != nullptr) { - emit getZone() -> cardAdded(this); - } - - if (getZone() != nullptr) { - getZone()->reorganizeCards(); - } -} diff --git a/libcockatrice_game/libcockatrice/game/board/card_state.h b/libcockatrice_game/libcockatrice/game/board/card_state.h deleted file mode 100644 index fa1e81393..000000000 --- a/libcockatrice_game/libcockatrice/game/board/card_state.h +++ /dev/null @@ -1,243 +0,0 @@ -#ifndef COCKATRICE_CARD_STATE_H -#define COCKATRICE_CARD_STATE_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class CardZoneLogic; -class CardState : public QObject -{ - Q_OBJECT - -private: - int id = -1; - CardRef cardRef; - ExactCard exactCard; - bool tapped = false; - bool facedown = false; - QPoint gridPoint; - - bool attacking = false; - QMap counters; - QString annotation; - QString pt; - bool doesntUntap = false; - bool destroyOnZoneChange = false; - QString color; - - CardState *attachedTo = nullptr; - QList attachedCards; - CardZoneLogic *zone = nullptr; - -signals: - void stateChanged(); - - void cardRefChanged(const CardRef &oldCardRef, const CardRef &newCardRef); - void cardInfoChanged(); - void cardPixmapUpdated(); - void tappedChanged(bool newTapped, bool canAnimate); - void facedownChanged(bool newFaceDown); - void stateReset(); - void colorChanged(); - - 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(CardState *newAttachedTo); - void zoneChanged(CardState *changedCard, CardZoneLogic *newZone); - - void viewDeleteRequested(); - -public: - explicit CardState(QObject *parent, CardZoneLogic *_zone = nullptr, const CardRef &_cardRef = {}, int _id = -1) - : QObject(parent), id(_id), cardRef(_cardRef), zone(_zone) - { - } - - void resetState(bool keepAnnotations); - - /** - * @brief Asks the card view owning this state to tear itself down. - * - * The state never holds a card item directly, so it cannot delete one. It asks instead and lets the view run - * its own teardown, which detaches attachments, drops it from the active card slot and unregisters its - * animations before the graphics object goes away. - */ - void deleteView(); - - int getId() const - { - return id; - } - - void setId(int _id) - { - id = _id; - } - - const CardRef &getCardRef() const - { - return cardRef; - } - - void setCardRef(const CardRef &_cardRef); - - QString getName() const - { - return cardRef.name; - } - - QString getProviderId() const - { - return cardRef.providerId; - } - - ExactCard getCard() const - { - return exactCard; - } - - const CardInfo &getCardInfo() const - { - return exactCard.getInfo(); - } - - void refreshCardInfo(); - - bool getTapped() const - { - return tapped; - } - - void setTapped(bool _tapped, bool canAnimate = false); - - bool getFaceDown() const - { - return facedown; - } - - void setFaceDown(bool _facedown); - - const QPoint &getGridPoint() const - { - return gridPoint; - } - - void setGridPoint(const QPoint &_gridPoint) - { - gridPoint = _gridPoint; - } - - 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); - - /** - * @brief Parses a string representing a p/t in order to extract the values from it. - * - * If the string contains '/', the string will be split at the '/' and each side will be parsed separately, - * which means the result list will have two elements. - * - * If '/' is not found, then the entire string is parsed together, which means the result list will - * have a single element. - * - * If either side of the split is empty, there will also only be a single element in the result list. - * - * This function will attempt to parse each substring as an int first, handling plus and minus prefixes. - * If successful, it will put the parsed value into the QVariant as an int. - * If failed, it will just put the substring into the QVariant as a QString. - * - * @param pt The p/t string - * @return A QVariantList that can contain one or two elements, where each QVariant can be either int or QString - */ - static QVariantList parsePT(const QString &pt); - - [[nodiscard]] QString getColor() const - { - return color; - } - - void setColor(const QString &_color); - - bool getDoesntUntap() const - { - return doesntUntap; - } - - void setDoesntUntap(bool _doesntUntap); - - bool getDestroyOnZoneChange() const - { - return destroyOnZoneChange; - } - - void setDestroyOnZoneChange(bool _destroyOnZoneChange); - - CardState *getAttachedTo() const - { - return attachedTo; - } - - void setAttachedTo(CardState *_attachedTo); - - void addAttachedCard(CardState *card) - { - attachedCards.append(card); - } - - void removeAttachedCard(CardState *card) - { - attachedCards.removeOne(card); - } - - const QList &getAttachedCards() const - { - return attachedCards; - } -}; - -#endif // COCKATRICE_CARD_STATE_H diff --git a/libcockatrice_game/libcockatrice/game/player/card_move_request.h b/libcockatrice_game/libcockatrice/game/player/card_move_request.h deleted file mode 100644 index bed46ab60..000000000 --- a/libcockatrice_game/libcockatrice/game/player/card_move_request.h +++ /dev/null @@ -1,22 +0,0 @@ -/** - * @file card_move_request.h - * @ingroup GameLogicActions - */ -//! \todo Document this file. - -#ifndef COCKATRICE_CARD_MOVE_REQUEST_H -#define COCKATRICE_CARD_MOVE_REQUEST_H - -#include - -/** - * @brief Describes one card a view wants moved, in logic terms rather than wire terms. - */ -struct CardMoveRequest -{ - int cardId = -1; - bool faceDown = false; - QString pt = {}; -}; - -#endif // COCKATRICE_CARD_MOVE_REQUEST_H diff --git a/libcockatrice_game/libcockatrice/game/player/move_top_cards_until_options.h b/libcockatrice_game/libcockatrice/game/player/move_top_cards_until_options.h deleted file mode 100644 index da8028b0f..000000000 --- a/libcockatrice_game/libcockatrice/game/player/move_top_cards_until_options.h +++ /dev/null @@ -1,19 +0,0 @@ -/** - * @file move_top_cards_until_options.h - * @ingroup GameLogicPlayers - */ -//! \todo Document this file. - -#ifndef COCKATRICE_MOVE_TOP_CARDS_UNTIL_OPTIONS_H -#define COCKATRICE_MOVE_TOP_CARDS_UNTIL_OPTIONS_H - -#include - -struct MoveTopCardsUntilOptions -{ - QStringList exprs = {}; - int numberOfHits = 1; - bool autoPlay = false; -}; - -#endif // COCKATRICE_MOVE_TOP_CARDS_UNTIL_OPTIONS_H diff --git a/libcockatrice_game/libcockatrice/game/player/token_info.h b/libcockatrice_game/libcockatrice/game/player/token_info.h deleted file mode 100644 index 99869f33e..000000000 --- a/libcockatrice_game/libcockatrice/game/player/token_info.h +++ /dev/null @@ -1,23 +0,0 @@ -/** - * @file token_info.h - * @ingroup GameLogicPlayers - */ -//! \todo Document this file. - -#ifndef COCKATRICE_TOKEN_INFO_H -#define COCKATRICE_TOKEN_INFO_H - -#include - -struct TokenInfo -{ - QString name; - QString color; - QString pt; - QString annotation; - bool destroy = true; - bool faceDown = false; - QString providerId; -}; - -#endif // COCKATRICE_TOKEN_INFO_H diff --git a/libcockatrice_game/libcockatrice/game/replay.h b/libcockatrice_game/libcockatrice/game/replay.h deleted file mode 100644 index f8f82a70d..000000000 --- a/libcockatrice_game/libcockatrice/game/replay.h +++ /dev/null @@ -1,32 +0,0 @@ -/** - * @file replay.h - * @ingroup GameLogic - * @ingroup Replay - */ -//! \todo Document this file. - -#ifndef COCKATRICE_REPLAY_H -#define COCKATRICE_REPLAY_H - -#include "abstract_game.h" - -#include - -class GameReplay; -class QObject; -class ICardPreferenceProvider; -class IInterfaceSettingsProvider; - -class Replay : public AbstractGame -{ - Q_OBJECT - -public: - explicit Replay(QObject *_parent, - const GameReplay *_replay, - bool isLocalGame, - IInterfaceSettingsProvider *interfaceSettings = nullptr, - ICardPreferenceProvider *cardPreferences = nullptr); -}; - -#endif // COCKATRICE_REPLAY_H diff --git a/libcockatrice_interfaces/CMakeLists.txt b/libcockatrice_interfaces/CMakeLists.txt index 16ea686b5..3b17569b1 100644 --- a/libcockatrice_interfaces/CMakeLists.txt +++ b/libcockatrice_interfaces/CMakeLists.txt @@ -24,7 +24,9 @@ set(HEADERS libcockatrice/interfaces/noop_card_set_priority_controller.h ) -add_library(libcockatrice_interfaces STATIC ${HEADERS}) +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) + +add_library(libcockatrice_interfaces STATIC ${MOC_SOURCES}) target_include_directories(libcockatrice_interfaces PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/libcockatrice_models/libcockatrice/models/database/CMakeLists.txt b/libcockatrice_models/libcockatrice/models/database/CMakeLists.txt index 2471adacf..a73e36dbb 100644 --- a/libcockatrice_models/libcockatrice/models/database/CMakeLists.txt +++ b/libcockatrice_models/libcockatrice/models/database/CMakeLists.txt @@ -12,9 +12,11 @@ set(HEADERS token/token_edit_model.h ) +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) + add_library( libcockatrice_models_database STATIC - ${HEADERS} + ${MOC_SOURCES} card_database_model.cpp card_database_display_model.cpp card/card_completer_proxy_model.cpp diff --git a/libcockatrice_models/libcockatrice/models/deck_list/CMakeLists.txt b/libcockatrice_models/libcockatrice/models/deck_list/CMakeLists.txt index b46275437..324bed102 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/CMakeLists.txt +++ b/libcockatrice_models/libcockatrice/models/deck_list/CMakeLists.txt @@ -4,8 +4,10 @@ set(CMAKE_AUTORCC ON) set(HEADERS deck_list_model.h deck_list_sort_filter_proxy_model.h) +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) + add_library( - libcockatrice_models_deck_list STATIC ${HEADERS} deck_list_model.cpp deck_list_model_custom_zones.cpp + libcockatrice_models_deck_list STATIC ${MOC_SOURCES} deck_list_model.cpp deck_list_model_custom_zones.cpp deck_list_sort_filter_proxy_model.cpp ) diff --git a/libcockatrice_network/libcockatrice/network/client/abstract/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/client/abstract/CMakeLists.txt index 880cef5a1..53ad524b4 100644 --- a/libcockatrice_network/libcockatrice/network/client/abstract/CMakeLists.txt +++ b/libcockatrice_network/libcockatrice/network/client/abstract/CMakeLists.txt @@ -6,7 +6,9 @@ set(HEADERS abstract_client.h latency_tracker.h) set(SOURCES abstract_client.cpp latency_tracker.cpp) -add_library(libcockatrice_network_client_abstract STATIC ${HEADERS} ${SOURCES}) +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) + +add_library(libcockatrice_network_client_abstract STATIC ${MOC_SOURCES} ${SOURCES}) add_dependencies(libcockatrice_network_client_abstract libcockatrice_protocol libcockatrice_network_server_remote) diff --git a/libcockatrice_network/libcockatrice/network/client/local/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/client/local/CMakeLists.txt index 4d5676fc0..08c12b7f8 100644 --- a/libcockatrice_network/libcockatrice/network/client/local/CMakeLists.txt +++ b/libcockatrice_network/libcockatrice/network/client/local/CMakeLists.txt @@ -6,7 +6,9 @@ set(HEADERS local_client.h) set(SOURCES local_client.cpp) -add_library(libcockatrice_network_client_local STATIC ${HEADERS} ${SOURCES}) +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) + +add_library(libcockatrice_network_client_local STATIC ${MOC_SOURCES} ${SOURCES}) add_dependencies(libcockatrice_network_client_local libcockatrice_network_client_abstract) diff --git a/libcockatrice_network/libcockatrice/network/client/remote/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/client/remote/CMakeLists.txt index ecc822b7c..c32ca0aa8 100644 --- a/libcockatrice_network/libcockatrice/network/client/remote/CMakeLists.txt +++ b/libcockatrice_network/libcockatrice/network/client/remote/CMakeLists.txt @@ -6,7 +6,9 @@ set(HEADERS remote_client.h) set(SOURCES remote_client.cpp) -add_library(libcockatrice_network_client_remote STATIC ${HEADERS} ${SOURCES}) +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) + +add_library(libcockatrice_network_client_remote STATIC ${MOC_SOURCES} ${SOURCES}) add_dependencies(libcockatrice_network_client_remote libcockatrice_network_client_abstract libcockatrice_protocol) diff --git a/libcockatrice_network/libcockatrice/network/client/remote/remote_client.cpp b/libcockatrice_network/libcockatrice/network/client/remote/remote_client.cpp index 7eab58668..e02f5d3d8 100644 --- a/libcockatrice_network/libcockatrice/network/client/remote/remote_client.cpp +++ b/libcockatrice_network/libcockatrice/network/client/remote/remote_client.cpp @@ -40,47 +40,6 @@ class QObject; static const unsigned int protocolVersion = 14; -// Explanations for logins that do not complete but carry no missing-feature list: the -// ones RemoteClient stops itself, plus a strict server asking for a password. They all -// travel with RespClientUpdateRequired and are shown by the same dialog, so their wording -// is kept together here and adjusted as a set. The context has to stay "RemoteClient" for -// the tr() calls at the use sites to find the translations. -namespace -{ -// The server warns that this account still uses the legacy password hash while this client -// holds a scrypt verifier. Sending the verifier would downgrade the proof, so the client -// stops; the login only succeeds after the saved verifier is gone and the user types the -// password, which also upgrades the account. -constexpr const char *const LOGIN_ABORT_AUTH_DOWNGRADE = QT_TRANSLATE_NOOP( - "RemoteClient", - "The server still uses an old password format for this account, so the saved login on this computer cannot be " - "used. Untick 'Save password' in the Connect window and connect; when it opens again, type your password."); -// The account is still on the legacy hash and only the plaintext password can produce it. -constexpr const char *const LOGIN_ABORT_PASSWORD_REQUIRED = - QT_TRANSLATE_NOOP("RemoteClient", - "This account still uses an old password format. Type your password in the Connect window and " - "log in; the account is upgraded automatically."); -// Rejected by PasswordHasher::costParamsAreSane, either for being weak or for being too -// expensive for this client. Nothing on the client side can fix it. -constexpr const char *const LOGIN_ABORT_UNSAFE_SCRYPT_PARAMS = - QT_TRANSLATE_NOOP("RemoteClient", - "The server asked for password settings that Cockatrice will not accept because they are too " - "weak or too demanding. The login was stopped; only the server's staff can change this."); -// Scrypt derivation failed locally: a device resource problem, or server parameters this -// client cannot satisfy. -constexpr const char *const LOGIN_ABORT_VERIFIER_NOT_DERIVED = - QT_TRANSLATE_NOOP("RemoteClient", - "Cockatrice could not prepare your password for this login. Try again; if it keeps happening, " - "the server's password settings may be too demanding for this version of Cockatrice."); -constexpr const char *const LOGIN_ABORT_STORED_VERIFIER_UNUSABLE = - QT_TRANSLATE_NOOP("RemoteClient", - "The saved login for this account cannot be used. Type your password in the Connect window; if " - "'Save password' is ticked, a new saved login is created after you log in."); -constexpr const char *const LOGIN_ABORT_PASSWORD_MISSING = - QT_TRANSLATE_NOOP("RemoteClient", - "This server requires your password to log in. Type it in the Connect window and try again."); -} // namespace - RemoteClient::RemoteClient(QObject *parent, INetworkSettingsProvider *_networkSettingsProvider) : AbstractClient(parent), networkSettingsProvider(_networkSettingsProvider), timeRunning(0), lastDataReceived(0), messageInProgress(false), handshakeStarted(false), usingWebSocket(false), messageLength(0), hashedPassword(), @@ -376,7 +335,9 @@ void RemoteClient::passwordSaltResponse(const Response &response) // 1000-round legacy hash is a downgrade of the KDF — refuse it rather // than hand an attacker a weakly-keyed HMAC they can attack offline. if (resp.needs_migration() && PasswordHasher::parsePasswordVerifier(storedVerifier).isValid) { - emit loginError(Response::RespClientUpdateRequired, tr(LOGIN_ABORT_AUTH_DOWNGRADE), 0, {}); + emit loginError(Response::RespClientUpdateRequired, + QStringLiteral("Server asked to downgrade authentication for a migrated account."), 0, + {}); return; } QByteArray key; @@ -384,7 +345,8 @@ void RemoteClient::passwordSaltResponse(const Response &response) // The account still uses the legacy format; the legacy full hash // is only derivable from the plaintext password. if (password.isEmpty()) { - emit loginError(Response::RespClientUpdateRequired, tr(LOGIN_ABORT_PASSWORD_REQUIRED), 0, {}); + emit loginError(Response::RespClientUpdateRequired, + QStringLiteral("This account must be logged in with its password once."), 0, {}); return; } key = PasswordHasher::computeHash(password, passwordSalt).toUtf8(); @@ -395,7 +357,8 @@ void RemoteClient::passwordSaltResponse(const Response &response) const int r = resp.has_r() ? resp.r() : SCRYPT_R; const int p = resp.has_p() ? resp.p() : SCRYPT_P; if (!PasswordHasher::costParamsAreSane(n, r, p)) { - emit loginError(Response::RespClientUpdateRequired, tr(LOGIN_ABORT_UNSAFE_SCRYPT_PARAMS), 0, {}); + emit loginError(Response::RespClientUpdateRequired, + QStringLiteral("The server requested unreasonable scrypt cost parameters."), 0, {}); return; } // password_salt may carry a legacy backup salt for pre-challenge clients; @@ -404,7 +367,8 @@ void RemoteClient::passwordSaltResponse(const Response &response) resp.has_scrypt_salt() ? QString::fromStdString(resp.scrypt_salt()) : passwordSalt; key = PasswordHasher::deriveKey(password, QByteArray::fromBase64(scryptSalt.toUtf8()), n, r, p); if (key.isEmpty()) { - emit loginError(Response::RespClientUpdateRequired, tr(LOGIN_ABORT_VERIFIER_NOT_DERIVED), 0, {}); + emit loginError(Response::RespClientUpdateRequired, QStringLiteral("Unable to derive verifier."), 0, + {}); return; } derivedVerifier = @@ -412,8 +376,8 @@ void RemoteClient::passwordSaltResponse(const Response &response) } else if (!storedVerifier.isEmpty()) { const PasswordVerifier verifier = PasswordHasher::parsePasswordVerifier(storedVerifier); if (!verifier.isValid) { - emit loginError(Response::RespClientUpdateRequired, tr(LOGIN_ABORT_STORED_VERIFIER_UNUSABLE), 0, - {}); + emit loginError(Response::RespClientUpdateRequired, QStringLiteral("Stored verifier is invalid."), + 0, {}); return; } key = verifier.verifier; @@ -482,19 +446,8 @@ void RemoteClient::loginResponse(const Response &response) missingFeatures << QString::fromStdString(resp.missing_features(i)); } } - - QString reasonStr = QString::fromStdString(resp.denied_reason_str()); - // A strict server reports a login that carried no credential as a missing feature, - // because challenge-response needs one. No client update can supply a password, so - // report it like the other local aborts instead of advising an update. - if (response.response_code() == Response::RespClientUpdateRequired && missingFeatures.size() == 1 && - missingFeatures.first() == "challenge_response_auth" && password.isEmpty() && storedVerifier.isEmpty()) { - missingFeatures.clear(); - reasonStr = tr(LOGIN_ABORT_PASSWORD_MISSING); - } - - emit loginError(response.response_code(), reasonStr, static_cast(resp.denied_end_time()), - missingFeatures); + emit loginError(response.response_code(), QString::fromStdString(resp.denied_reason_str()), + static_cast(resp.denied_end_time()), missingFeatures); setStatus(StatusDisconnecting); } } @@ -884,7 +837,7 @@ void RemoteClient::requestForgotPasswordResponse(const Response &response) emit sigPromptForForgotPasswordReset(); } } else { - emit sigForgotPasswordError(response.response_code()); + emit sigForgotPasswordError(); } doDisconnectFromServer(); @@ -920,7 +873,7 @@ void RemoteClient::submitForgotPasswordResetResponse(const Response &response) derivedVerifier.clear(); } } else { - emit sigForgotPasswordError(response.response_code()); + emit sigForgotPasswordError(); } doDisconnectFromServer(); @@ -955,7 +908,7 @@ void RemoteClient::submitForgotPasswordChallengeResponse(const Response &respons if (response.response_code() == Response::RespOk) { emit sigPromptForForgotPasswordReset(); } else { - emit sigForgotPasswordError(response.response_code()); + emit sigForgotPasswordError(); } doDisconnectFromServer(); diff --git a/libcockatrice_network/libcockatrice/network/client/remote/remote_client.h b/libcockatrice_network/libcockatrice/network/client/remote/remote_client.h index d0c0a18ce..dd91c4c6d 100644 --- a/libcockatrice_network/libcockatrice/network/client/remote/remote_client.h +++ b/libcockatrice_network/libcockatrice/network/client/remote/remote_client.h @@ -54,7 +54,7 @@ signals: void notifyUserAboutUpdate(); void sigRequestForgotPasswordToServer(const QString &hostname, unsigned int port, const QString &_userName); void sigForgotPasswordSuccess(); - void sigForgotPasswordError(int responseCode); + void sigForgotPasswordError(); void sigPromptForForgotPasswordReset(); void sigSubmitForgotPasswordResetToServer(const QString &hostname, unsigned int port, diff --git a/libcockatrice_network/libcockatrice/network/server/local/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/server/local/CMakeLists.txt index c5f8ee432..020753d7f 100644 --- a/libcockatrice_network/libcockatrice/network/server/local/CMakeLists.txt +++ b/libcockatrice_network/libcockatrice/network/server/local/CMakeLists.txt @@ -6,7 +6,9 @@ set(HEADERS local_server.h local_server_interface.h) set(SOURCES local_server.cpp local_server_interface.cpp) -add_library(libcockatrice_network_server_local STATIC ${HEADERS} ${SOURCES}) +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) + +add_library(libcockatrice_network_server_local STATIC ${MOC_SOURCES} ${SOURCES}) add_dependencies(libcockatrice_network_server_local libcockatrice_protocol) diff --git a/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt index 26c2be69a..2127487d2 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt +++ b/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt @@ -29,9 +29,11 @@ set(HEADERS serverinfo_user_container.h ) +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) + add_library( libcockatrice_network_server_remote STATIC - ${HEADERS} + ${MOC_SOURCES} game/dump_zone_range.cpp game/server_abstract_participant.cpp game/server_abstract_player.cpp diff --git a/libcockatrice_rng/CMakeLists.txt b/libcockatrice_rng/CMakeLists.txt index c5b19ae85..6128e3f6e 100644 --- a/libcockatrice_rng/CMakeLists.txt +++ b/libcockatrice_rng/CMakeLists.txt @@ -4,8 +4,10 @@ set(CMAKE_AUTORCC ON) set(HEADERS libcockatrice/rng/rng_abstract.h libcockatrice/rng/rng_sfmt.h libcockatrice/rng/sfmt/SFMT.h) +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) + add_library( - libcockatrice_rng STATIC ${HEADERS} libcockatrice/rng/rng_abstract.cpp libcockatrice/rng/rng_sfmt.cpp + libcockatrice_rng STATIC ${MOC_SOURCES} libcockatrice/rng/rng_abstract.cpp libcockatrice/rng/rng_sfmt.cpp libcockatrice/rng/sfmt/SFMT.c ) diff --git a/libcockatrice_rng/libcockatrice/rng/sfmt/SFMT.h b/libcockatrice_rng/libcockatrice/rng/sfmt/SFMT.h index df114cf9b..34d9e746f 100644 --- a/libcockatrice_rng/libcockatrice/rng/sfmt/SFMT.h +++ b/libcockatrice_rng/libcockatrice/rng/sfmt/SFMT.h @@ -113,14 +113,6 @@ union W128_T { /** 128-bit data type */ typedef union W128_T w128_t; -#if defined(__AVX2__) -/* The AVX2 state below names __m256i (and __m512i under AVX-512VL). The SIMD header - above is only pulled in for HAVE_SSE2, so a build that has __AVX2__ from the compiler - (e.g. -march=native) but not HAVE_SSE2 from the build system would otherwise not have - those types declared. */ - #include -#endif - /** * SFMT internal state */ diff --git a/libcockatrice_settings/CMakeLists.txt b/libcockatrice_settings/CMakeLists.txt index d23fb3350..5a2fa42a9 100644 --- a/libcockatrice_settings/CMakeLists.txt +++ b/libcockatrice_settings/CMakeLists.txt @@ -31,9 +31,11 @@ set(HEADERS libcockatrice/settings/visual_deck_storage_settings.h ) +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) + add_library( libcockatrice_settings STATIC - ${HEADERS} + ${MOC_SOURCES} libcockatrice/settings/appearance_settings.cpp libcockatrice/settings/cache_storage_settings.cpp libcockatrice/settings/card_database_settings.cpp diff --git a/libcockatrice_utility/CMakeLists.txt b/libcockatrice_utility/CMakeLists.txt index 55f1daf24..748f6b90f 100644 --- a/libcockatrice_utility/CMakeLists.txt +++ b/libcockatrice_utility/CMakeLists.txt @@ -23,7 +23,6 @@ set(UTILITY_HEADERS libcockatrice/utility/cryptoutil.h libcockatrice/utility/debouncer.h libcockatrice/utility/expression.h - libcockatrice/utility/installer_close_protocol.h libcockatrice/utility/levenshtein.h libcockatrice/utility/local_server_name.h libcockatrice/utility/macros.h diff --git a/libcockatrice_utility/libcockatrice/utility/installer_close_protocol.h b/libcockatrice_utility/libcockatrice/utility/installer_close_protocol.h deleted file mode 100644 index 1db467b2f..000000000 --- a/libcockatrice_utility/libcockatrice/utility/installer_close_protocol.h +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef COCKATRICE_INSTALLER_CLOSE_PROTOCOL_H -#define COCKATRICE_INSTALLER_CLOSE_PROTOCOL_H - -/** - * @brief The one line the installer's holder probe writes to a running instance. - * - * Both ends include this header: the probe writes the line - * (tools/installer-holder-probe/installer_close_request.cpp) and the client's - * InstallerCloseServer reads it. Keeping the bytes in one place means the two ends cannot - * drift apart, and the test pins the exact line so a change to it is a deliberate protocol - * change rather than a typo in one copy. - * - * Plain C++ on purpose: the probe links no Qt and no part of this project, so this header - * must stay free of both. - */ -inline constexpr char INSTALLER_CLOSE_COMMAND[] = "COCKATRICE_INSTALLER_CLOSE\n"; - -#endif // COCKATRICE_INSTALLER_CLOSE_PROTOCOL_H diff --git a/libcockatrice_utility/libcockatrice/utility/translation_loader.cpp b/libcockatrice_utility/libcockatrice/utility/translation_loader.cpp index e5761fcaf..41c6107b3 100644 --- a/libcockatrice_utility/libcockatrice/utility/translation_loader.cpp +++ b/libcockatrice_utility/libcockatrice/utility/translation_loader.cpp @@ -5,10 +5,8 @@ #include #include #include -#include #include #include -#include #include #include #include @@ -102,60 +100,6 @@ QList TranslationLoader::availableLanguages(const QString &_prefix, con return sortedLanguages; } -QString TranslationLoader::languageDisplayName(const QString &_lang) -{ - if (_lang.isEmpty()) { - return QString(); - } - - // The native name first, so a speaker finds their own language, then the English one, so everyone - // else does. A language shipped with a .qm file belongs in here: the names used to come from the - // translation files, where a missing translation left the selector blank. - static const QHash displayNames = { - {QStringLiteral("ca"), QStringLiteral("Català (Catalan)")}, - {QStringLiteral("cs"), QStringLiteral("Česky (Czech)")}, - {QStringLiteral("de"), QStringLiteral("Deutsch (German)")}, - {QStringLiteral("el"), QStringLiteral("Ελληνικά (Greek)")}, - {QStringLiteral("en@pirate"), QStringLiteral("English, arr! (Pirate English)")}, - {QStringLiteral("en_US"), QStringLiteral("English (United States)")}, - {QStringLiteral("es"), QStringLiteral("Español (Spanish)")}, - {QStringLiteral("et"), QStringLiteral("Eesti Keel (Estonian)")}, - {QStringLiteral("fi"), QStringLiteral("Suomi (Finnish)")}, - {QStringLiteral("fr"), QStringLiteral("Français (French)")}, - {QStringLiteral("hu"), QStringLiteral("Magyar (Hungarian)")}, - {QStringLiteral("it"), QStringLiteral("Italiano (Italian)")}, - {QStringLiteral("ja"), QStringLiteral("日本語 (Japanese)")}, - {QStringLiteral("ko"), QStringLiteral("한국어 (Korean)")}, - {QStringLiteral("nb"), QStringLiteral("Norsk Bokmål (Norwegian Bokmål)")}, - {QStringLiteral("nl"), QStringLiteral("Nederlands (Dutch)")}, - {QStringLiteral("pl"), QStringLiteral("Polski (Polish)")}, - {QStringLiteral("pt"), QStringLiteral("Português (Portuguese)")}, - {QStringLiteral("pt_BR"), QStringLiteral("Português do Brasil (Brazilian Portuguese)")}, - {QStringLiteral("ru"), QStringLiteral("Русский (Russian)")}, - {QStringLiteral("sr"), QStringLiteral("Srpski (Serbian)")}, - {QStringLiteral("sv"), QStringLiteral("Svenska (Swedish)")}, - {QStringLiteral("tr"), QStringLiteral("Türkçe (Turkish)")}, - {QStringLiteral("yue"), QStringLiteral("粵語 (Cantonese)")}, - {QStringLiteral("zh-Hans"), QStringLiteral("简体中文 (Chinese Simplified)")}, - {QStringLiteral("zh-Hant"), QStringLiteral("繁體中文 (Chinese Traditional)")}, - }; - - const QString displayName = displayNames.value(_lang); - if (!displayName.isEmpty()) { - return displayName; - } - - // A language added after this table still gets a label instead of the blank one this function exists - // to prevent. Qt knows no name for an unknown code and answers with an empty string. - const QString nativeName = QLocale(_lang).nativeLanguageName(); - return nativeName.isEmpty() ? _lang : nativeName; -} - -QString TranslationLoader::defaultLanguageCode() -{ - return QStringLiteral("en_US"); -} - TranslationLoadResult TranslationLoader::loadFrom(QTranslator &_translator, const QString &_nameHint, const QList &_directories) { diff --git a/libcockatrice_utility/libcockatrice/utility/translation_loader.h b/libcockatrice_utility/libcockatrice/utility/translation_loader.h index 53f419cf2..645489696 100644 --- a/libcockatrice_utility/libcockatrice/utility/translation_loader.h +++ b/libcockatrice_utility/libcockatrice/utility/translation_loader.h @@ -74,26 +74,6 @@ TranslationLoadResult loadQt(QTranslator &_translator, const QString &_lang); */ QList availableLanguages(const QString &_prefix, const QList &_directories); -/** - * @brief A human-readable name for a language, e.g. "Deutsch (German)". - * - * The names are data, not translations: a language selector has to be readable before any translation - * is loaded, and its labels must not depend on the translation of the language they name. - * - * @param _lang The language code, e.g. "de" or "pt_BR". - * @return The curated name; for a code it does not know, the native language name; the code itself when - * Qt has no name for it either; an empty string when _lang is empty. - */ -QString languageDisplayName(const QString &_lang); - -/** - * @brief The language a client runs with when none is configured, for preselecting a selector entry. - * - * English is the untranslated default and ships no translation file of its own, so a selector has nothing - * of its own to point at and uses the entry that carries an English translation instead. - */ -QString defaultLanguageCode(); - /** * @brief Loads the <_prefix>_<_lang>.qm translation file from _directories into _translator. * diff --git a/oracle/CMakeLists.txt b/oracle/CMakeLists.txt index 587463572..0df345812 100644 --- a/oracle/CMakeLists.txt +++ b/oracle/CMakeLists.txt @@ -63,7 +63,7 @@ set(oracle_RESOURCES oracle.qrc) # ------------------------ # Qt resources # ------------------------ -qt_add_resources(oracle_RESOURCES_RCC ${oracle_RESOURCES}) +qt6_add_resources(oracle_RESOURCES_RCC ${oracle_RESOURCES}) # ------------------------ # Include directories @@ -108,7 +108,7 @@ set(ORACLE_UNIX_QM_INSTALL_DIR "share/oracle/translations") set(ORACLE_WIN32_QM_INSTALL_DIR "translations") # Qt6 Translations are linked after the executable is created in manual mode -qt_add_executable( +qt6_add_executable( oracle WIN32 MACOSX_BUNDLE @@ -283,7 +283,7 @@ endif() if(Qt6LinguistTools_FOUND) #Qt6 Translations happen after the executable is built up if(UPDATE_TRANSLATIONS) - qt_add_translations( + qt6_add_translations( oracle TS_FILES ${oracle_TS} @@ -293,7 +293,7 @@ if(Qt6LinguistTools_FOUND) oracle_QM ) else() - qt_add_translations(oracle TS_FILES ${oracle_TS} QM_FILES_OUTPUT_VARIABLE oracle_QM) + qt6_add_translations(oracle TS_FILES ${oracle_TS} QM_FILES_OUTPUT_VARIABLE oracle_QM) endif() if(UNIX) @@ -319,4 +319,4 @@ if(Qt6LinguistTools_FOUND) endforeach() endif() -qt_finalize_target(oracle) +qt6_finalize_target(oracle) diff --git a/oracle/src/oraclewizard.cpp b/oracle/src/oraclewizard.cpp index c33d821a5..47afecc06 100644 --- a/oracle/src/oraclewizard.cpp +++ b/oracle/src/oraclewizard.cpp @@ -32,6 +32,9 @@ class QWidget; OracleWizard::OracleWizard(QWidget *parent) : QWizard(parent) { + // define a dummy context that will be used where needed + QString dummy = QT_TRANSLATE_NOOP("i18n", "English"); + #ifdef Q_OS_WIN setWizardStyle(QWizard::ModernStyle); #endif diff --git a/oracle/src/pages.cpp b/oracle/src/pages.cpp index 4c664a38c..ed8e04d1a 100644 --- a/oracle/src/pages.cpp +++ b/oracle/src/pages.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -38,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -201,23 +203,16 @@ IntroPage::IntroPage(QWidget *parent) : OracleWizardPage(parent) QStringList languageCodes = findQmFiles(); for (const QString &code : languageCodes) { - languageBox->addItem(TranslationLoader::languageDisplayName(code), code); + QString langName = languageName(code); + languageBox->addItem(langName, code); } - // Select the language the wizard runs with: its own entry, the base language for a variant without a - // file of its own (de_CH -> de), and the default language for an unset setting, which names no file. - const QString configuredLanguage = SettingsCache::instance().personal().getLang(); - int index = languageBox->findData(configuredLanguage); - if (index < 0) { - index = languageBox->findData(configuredLanguage.section('_', 0, 0)); - } - if (index < 0) { - index = languageBox->findData(TranslationLoader::defaultLanguageCode()); - } - if (index >= 0) { - languageBox->setCurrentIndex(index); + QString setLanguage = QCoreApplication::translate("i18n", DEFAULT_LANG_NAME); + int index = languageBox->findText(setLanguage, Qt::MatchExactly); + if (index == -1) { + qWarning() << "could not find language" << setLanguage; } else { - qWarning() << "could not find language" << configuredLanguage; + languageBox->setCurrentIndex(index); } connect(languageBox, qOverload(&QComboBox::currentIndexChanged), this, &IntroPage::languageBoxChanged); @@ -244,6 +239,18 @@ QStringList IntroPage::findQmFiles() TranslationLoader::applicationTranslationPaths(translationPath)); } +QString IntroPage::languageName(const QString &lang) +{ + QTranslator qTranslator; + + // Only the language name is needed here, so the file is loaded without logging a line per + // language: findQmFiles() asked for languages whose files exist in the first place. + TranslationLoader::loadFrom(qTranslator, translationPrefix + "_" + lang, + TranslationLoader::applicationTranslationPaths(translationPath)); + + return qTranslator.translate("i18n", DEFAULT_LANG_NAME); +} + void IntroPage::languageBoxChanged(int index) { SettingsCache::instance().personal().setLang(languageBox->itemData(index).toString()); diff --git a/oracle/src/pages.h b/oracle/src/pages.h index 0530010ba..221f032d3 100644 --- a/oracle/src/pages.h +++ b/oracle/src/pages.h @@ -31,6 +31,7 @@ public: private: QStringList findQmFiles(); + QString languageName(const QString &lang); private: QLabel *label, *languageLabel, *versionLabel; diff --git a/servatrice/CMakeLists.txt b/servatrice/CMakeLists.txt index 9d8dd46d2..255469934 100644 --- a/servatrice/CMakeLists.txt +++ b/servatrice/CMakeLists.txt @@ -56,7 +56,7 @@ if(APPLE) set(servatrice_SOURCES ${servatrice_SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/resources/appicon.icns) endif(APPLE) -qt_add_resources(servatrice_RESOURCES_RCC ${servatrice_RESOURCES}) +qt6_add_resources(servatrice_RESOURCES_RCC ${servatrice_RESOURCES}) set(QT_DONT_USE_QTGUI TRUE) diff --git a/servatrice/src/isl_interface.cpp b/servatrice/src/isl_interface.cpp index 8635808ac..7121bfa48 100644 --- a/servatrice/src/isl_interface.cpp +++ b/servatrice/src/isl_interface.cpp @@ -59,7 +59,8 @@ void IslInterface::sharedCtor(const QSslCertificate &cert, const QSslKey &privat socket->setPrivateKey(privateKey); connect(socket, SIGNAL(readyRead()), this, SLOT(readClient()), Qt::QueuedConnection); - connect(socket, &QAbstractSocket::errorOccurred, this, &IslInterface::catchSocketError); + connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), this, + SLOT(catchSocketError(QAbstractSocket::SocketError))); connect(this, SIGNAL(outputBufferChanged()), this, SLOT(flushOutputBuffer()), Qt::QueuedConnection); } @@ -67,7 +68,7 @@ IslInterface::IslInterface(int _socketDescriptor, const QSslCertificate &cert, const QSslKey &privateKey, Servatrice *_server) - : QObject(), serverId(-1), socketDescriptor(_socketDescriptor), server(_server), messageInProgress(false) + : QObject(), socketDescriptor(_socketDescriptor), server(_server), messageInProgress(false) { sharedCtor(cert, privateKey); } diff --git a/servatrice/src/main.cpp b/servatrice/src/main.cpp index 4d513d026..eb019a2b7 100644 --- a/servatrice/src/main.cpp +++ b/servatrice/src/main.cpp @@ -41,7 +41,6 @@ #include #include #include -#include #include #include #include @@ -163,15 +162,14 @@ int main(int argc, char *argv[]) configPath = SettingsCache::guessConfigurationPath(); } else if (!QFile::exists(configPath)) { qCritical() << "Could not find configuration file at" << configPath; - return EXIT_FAILURE; + return 1; } qWarning() << "Using configuration file: " << configPath; settingsCache = new SettingsCache(configPath); loggerThread = new QThread; loggerThread->setObjectName("logger"); - logger = new ServerLogger(logToConsole, settingsCache->value("server/writelog", 1).toBool(), - settingsCache->value("server/logfilters").toString()); + logger = new ServerLogger(logToConsole); logger->moveToThread(loggerThread); loggerThread->start(); @@ -216,16 +214,8 @@ int main(int argc, char *argv[]) std::cerr << "Server quit." << std::endl; std::cerr << "-------------------------" << std::endl; - } else { - // initServer() logged the reason; a failed startup must not look like a clean exit. - std::cerr << "Server initialization failed, exiting." << std::endl; - retval = EXIT_FAILURE; } - // The handler reads the logger and settingsCache globals torn down below; leaving it - // installed lets Qt's own shutdown warnings dereference freed memory. - qInstallMessageHandler(nullptr); - delete smtpClient; delete rng; delete signalhandler; diff --git a/servatrice/src/servatrice.cpp b/servatrice/src/servatrice.cpp index 49d8e1e4c..b6eaf11f4 100644 --- a/servatrice/src/servatrice.cpp +++ b/servatrice/src/servatrice.cpp @@ -308,8 +308,7 @@ bool Servatrice::initServer() authenticationMethod = AuthenticationPassword; } else { if (getRegOnlyServerEnabled()) { - qCCritical(ServatriceLog) << "Registration only server enabled but no authentication method defined; " - "refusing to start."; + qDebug() << "Registration only server enabled but no authentication method defined: Error."; return false; } qDebug() << "Authenticating method: none"; @@ -380,7 +379,7 @@ bool Servatrice::initServer() bool dbOpened = servatriceDatabaseInterface->initDatabase( "QMYSQL", getDBHostNameString(), getDBDatabaseNameString(), getDBUserNameString(), getDBPasswordString()); if (!dbOpened) { - qCCritical(ServatriceLog) << "Failed to open the database; refusing to start."; + qDebug() << "Failed to open database"; return false; } updateServerList(); @@ -601,11 +600,11 @@ bool Servatrice::initServer() if (islServer->listen(QHostAddress::Any, static_cast(getISLNetworkPort()))) { qDebug() << "ISL server listening."; } else { - throw islServer->errorString(); + throw QString("islServer->listen()"); } } } catch (QString &error) { - qCCritical(ServatriceLog) << "Failed to start the ISL server:" << error; + qDebug() << "ERROR --" << error; return false; } @@ -642,7 +641,7 @@ bool Servatrice::initServer() if (gameServer->listen(tcpHost, static_cast(getServerTCPPort()))) { qDebug() << "Server listening."; } else { - qCCritical(ServatriceLog) << "Failed to listen for TCP clients:" << gameServer->errorString(); + qDebug() << "gameServer->listen(): Error:" << gameServer->errorString(); return false; } } @@ -658,8 +657,7 @@ bool Servatrice::initServer() if (websocketGameServer->listen(webSocketHost, static_cast(getServerWebSocketPort()))) { qDebug() << "Websocket server listening."; } else { - qCCritical(ServatriceLog) << "Failed to listen for websocket clients:" - << websocketGameServer->errorString(); + qDebug() << "websocketGameServer->listen(): Error:" << websocketGameServer->errorString(); return false; } } diff --git a/servatrice/src/server_logger.cpp b/servatrice/src/server_logger.cpp index 09453b7ee..1c83299b8 100644 --- a/servatrice/src/server_logger.cpp +++ b/servatrice/src/server_logger.cpp @@ -1,45 +1,42 @@ #include "server_logger.h" +#include "settingscache.h" + #include #include #include #include #include #include -#include #include #include +#include #include #include #include #include #include -ServerLogger::ServerLogger(bool _logToConsole, bool _writeLog, const QString &_logFilters, QObject *parent) +ServerLogger::ServerLogger(bool _logToConsole, QObject *parent) : QObject(parent), logToConsole(_logToConsole), flushRunning(false) { - reloadConfig(_writeLog, _logFilters); - connect(this, SIGNAL(sigFlushBuffer()), this, SLOT(flushBuffer()), Qt::QueuedConnection); } ServerLogger::~ServerLogger() { flushBuffer(); - // logFile is a child of this object and is closed and deleted by ~QObject. // This does not work with the destroyed() signal as this destructor is called after the main event loop is done. thread()->quit(); } void ServerLogger::startLog(const QString &logFileName) { - delete logFile; - logFile = nullptr; - if (!logFileName.isEmpty()) { QFileInfo fi(logFileName); QDir fileDir(fi.path()); if (!fileDir.exists() && !fileDir.mkpath(fileDir.absolutePath())) { std::cerr << "ERROR: logfile folder doesn't exist and i can't create it." << std::endl; + logFile = 0; return; } @@ -47,81 +44,60 @@ void ServerLogger::startLog(const QString &logFileName) if (!logFile->open(QIODevice::Append)) { std::cerr << "ERROR: can't open() logfile." << std::endl; delete logFile; - logFile = nullptr; + logFile = 0; return; } + } else { + logFile = 0; } -} -void ServerLogger::reloadConfig(bool _writeLog, const QString &_logFilters) -{ - const bool filterActive = !_logFilters.trimmed().isEmpty(); - const QStringList filters = _logFilters.split(QStringLiteral(","), Qt::SkipEmptyParts); - - configMutex.lock(); - writeLog = _writeLog; - messageFilterActive = filterActive; - logFilters = filters; - configMutex.unlock(); + connect(this, SIGNAL(sigFlushBuffer()), this, SLOT(flushBuffer()), Qt::QueuedConnection); } bool ServerLogger::isLoggingEnabled() const { - // Mirror logMessage(): the console is an independent sink and the logfile needs writeLog. - QMutexLocker locker(&configMutex); - return logToConsole || (logFile != nullptr && writeLog); + return logFile != nullptr && settingsCache->value("server/writelog", 1).toBool(); } void ServerLogger::logMessage(const QString &message, void *caller) { + if (!logFile) { + return; + } + QString callerString; if (caller) { - callerString = QString::number(reinterpret_cast(caller), 16) + " "; + callerString = QString::number((qulonglong)caller, 16) + " "; } - const QString line = QDateTime::currentDateTime().toString() + " " + callerString + message; - bool queued = false; + // filter out all log entries based on values in configuration file + bool shouldWeWriteLog = settingsCache->value("server/writelog", 1).toBool(); + QString logFilters = settingsCache->value("server/logfilters").toString(); + QStringList listlogFilters = logFilters.split(",", Qt::SkipEmptyParts); + bool shouldWeSkipLine = false; - // The console is an independent sink: writelog and logfilters only describe the logfile - // (see servatrice.ini.example), so a disabled or unopenable logfile must not swallow - // startup errors such as a refused database connection. Lines are buffered for the logger - // thread instead of written here, so a stalled stdout reader cannot stall game threads. - if (logToConsole) { - bufferMutex.lock(); - consoleBuffer.append(line); - bufferMutex.unlock(); - queued = true; + if (!shouldWeWriteLog) { + return; } - configMutex.lock(); - const bool shouldWriteFile = writeLog; - const bool filterMessages = messageFilterActive; - const QStringList filters = logFilters; - configMutex.unlock(); - - if (logFile && shouldWriteFile) { - // filter out all log entries based on values in configuration file - bool shouldWriteLine = !filterMessages; - if (filterMessages) { - for (const QString &logFilter : filters) { - if (message.contains(logFilter, Qt::CaseInsensitive)) { - shouldWriteLine = true; - break; - } + if (!logFilters.trimmed().isEmpty()) { + shouldWeSkipLine = true; + for (const QString &logFilter : listlogFilters) { + if (message.contains(logFilter, Qt::CaseInsensitive)) { + shouldWeSkipLine = false; + break; } } - - if (shouldWriteLine) { - bufferMutex.lock(); - buffer.append(line); - bufferMutex.unlock(); - queued = true; - } } - if (queued) { - emit sigFlushBuffer(); + if (shouldWeSkipLine) { + return; } + + bufferMutex.lock(); + buffer.append(QDateTime::currentDateTime().toString() + " " + callerString + message); + bufferMutex.unlock(); + emit sigFlushBuffer(); } void ServerLogger::flushBuffer() @@ -131,42 +107,25 @@ void ServerLogger::flushBuffer() } flushRunning = true; - - bufferMutex.lock(); - QStringList consoleLines; - consoleLines.swap(consoleBuffer); - bufferMutex.unlock(); - - for (const QString &line : consoleLines) { - std::cout << line.toStdString() << '\n'; - } - if (!consoleLines.isEmpty()) { - std::cout.flush(); - } - - if (logFile) { - QTextStream stream(logFile); - forever - { - bufferMutex.lock(); - if (buffer.isEmpty()) { - bufferMutex.unlock(); - break; - } - QString message = buffer.takeFirst(); - bufferMutex.unlock(); - - stream << message << "\n"; - stream.flush(); - } - } else { - // No logfile to write to; do not let the buffer grow without bound. + QTextStream stream(logFile); + forever + { bufferMutex.lock(); - buffer.clear(); + if (buffer.isEmpty()) { + bufferMutex.unlock(); + flushRunning = false; + return; + } + QString message = buffer.takeFirst(); bufferMutex.unlock(); - } - flushRunning = false; + stream << message << "\n"; + stream.flush(); + + if (logToConsole) { + std::cout << message.toStdString() << std::endl; + } + } } void ServerLogger::rotateLogs() @@ -182,3 +141,5 @@ void ServerLogger::rotateLogs() std::cerr << "ERROR: Failed to open log file for writing!" << std::endl; } } + +QFile *ServerLogger::logFile; diff --git a/servatrice/src/server_logger.h b/servatrice/src/server_logger.h index 40df9fd62..e0be4b712 100644 --- a/servatrice/src/server_logger.h +++ b/servatrice/src/server_logger.h @@ -14,7 +14,7 @@ class ServerLogger : public QObject { Q_OBJECT public: - ServerLogger(bool _logToConsole, bool _writeLog, const QString &_logFilters, QObject *parent = nullptr); + ServerLogger(bool _logToConsole, QObject *parent = 0); ~ServerLogger(); /** @@ -27,7 +27,6 @@ public: public slots: void startLog(const QString &logFileName); - void reloadConfig(bool _writeLog, const QString &_logFilters); void logMessage(const QString &message, void *caller = 0); void rotateLogs(); private slots: @@ -37,17 +36,9 @@ signals: private: bool logToConsole; - bool writeLog = true; - bool messageFilterActive = false; - QStringList logFilters; - // Guards writeLog/messageFilterActive/logFilters: SIGHUP rewrites them while other threads log. - mutable QMutex configMutex; - QFile *logFile = nullptr; + static QFile *logFile; bool flushRunning; QStringList buffer; - // Console lines wait here for the logger thread, so game threads never block on stdout. - QStringList consoleBuffer; - // Guards buffer and consoleBuffer. QMutex bufferMutex; }; diff --git a/servatrice/src/serversocketinterface.cpp b/servatrice/src/serversocketinterface.cpp index 78593f6dc..2cb4df8ba 100644 --- a/servatrice/src/serversocketinterface.cpp +++ b/servatrice/src/serversocketinterface.cpp @@ -4651,15 +4651,8 @@ void WebsocketServerSocketInterface::initConnection(void *_socket) connect(socket, SIGNAL(binaryMessageReceived(const QByteArray &)), this, SLOT(binaryMessageReceived(const QByteArray &))); - // QWebSocket::errorOccurred was added in Qt 6.5 (error() is deprecated from 6.5 on), - // so a typed connect cannot compile against the 6.4 floor. -#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) - connect(socket, &QWebSocket::errorOccurred, this, - [this](QAbstractSocket::SocketError error) { catchSocketError(error); }); -#else connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(catchSocketError(QAbstractSocket::SocketError))); -#endif connect(socket, SIGNAL(disconnected()), this, SLOT(catchSocketDisconnected())); // Add this object to the server's list of connections before it can receive socket events. diff --git a/servatrice/src/signalhandler.cpp b/servatrice/src/signalhandler.cpp index b8a7e3538..32cd5b206 100644 --- a/servatrice/src/signalhandler.cpp +++ b/servatrice/src/signalhandler.cpp @@ -73,8 +73,6 @@ void SignalHandler::internalSigHupHandler() logger->rotateLogs(); settingsCache->sync(); - logger->reloadConfig(settingsCache->value("server/writelog", 1).toBool(), - settingsCache->value("server/logfilters").toString()); snHup->setEnabled(true); } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c8d3fbd59..e2c3252ee 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -178,9 +178,8 @@ cockatrice_add_test(NAME user_data_backup_test SOURCES user_data_backup_test.cpp cockatrice_add_test(NAME password_hash_test SOURCES password_hash_test.cpp LIBS libcockatrice_utility) cockatrice_add_test(NAME report_categories_test SOURCES report_categories_test.cpp LIBS libcockatrice_utility) cockatrice_add_test(NAME warning_categories_test SOURCES warning_categories_test.cpp LIBS libcockatrice_utility) -# The installer's holder probe is Windows-only, but the wording it produces and the -# question which programs are the payload's own are plain C++ and are what a stuck user -# has to act on, so they are tested everywhere. +# The installer's holder probe is Windows-only, but the wording it produces is plain C++ +# and is what a stuck user has to act on, so it is tested everywhere. cockatrice_add_test( NAME holder_line_test @@ -190,15 +189,6 @@ cockatrice_add_test( INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/tools/installer-holder-probe ) -cockatrice_add_test( - NAME - payload_paths_test - SOURCES - ${CMAKE_SOURCE_DIR}/tools/installer-holder-probe/payload_paths.cpp - payload_paths_test.cpp - INCLUDE_DIRS - ${CMAKE_SOURCE_DIR}/tools/installer-holder-probe -) cockatrice_add_test( NAME playmat_resolver_test @@ -251,17 +241,6 @@ cockatrice_add_test( LABELS server ) -cockatrice_add_test( - NAME - server_logger_test - SOURCES - ${CMAKE_SOURCE_DIR}/servatrice/src/server_logger.cpp - server_logger_test.cpp - INCLUDE_DIRS - ${CMAKE_SOURCE_DIR}/servatrice/src - LABELS - server -) cockatrice_add_test( NAME server_card_counter_test @@ -427,7 +406,6 @@ if(TARGET Qt6::lrelease) libcockatrice_utility DEFINITIONS TRANSLATION_LOADER_TEST_DIR="${CMAKE_CURRENT_BINARY_DIR}" - COCKATRICE_TRANSLATIONS_DIR="${CMAKE_SOURCE_DIR}/cockatrice/translations" ) add_dependencies(translation_loader_test translation_loader_test_qm) @@ -456,22 +434,6 @@ if(WITH_CLIENT) ${CMAKE_SOURCE_DIR}/cockatrice/src ${CMAKE_SOURCE_DIR}/libcockatrice_utility ) - # The installer's probe and the client build the socket name independently, and the - # command they exchange is spelled in both places. Compiling both ends into one test is - # what keeps them from drifting apart unnoticed: a mismatch would leave the installer - # unable to close an instance whose window it cannot reach. - cockatrice_add_test( - NAME - installer_close_server_test - SOURCES - ${CMAKE_SOURCE_DIR}/cockatrice/src/installer_close_server.cpp - ${CMAKE_SOURCE_DIR}/tools/installer-holder-probe/installer_close_name.cpp - installer_close_server_test.cpp - INCLUDE_DIRS - ${CMAKE_SOURCE_DIR}/cockatrice/src - ${CMAKE_SOURCE_DIR}/libcockatrice_utility - ${CMAKE_SOURCE_DIR}/tools/installer-holder-probe - ) # CrashReporter is client code; DEFINITIONS keeps the test hermetic by compiling the # no-op path (no sentry-native headers needed) regardless of WITH_CRASH_REPORTING. cockatrice_add_test( @@ -498,11 +460,6 @@ add_subdirectory(movecard_tests) add_subdirectory(oracle) add_subdirectory(settings) -# libcockatrice_game only exists when the client is built. -if(WITH_CLIENT) - add_subdirectory(game) -endif() - # theme_migration_test compiles client sources, whose headers are only reachable from a client build. if(WITH_CLIENT) add_subdirectory(theme_migration) diff --git a/tests/card_zone_algorithms/CMakeLists.txt b/tests/card_zone_algorithms/CMakeLists.txt index 2418092de..945f8a0ed 100644 --- a/tests/card_zone_algorithms/CMakeLists.txt +++ b/tests/card_zone_algorithms/CMakeLists.txt @@ -1,4 +1,4 @@ cockatrice_add_test( NAME card_zone_algorithms_test SOURCES card_zone_algorithms_test.cpp INCLUDE_DIRS - ${CMAKE_SOURCE_DIR}/libcockatrice_game/libcockatrice/game/zones + ${CMAKE_SOURCE_DIR}/cockatrice/src/game/zones ) diff --git a/tests/card_zone_algorithms/card_zone_algorithms_test.cpp b/tests/card_zone_algorithms/card_zone_algorithms_test.cpp index 3d4baa430..4f5282235 100644 --- a/tests/card_zone_algorithms/card_zone_algorithms_test.cpp +++ b/tests/card_zone_algorithms/card_zone_algorithms_test.cpp @@ -16,6 +16,7 @@ struct MockCard bool cardRefWasCalled = false; bool resetStateCalled = false; bool resetStateKeepAnnotations = false; + bool visibleSet = false; void setId(int id) { @@ -34,6 +35,11 @@ struct MockCard resetStateCalled = true; resetStateKeepAnnotations = keepAnnotations; } + + void setVisible(bool visible) + { + visibleSet = visible; + } }; class MockCardList @@ -103,6 +109,7 @@ TEST_F(AddCardAlgorithmTest, ContentsKnownPreservesIdentity) EXPECT_FALSE(card.idWasCalled); EXPECT_FALSE(card.cardRefWasCalled); + EXPECT_TRUE(card.visibleSet); } TEST_F(AddCardAlgorithmTest, ContentsUnknownClearsIdentity) diff --git a/tests/carddatabase/carddatabase_test.cpp b/tests/carddatabase/carddatabase_test.cpp index 22cbe9074..eb522434d 100644 --- a/tests/carddatabase/carddatabase_test.cpp +++ b/tests/carddatabase/carddatabase_test.cpp @@ -17,12 +17,10 @@ #include #include #include -#include #include #include #include #include -#include #include #include #include @@ -610,110 +608,6 @@ TEST_F(CardDatabaseCacheInvalidationTest, MarkAllSetsAsKnownIsBatched) // Ignored sets keep their known state but are enabled. EXPECT_TRUE(controller.isEnabled("XXX")); } - -TEST_F(CardDatabaseCacheInvalidationTest, ReloadReleasesPreviousSnapshot) -{ - NoopCardPreferenceProvider prefs; - SandboxPathProvider pathProvider(root); - CardDatabase db(nullptr, &prefs, &pathProvider, &controller); - - db.loadCardDatabases(); - CardInfoPtr card = db.getCardList().value("Cat"); - ASSERT_FALSE(card.isNull()); - QWeakPointer weakCard(card); - card.clear(); - - db.reloadCardDatabasesAndNotify(); - - // CardSet holds strong CardInfoPtrs and CardInfo holds the CardSetPtr through its printings; if that - // cycle is not broken, the previous snapshot survives the reload. - EXPECT_TRUE(weakCard.isNull()) << "a reload must release the previous card snapshot"; -} - -TEST_F(CardDatabaseCacheInvalidationTest, ClearReleasesCards) -{ - NoopCardPreferenceProvider prefs; - SandboxPathProvider pathProvider(root); - CardDatabase db(nullptr, &prefs, &pathProvider, &controller); - - db.loadCardDatabases(); - CardInfoPtr card = db.getCardList().value("Cat"); - ASSERT_FALSE(card.isNull()); - QWeakPointer weakCard(card); - card.clear(); - - db.clear(); - - EXPECT_TRUE(weakCard.isNull()) << "clear() must release cards held by set membership"; -} - -TEST_F(CardDatabaseCacheInvalidationTest, RemoveCardDetachesItFromItsSet) -{ - // removeCard() is the one teardown path a caller can trigger for a single card (the token editor - // uses it). Without the detach the set's strong reference keeps the removed card alive forever. - NoopCardPreferenceProvider prefs; - SandboxPathProvider pathProvider(root); - CardDatabase db(nullptr, &prefs, &pathProvider, &controller); - - db.loadCardDatabases(); - CardInfoPtr card = db.getCardList().value("Cat"); - ASSERT_FALSE(card.isNull()); - const CardSetPtr catSet = db.getSet("CAT"); - ASSERT_TRUE(catSet->contains(card)); - - db.removeCard(card); - - EXPECT_FALSE(catSet->contains(card)) << "removeCard() must detach the card from its set"; - QWeakPointer weakCard(card); - card.clear(); - EXPECT_TRUE(weakCard.isNull()) << "a removed card must not stay alive through its set"; -} - -TEST_F(CardDatabaseCacheInvalidationTest, RemoveSelfPointerStillNotifiesDatabaseCard) -{ - // cardInfoChanged() used to be emitted through a self pointer stored on the card. Without it the - // card must still notify its observers whenever its properties change. - NoopCardPreferenceProvider prefs; - SandboxPathProvider pathProvider(root); - CardDatabase db(nullptr, &prefs, &pathProvider, &controller); - - db.loadCardDatabases(); - const CardInfoPtr card = db.getCardList().value("Cat"); - ASSERT_FALSE(card.isNull()); - QSignalSpy spy(card.data(), &CardInfo::cardInfoChanged); - - card->setProperty("manacost", "1G"); - - ASSERT_EQ(spy.count(), 1) << "a property change on a database card must still emit cardInfoChanged"; - const QList arguments = spy.takeFirst(); - ASSERT_EQ(arguments.size(), 1); - EXPECT_EQ(arguments.first().value().data(), card.data()) - << "the signal must carry the card that changed"; -} - -TEST_F(CardDatabaseCacheInvalidationTest, RemoveSelfPointerCloneNotifiesAsItself) -{ - // A clone owns its own shared reference, so its changes must be reported as the clone's, never the - // original's: decks and views listening to the original must not be invalidated by a clone edit. - NoopCardPreferenceProvider prefs; - SandboxPathProvider pathProvider(root); - CardDatabase db(nullptr, &prefs, &pathProvider, &controller); - - db.loadCardDatabases(); - const CardInfoPtr original = db.getCardList().value("Cat"); - ASSERT_FALSE(original.isNull()); - const CardInfoPtr clone = original->clone(); - QSignalSpy originalSpy(original.data(), &CardInfo::cardInfoChanged); - QSignalSpy cloneSpy(clone.data(), &CardInfo::cardInfoChanged); - - clone->setProperty("manacost", "1G"); - - ASSERT_EQ(cloneSpy.count(), 1) << "the clone must notify its own observers"; - EXPECT_EQ(cloneSpy.first().first().value().data(), clone.data()) - << "the clone's signal must carry the clone"; - EXPECT_EQ(originalSpy.count(), 0) << "the clone must not notify the original's observers"; -} - } // namespace int main(int argc, char **argv) diff --git a/tests/game/CMakeLists.txt b/tests/game/CMakeLists.txt deleted file mode 100644 index 4a7492c71..000000000 --- a/tests/game/CMakeLists.txt +++ /dev/null @@ -1,28 +0,0 @@ -add_executable( - game_smoke_test - ${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/cache_settings.cpp - ${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/card_counter_settings.cpp - ${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/shortcuts_settings.cpp - ${CMAKE_SOURCE_DIR}/cockatrice/src/client/network/update/client/release_channel.cpp - ${VERSION_STRING_CPP} - game_smoke_test.cpp -) - -# SettingsCache is still reached from the game logic, but it lives in the application sources -# rather than in libcockatrice_settings, so the client settings sources are compiled in here -# exactly like loader_local_matching_test and theme_migration_test do. -target_include_directories(game_smoke_test PRIVATE ${CMAKE_SOURCE_DIR}/cockatrice/src) - -target_link_libraries( - game_smoke_test - PRIVATE libcockatrice_game - PRIVATE Threads::Threads - PRIVATE ${GTEST_BOTH_LIBRARIES} - PRIVATE ${TEST_QT_MODULES} -) - -add_test(NAME game_smoke_test COMMAND game_smoke_test) - -if(NOT GTEST_FOUND) - add_dependencies(game_smoke_test gtest) -endif() diff --git a/tests/game/game_smoke_test.cpp b/tests/game/game_smoke_test.cpp deleted file mode 100644 index 356d544bf..000000000 --- a/tests/game/game_smoke_test.cpp +++ /dev/null @@ -1,106 +0,0 @@ -/** - * @file game_smoke_test.cpp - * - * Links the game logic library into a target that contains no graphics code at all, and drives - * the two seams the graphics layer used to own: the attachment graph on CardState and the - * card-creation request a zone makes so the graphics side can build the matching card item. - */ - -#include "gtest/gtest.h" -#include -#include -#include -#include -#include -#include -#include - -namespace -{ - -CardRef cardRefFor(const QString &name) -{ - CardRef cardRef; - cardRef.name = name; - return cardRef; -} - -} // namespace - -TEST(CardStateTest, AttachmentGraphKeepsBothEndsConsistent) -{ - CardState host(nullptr, nullptr, cardRefFor(QStringLiteral("Host")), 1); - CardState guest(nullptr, nullptr, cardRefFor(QStringLiteral("Guest")), 2); - - guest.setAttachedTo(&host); - host.addAttachedCard(&guest); - - EXPECT_EQ(guest.getAttachedTo(), &host); - ASSERT_EQ(host.getAttachedCards().size(), 1); - EXPECT_EQ(host.getAttachedCards().first(), &guest); - - host.removeAttachedCard(&guest); - guest.setAttachedTo(nullptr); - - EXPECT_TRUE(host.getAttachedCards().isEmpty()); - EXPECT_EQ(guest.getAttachedTo(), nullptr); -} - -TEST(CardZoneLogicTest, AddsAndTakesCardsWithoutAGraphicsSide) -{ - HandZoneLogic zone(nullptr, QStringLiteral("hand"), false, false, true); - CardState card(nullptr, nullptr, cardRefFor(QStringLiteral("Lightning Bolt")), 7); - - QSignalSpy cardAddedSpy(&zone, &CardZoneLogic::cardAdded); - QSignalSpy cardCountSpy(&zone, &CardZoneLogic::cardCountChanged); - - zone.addCard(&card, false, 0); - - ASSERT_EQ(zone.getCards().size(), 1); - EXPECT_EQ(zone.getCards().first(), &card); - EXPECT_EQ(card.getZone(), &zone); - EXPECT_EQ(cardAddedSpy.count(), 1); - EXPECT_GE(cardCountSpy.count(), 1); - - EXPECT_EQ(zone.takeCard(0, 7), &card); - EXPECT_TRUE(zone.getCards().isEmpty()); - EXPECT_EQ(card.getId(), 7); -} - -TEST(CardZoneLogicTest, AsksTheGraphicsSideToCreateCards) -{ - HandZoneLogic zone(nullptr, QStringLiteral("library"), false, true, true); - QSignalSpy requestSpy(&zone, &CardZoneLogic::createCardRequested); - - ServerInfo_Card cardInfo; - cardInfo.set_name("Lightning Bolt"); - cardInfo.set_id(7); - cardInfo.set_x(0); - cardInfo.set_y(1); - - zone.createCardRequested(cardInfo, true); - - ASSERT_EQ(requestSpy.count(), 1); - const QList arguments = requestSpy.takeFirst(); - EXPECT_EQ(arguments.at(0).value().id(), 7); - EXPECT_TRUE(arguments.at(1).toBool()); -} - -int main(int argc, char **argv) -{ - // Keep SettingsCache reads and writes away from the real user profile: the game logic still - // reaches for a handful of settings, and the client settings sources linked into this test - // would otherwise touch the developer's own configuration. - QStandardPaths::setTestModeEnabled(true); - - QTemporaryDir home; - if (home.isValid()) { - qputenv("HOME", home.path().toLocal8Bit()); - } - - QCoreApplication app(argc, argv); - QLoggingCategory::setFilterRules("settings_cache.*=false"); - - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/tests/installer_close_server_test.cpp b/tests/installer_close_server_test.cpp deleted file mode 100644 index 158d0ce47..000000000 --- a/tests/installer_close_server_test.cpp +++ /dev/null @@ -1,101 +0,0 @@ -#include "installer_close_name.h" -#include "installer_close_server.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ -constexpr int HANDSHAKE_TIMEOUT_MS = 5000; - -/// The bytes both ends exchange, from the header they both include. The separate -/// WireCommandIsThePinnedBytes test is what catches an accidental change to the literal. -const QByteArray CLOSE_COMMAND = QByteArray::fromRawData(INSTALLER_CLOSE_COMMAND, sizeof(INSTALLER_CLOSE_COMMAND) - 1); - -/// Scopes the socket to this test process, so a Cockatrice the developer happens to be -/// running cannot claim the name and the test cannot close it. Kept short on purpose: -/// QLocalServer on Unix puts the socket file under QDir::tempPath(), and macOS caps that -/// path at 104 bytes (sun_path). A longer name passes on Linux (/tmp) and fails to listen -/// on the macOS CI runners, whose temp directory is already ~48 characters. -QByteArray testUserName() -{ - return QByteArrayLiteral("t") + QByteArray::number(QCoreApplication::applicationPid()); -} -} // namespace - -// The probe builds the name from the process id and the account name it reads from the -// environment; the client builds it with scopedLocalServerName(). They have to produce the -// same string or the installer can never reach the instance it is trying to close. -TEST(InstallerCloseServerTest, SocketNameMatchesTheProbeForTheSameProcessAndUser) -{ - constexpr unsigned long PROCESS_ID = 4242; - const QString userName = QStringLiteral("installer-test"); - const QString expected = QStringLiteral("CockatriceInstallerClose-4242-installer-test"); - - EXPECT_EQ(expected, InstallerCloseServer::socketNameForUser(PROCESS_ID, userName)); - EXPECT_EQ(expected.toStdWString(), installerCloseSocketName(PROCESS_ID, userName.toStdWString())); -} - -TEST(InstallerCloseServerTest, SocketNameScopesTheProcessIdByTheCurrentUser) -{ - qputenv("USER", testUserName()); - - EXPECT_EQ(QStringLiteral("CockatriceInstallerClose-4242-") + QString::fromLatin1(testUserName()), - InstallerCloseServer::socketName(4242)); -} - -// The line is a wire format shared with the Windows-only probe, which cannot be compiled -// into this test. Pinning the exact bytes here makes a change to the shared header a -// deliberate protocol change rather than a typo that only shows up in the installer. -TEST(InstallerCloseServerTest, WireCommandIsThePinnedBytes) -{ - EXPECT_EQ(QByteArrayLiteral("COCKATRICE_INSTALLER_CLOSE\n"), CLOSE_COMMAND); - EXPECT_EQ(27, CLOSE_COMMAND.size()); -} - -// The installer only writes the command; the listening instance is what turns it into a -// close. Anything but that exact line must not be treated as a request. -TEST(InstallerCloseServerTest, CloseRequestIsEmittedForTheProbeCommand) -{ - qputenv("USER", testUserName()); - - InstallerCloseServer server; - ASSERT_TRUE(server.listen()) << "the installer close socket could not be started"; - - int closeRequests = 0; - QObject::connect(&server, &InstallerCloseServer::closeRequested, &server, [&closeRequests]() { ++closeRequests; }); - - QLocalSocket socket; - socket.connectToServer(InstallerCloseServer::socketName(QCoreApplication::applicationPid())); - ASSERT_TRUE(socket.waitForConnected(HANDSHAKE_TIMEOUT_MS)) << "could not reach the installer close socket"; - ASSERT_EQ(CLOSE_COMMAND.size(), socket.write(CLOSE_COMMAND)) << "the close request was not accepted"; - - // No waitForBytesWritten(): on Windows QLocalSocket completes the write through the - // event dispatcher, and this test runs the server in the same thread, so a blocking - // wait would hold the loop the server needs to accept and read. The loop below is the - // real handshake: it flushes the write and delivers the server's readyRead. - QEventLoop loop; - QTimer timeout; - timeout.setSingleShot(true); - QObject::connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit); - QObject::connect(&server, &InstallerCloseServer::closeRequested, &loop, &QEventLoop::quit); - timeout.start(HANDSHAKE_TIMEOUT_MS); - loop.exec(); - - EXPECT_EQ(1, closeRequests) << "the probe's line must reach the close handler exactly once"; -} - -int main(int argc, char **argv) -{ - QCoreApplication app(argc, argv); - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/tests/payload_paths_test.cpp b/tests/payload_paths_test.cpp deleted file mode 100644 index c4576d751..000000000 --- a/tests/payload_paths_test.cpp +++ /dev/null @@ -1,57 +0,0 @@ -#include "payload_paths.h" - -#include -#include - -namespace -{ -/// The directory part of a path spelled with the same case as the install directory, which -/// is the comparison the probe makes once both sides are folded - see isSameDirectory in -/// restart_manager_holders.cpp. Kept as a plain string here so the shape of a path can be -/// tested without Windows. -bool runsFrom(const std::wstring &imagePath, const std::wstring &installDir) -{ - return parentDirectoryOf(imagePath) == installDir; -} -} // namespace - -TEST(PayloadPaths, SplitsAnImagePathIntoItsDirectoryAndName) -{ - EXPECT_EQ(fileNameOf(L"D:\\Games\\Cockatrice\\cockatrice.exe"), std::wstring(L"cockatrice.exe")); - EXPECT_EQ(parentDirectoryOf(L"D:\\Games\\Cockatrice\\cockatrice.exe"), std::wstring(L"D:\\Games\\Cockatrice")); - EXPECT_EQ(fileNameOf(L"cockatrice.exe"), std::wstring(L"cockatrice.exe")); - EXPECT_EQ(parentDirectoryOf(L"cockatrice.exe"), std::wstring()); - EXPECT_EQ(parentDirectoryOf(L"D:/Games/Cockatrice/cockatrice.exe"), std::wstring(L"D:/Games/Cockatrice")); -} - -TEST(PayloadPaths, OnlyAnImageInTheInstallDirectoryRunsFromIt) -{ - const std::wstring installDir = L"D:\\Games\\Cockatrice"; - EXPECT_TRUE(runsFrom(L"D:\\Games\\Cockatrice\\cockatrice.exe", installDir)); - // A folder the user keeps other applications in is not the installation, and neither is - // a sibling whose name merely begins with the same letters. - EXPECT_FALSE(runsFrom(L"D:\\Games\\Cockatrice\\old\\cockatrice.exe", installDir)); - EXPECT_FALSE(runsFrom(L"D:\\Games\\Cockatrice-old\\cockatrice.exe", installDir)); - EXPECT_FALSE(runsFrom(L"D:\\Games\\cockatrice.exe", installDir)); -} - -TEST(PayloadPaths, KnowsTheProgramsThePayloadInstalls) -{ - EXPECT_TRUE(isPayloadProgram(L"cockatrice.exe")); - EXPECT_TRUE(isPayloadProgram(L"oracle.exe")); - EXPECT_TRUE(isPayloadProgram(L"servatrice.exe")); - EXPECT_TRUE(isPayloadProgram(L"COCKATRICE.EXE")); - EXPECT_TRUE(isPayloadProgram(L"Servatrice.Exe")); -} - -TEST(PayloadPaths, LeavesEveryOtherProgramAlone) -{ - // A program the user keeps in the install directory, the uninstaller, a service host and - // a name that only looks like a payload program are all not instances of the payload. - EXPECT_FALSE(isPayloadProgram(L"uninstall.exe")); - EXPECT_FALSE(isPayloadProgram(L"tool.exe")); - EXPECT_FALSE(isPayloadProgram(L"svchost.exe")); - EXPECT_FALSE(isPayloadProgram(L"cockatrice2.exe")); - EXPECT_FALSE(isPayloadProgram(L"mycockatrice.exe")); - EXPECT_FALSE(isPayloadProgram(L"")); -} diff --git a/tests/server_card_counter_test.cpp b/tests/server_card_counter_test.cpp index 6b884864d..6308d80b9 100644 --- a/tests/server_card_counter_test.cpp +++ b/tests/server_card_counter_test.cpp @@ -8,14 +8,10 @@ #include #include #include -#include #include #include #include -// Referenced by other translation units in the server_remote library. -RNG_Abstract *rng = nullptr; - TEST(ServerCardCounter, IncrementNewCounter) { Server_Card card(CardRef{"TestCard", ""}, 1, 0, 0); diff --git a/tests/server_logger_test.cpp b/tests/server_logger_test.cpp deleted file mode 100644 index 3c2d047cc..000000000 --- a/tests/server_logger_test.cpp +++ /dev/null @@ -1,201 +0,0 @@ -#include "server_logger.h" - -#include "gtest/gtest.h" -#include -#include -#include -#include -#include -#include - -namespace -{ - -/** - * @brief Redirects std::cout for the lifetime of the object so the logger's console - * sink can be asserted on. - */ -class CoutCapture -{ -public: - CoutCapture() : previousBuffer(std::cout.rdbuf(captured.rdbuf())) - { - } - ~CoutCapture() - { - std::cout.rdbuf(previousBuffer); - } - - QString text() const - { - return QString::fromStdString(captured.str()); - } - -private: - std::ostringstream captured; - std::streambuf *previousBuffer; -}; - -// The console sink is written by the logger thread; drive it directly here, as the event -// loop would in the server. -void flushLogger(ServerLogger &logger) -{ - QMetaObject::invokeMethod(&logger, "flushBuffer", Qt::DirectConnection); -} - -// Regression: --log-to-console used to be gated behind a successfully opened logfile, so an -// empty server/logfile silenced every diagnostic, including critical startup errors. -TEST(ServerLoggerTest, ConsoleSinkDoesNotDependOnLogFile) -{ - ServerLogger logger(true, true, QString()); - logger.startLog(QString()); - - CoutCapture capture; - logger.logMessage("could not open the database"); - flushLogger(logger); - - EXPECT_TRUE(capture.text().contains("could not open the database")) << capture.text().toStdString(); -} - -// writelog only describes the logfile; disabling it must not silence the console. -TEST(ServerLoggerTest, ConsoleSinkSurvivesDisabledFileLog) -{ - QTemporaryDir dir; - ASSERT_TRUE(dir.isValid()); - - ServerLogger logger(true, false, QString()); - logger.startLog(dir.filePath("server.log")); - - CoutCapture capture; - logger.logMessage("startup failed"); - flushLogger(logger); - - EXPECT_TRUE(capture.text().contains("startup failed")) << capture.text().toStdString(); -} - -// logfilters selects what goes into the logfile; the console is not filtered. -TEST(ServerLoggerTest, ConsoleSinkIgnoresLogFilters) -{ - ServerLogger logger(true, true, "database"); - logger.startLog(QString()); - - CoutCapture capture; - logger.logMessage("unrelated message"); - flushLogger(logger); - - EXPECT_TRUE(capture.text().contains("unrelated message")) << capture.text().toStdString(); -} - -TEST(ServerLoggerTest, StaysQuietWhenConsoleIsDisabled) -{ - ServerLogger logger(false, false, QString()); - logger.startLog(QString()); - - CoutCapture capture; - logger.logMessage("not for the console"); - flushLogger(logger); - - EXPECT_TRUE(capture.text().isEmpty()) << capture.text().toStdString(); -} - -TEST(ServerLoggerTest, IsLoggingEnabledMirrorsTheSinks) -{ - QTemporaryDir dir; - ASSERT_TRUE(dir.isValid()); - - // The console is a sink of its own: messages must not be discarded for want of a logfile. - { - ServerLogger logger(true, false, QString()); - EXPECT_TRUE(logger.isLoggingEnabled()); - } - - // The logfile sink needs an open file and writelog; reloadConfig() carries the SIGHUP change. - { - ServerLogger logger(false, true, QString()); - EXPECT_FALSE(logger.isLoggingEnabled()) << "no logfile is open yet"; - logger.startLog(dir.filePath("enabled.log")); - EXPECT_TRUE(logger.isLoggingEnabled()); - logger.reloadConfig(false, QString()); - EXPECT_FALSE(logger.isLoggingEnabled()) << "writelog=false leaves no sink enabled"; - } -} - -TEST(ServerLoggerTest, WritesMatchingFilteredMessagesToTheLogFile) -{ - QTemporaryDir dir; - ASSERT_TRUE(dir.isValid()); - const QString logPath = dir.filePath("server.log"); - - { - ServerLogger logger(false, true, "database"); - logger.startLog(logPath); - logger.logMessage("database is unavailable"); - logger.logMessage("unrelated message"); - } - - QFile logFile(logPath); - ASSERT_TRUE(logFile.open(QIODevice::ReadOnly)); - const QString contents = QString::fromUtf8(logFile.readAll()); - EXPECT_TRUE(contents.contains("database is unavailable")) << contents.toStdString(); - EXPECT_FALSE(contents.contains("unrelated message")) << contents.toStdString(); -} - -// SIGHUP reloads the configuration; the logger must pick up the new filter. -TEST(ServerLoggerTest, ReloadConfigAppliesANewFilter) -{ - QTemporaryDir dir; - ASSERT_TRUE(dir.isValid()); - const QString logPath = dir.filePath("server.log"); - - { - ServerLogger logger(false, true, "database"); - logger.startLog(logPath); - logger.logMessage("filtered out"); - logger.reloadConfig(true, QString()); - logger.logMessage("kept after reload"); - } - - QFile logFile(logPath); - ASSERT_TRUE(logFile.open(QIODevice::ReadOnly)); - const QString contents = QString::fromUtf8(logFile.readAll()); - EXPECT_FALSE(contents.contains("filtered out")) << contents.toStdString(); - EXPECT_TRUE(contents.contains("kept after reload")) << contents.toStdString(); -} - -TEST(ServerLoggerTest, DisabledFileLogLeavesTheLogFileEmpty) -{ - QTemporaryDir dir; - ASSERT_TRUE(dir.isValid()); - const QString logPath = dir.filePath("server.log"); - - { - ServerLogger logger(false, false, QString()); - logger.startLog(logPath); - logger.logMessage("not for the logfile"); - } - - QFile logFile(logPath); - ASSERT_TRUE(logFile.open(QIODevice::ReadOnly)); - EXPECT_TRUE(logFile.readAll().isEmpty()); -} - -// A filter list consisting only of separators activates filtering with no entries; the old -// trimmed-empty check suppressed the logfile in that case and this preserves it. -TEST(ServerLoggerTest, DegenerateFilterSuppressesTheLogFile) -{ - QTemporaryDir dir; - ASSERT_TRUE(dir.isValid()); - const QString logPath = dir.filePath("server.log"); - - { - ServerLogger logger(false, true, ","); - logger.startLog(logPath); - logger.logMessage("anything"); - } - - QFile logFile(logPath); - ASSERT_TRUE(logFile.open(QIODevice::ReadOnly)); - EXPECT_TRUE(logFile.readAll().isEmpty()); -} - -} // namespace diff --git a/tests/translation_loader_test.cpp b/tests/translation_loader_test.cpp index 7a59e1b9b..745cd04bc 100644 --- a/tests/translation_loader_test.cpp +++ b/tests/translation_loader_test.cpp @@ -2,7 +2,6 @@ #include #include #include -#include #include #include #include @@ -20,11 +19,6 @@ #define TRANSLATION_LOADER_TEST_DIR "" #endif -// The client's translation files, whose language codes are the languages the language selector offers. -#ifndef COCKATRICE_TRANSLATIONS_DIR -#define COCKATRICE_TRANSLATIONS_DIR "" -#endif - namespace { const QString testContext = QStringLiteral("TranslationLoaderTest"); @@ -239,49 +233,6 @@ TEST(TranslationLoaderTest, AvailableLanguagesListsTheTranslationsThatCanBeLoade EXPECT_TRUE(TranslationLoader::availableLanguages(QStringLiteral("translation_loader_test"), {}).isEmpty()); } -TEST(TranslationLoaderTest, LanguageDisplayNamesDoNotNeedATranslationFile) -{ - // The regression this guards: the selector took every label from the language's own .qm file, so it - // went blank as soon as translators had not translated the "English" placeholder in the i18n context. - EXPECT_EQ(QStringLiteral("Deutsch (German)"), TranslationLoader::languageDisplayName(QStringLiteral("de"))); - EXPECT_EQ(QStringLiteral("简体中文 (Chinese Simplified)"), - TranslationLoader::languageDisplayName(QStringLiteral("zh-Hans"))); - EXPECT_EQ(QStringLiteral("English, arr! (Pirate English)"), - TranslationLoader::languageDisplayName(QStringLiteral("en@pirate"))); -} - -TEST(TranslationLoaderTest, EveryLanguageTheClientShipsHasItsOwnDisplayName) -{ - // The shipped translation files are what the selector offers, so each of them has to end up with a - // label: an empty one is what made the dropdown look broken, and two equal ones are a coin flip. - const QDir translations(QStringLiteral(COCKATRICE_TRANSLATIONS_DIR)); - const QStringList files = translations.entryList({QStringLiteral("cockatrice_*.ts")}, QDir::Files); - ASSERT_FALSE(files.isEmpty()); - - QStringList languages; - QSet displayNames; - for (const QString &file : files) { - const QString language = QFileInfo(file).completeBaseName().section(QLatin1Char('_'), 1); - const QString displayName = TranslationLoader::languageDisplayName(language); - EXPECT_FALSE(displayName.isEmpty()) << language.toStdString(); - languages << language; - displayNames.insert(displayName); - } - - EXPECT_EQ(files.size(), displayNames.size()); - - // The selector preselects this one for an unset setting, so it has to be offered at all. - EXPECT_TRUE(languages.contains(TranslationLoader::defaultLanguageCode())); -} - -TEST(TranslationLoaderTest, UnknownLanguagesFallBackToTheNativeNameThenTheCode) -{ - // A language added after the table, and a code Qt does not know either. - EXPECT_FALSE(TranslationLoader::languageDisplayName(QStringLiteral("sv_FI")).isEmpty()); - EXPECT_EQ(QStringLiteral("zz_QQ"), TranslationLoader::languageDisplayName(QStringLiteral("zz_QQ"))); - EXPECT_TRUE(TranslationLoader::languageDisplayName(QString()).isEmpty()); -} - TEST(TranslationLoaderTest, LoadApplicationReportsAMissingTranslationDirectory) { QTemporaryDir emptyDir; diff --git a/tools/installer-holder-probe/CMakeLists.txt b/tools/installer-holder-probe/CMakeLists.txt index 99e7d5045..3098669cc 100644 --- a/tools/installer-holder-probe/CMakeLists.txt +++ b/tools/installer-holder-probe/CMakeLists.txt @@ -1,28 +1,13 @@ -# Windows-only. The installer runs this before anything is replaced (the running-instance -# guard in .onInit) and on the abort path of a failed purge, to name the applications and -# services holding the payload open (Restart Manager); see the GuardAppsNotRunning and -# LogProcessesUsingInstallDir functions in cmake/NSIS.template.in. It ships inside the -# installer and the uninstaller and is extracted to $PLUGINSDIR there, never installed, so -# it links nothing from this project: no Qt, and a static C runtime because the installed -# runtime is exactly what the purge may have failed to replace. -add_executable( - installer-holder-probe - close_holders.cpp - holder_line.cpp - installer_close_name.cpp - installer_close_request.cpp - main.cpp - payload_locks.cpp - payload_paths.cpp - restart_manager_holders.cpp -) +# Windows-only. The installer runs this on the abort path of a failed purge to name the +# applications and services holding the install directory open (Restart Manager); see +# the LogProcessesUsingInstallDir function in cmake/NSIS.template.in. It ships inside +# the installer and is extracted to $PLUGINSDIR there, never installed, so it links +# nothing from this project: no Qt, and a static C runtime because the installed runtime +# is exactly what the purge may have failed to replace. +add_executable(installer-holder-probe holder_line.cpp main.cpp restart_manager_holders.cpp) target_link_libraries(installer-holder-probe PRIVATE rstrtmgr) -# The close command is a header-only constant shared with the client. The probe still links -# nothing from this project: the header is plain C++ and pulls in no library. -target_include_directories(installer-holder-probe PRIVATE ${CMAKE_SOURCE_DIR}/libcockatrice_utility) - # The NSIS template embeds the built file by the same relative path that resolves # NSIS.definitions.nsh, and that path points at the build root. set_target_properties( diff --git a/tools/installer-holder-probe/close_holders.cpp b/tools/installer-holder-probe/close_holders.cpp deleted file mode 100644 index 24fe7f0e7..000000000 --- a/tools/installer-holder-probe/close_holders.cpp +++ /dev/null @@ -1,417 +0,0 @@ -#include "close_holders.h" - -#include "installer_close_request.h" -#include "payload_paths.h" -#include "restart_manager_holders.h" - -// clang-format off -// windows.h has to come before anything that pulls in a Windows type, as everywhere in -// this tool. -#include -// clang-format on - -#include -#include -#include -#include - -namespace -{ -/// How long one poll of the asked processes takes. One WaitForSingleObject per process -/// and no enumeration, so polling costs nothing next to the seconds this is called on. -constexpr unsigned long WAIT_POLL_MILLISECONDS = 200; - -/// How long the report line may get before it is cut. NSIS holds one string in 1024 -/// characters, and this one is written there verbatim; every holder is named, so normally -/// the line stays far below it. -constexpr std::size_t REPORT_CHAR_MAX = 900; - -/// The holder as the log has to name it to be actionable. Deliberately not -/// formatHolder(): that one is worded for the dialog, where ", running" says something -/// the line here continues after. -std::wstring processLabel(const Holder &holder) -{ - std::wstring label = holder.imageName.empty() ? holder.appName : holder.imageName; - if (label.empty()) { - label = L"an unnamed process"; - } - label += L" (pid "; - label += std::to_wstring(holder.processId); - label += L")"; - return label; -} - -/// Joins the per-holder lines into the one line the installer logs and cuts it to what one -/// NSIS string holds. -std::wstring joinReport(const std::vector &lines) -{ - std::wstring report; - for (const std::wstring &line : lines) { - if (!report.empty()) { - report += L"; "; - } - report += line; - } - if (report.size() > REPORT_CHAR_MAX) { - report.erase(REPORT_CHAR_MAX); - report += L"..."; - } - return report; -} - -struct WindowSearch -{ - DWORD processId = 0; - std::vector windows; ///< every top-level window of the process, in Z-order -}; - -BOOL CALLBACK collectProcessWindow(HWND window, LPARAM parameter) -{ - auto &search = *reinterpret_cast(parameter); - DWORD processId = 0; - GetWindowThreadProcessId(window, &processId); - if (processId == search.processId) { - search.windows.push_back(window); - } - return TRUE; -} - -/// Whether a window is one the user could be asked to answer. -/// -/// Only a framed, non-empty window qualifies. Qt keeps hidden top-level windows of its own -/// next to the application's: the tray icon creates one to receive the TaskbarCreated -/// message, styled WS_OVERLAPPED - which is no frame at all - and titled -/// QTrayIconMessageWindow. Showing one of those puts an empty white rectangle on the -/// taskbar that nothing ever closes again: it has no window procedure of its own to hide it, -/// and the process is busy in the loop this is meant to break, so it stays until the process -/// dies. Posting a close to one is worse than useless too: Qt's tray window answers WM_CLOSE -/// by asking the application to terminate, without going through the confirmation the close -/// is supposed to reach. -bool isAnswerableWindow(HWND window) -{ - if ((GetWindowLongPtr(window, GWL_EXSTYLE) & WS_EX_TOOLWINDOW) != 0) { - return false; - } - if ((GetWindowLongPtr(window, GWL_STYLE) & WS_CAPTION) == 0) { - return false; - } - RECT bounds{}; - if (GetWindowRect(window, &bounds) == FALSE) { - return false; - } - return bounds.right > bounds.left && bounds.bottom > bounds.top; -} - -/// Picks the window to post the close to and the one the user should end up looking at. -struct CloseTarget -{ - HWND main = nullptr; ///< first answerable window without an owner: the application's own - HWND toRaise = nullptr; ///< topmost visible window: a dialog when one is up - bool hidden = false; ///< the window being closed is not on screen (notification area) -}; - -/// Only the windows the user could be asked to answer are candidates - see isAnswerableWindow. -/// -/// A hidden window is a candidate like a visible one: WM_CLOSE does not need a window on -/// screen, so an instance that is only in the notification area answers it the way it answers -/// its own close button, and the confirmation it raises then is a window Qt shows itself - -/// painted, unlike one restored from outside. Nothing is shown from here. Qt's own bookkeeping -/// windows stay excluded, because a close posted to the notification-area one terminates the -/// instance without the confirmation and that is worse than no close at all. -/// -/// @param windows every top-level window the process owns, in Z-order -/// @param askHidden when false, only a window that is on screen is a target: the launcher -/// mode reads "no window" as "past its own prompts and stuck", which stops being true -/// the moment a hidden window is asked to close -CloseTarget chooseCloseTarget(const std::vector &windows, bool askHidden) -{ - CloseTarget target; - for (const HWND window : windows) { - if (!isAnswerableWindow(window)) { - continue; - } - const bool visible = IsWindowVisible(window) != FALSE; - if (visible && target.toRaise == nullptr) { - target.toRaise = window; - } - if (target.main == nullptr && GetWindow(window, GW_OWNER) == nullptr && (visible || askHidden)) { - target.main = window; - target.hidden = !visible; - } - } - return target; -} - -/// What is left of the window the close was posted to, so a log line tells an instance that -/// answered by hiding from one that never answered at all. A process that hid its window and -/// stayed alive is the one the guard's end answer is for. -/// -/// @param window the window the close was posted to, or null when there was none -/// @param wasHidden whether that window was already off screen before the close -std::wstring describeWindowState(HWND window, bool wasHidden) -{ - if (window == nullptr || IsWindow(window) == FALSE) { - return L", its window is gone"; - } - // WM_NULL is the cheapest way to ask whether the thread behind the window still dispatches. - // A hung instance does not answer at all, which is a different problem from one that - // answered the close and stayed open, and the two want different words in the prompt. - DWORD_PTR ignored = 0; - const bool responding = - SendMessageTimeoutW(window, WM_NULL, 0, 0, SMTO_ABORTIFHUNG | SMTO_BLOCK, 1000, &ignored) != 0; - std::wstring state; - if (IsWindowVisible(window) != FALSE) { - state = L", window still open"; - } else { - state = wasHidden ? L", window still hidden" : L", window hidden again"; - } - if (!responding) { - state += L" and not responding"; - } - return state; -} - -/// Restores a minimized window and takes the foreground, so a confirmation Cockatrice -/// answers the close with is in front of the installer instead of behind it. -/// -/// A process in the background may not take the foreground on its own. Attaching to the -/// thread that owns the foreground window lifts that restriction for the duration of the -/// call; detaching afterwards leaves the two input queues as they were. -bool bringForward(HWND window) -{ - if (IsIconic(window) != FALSE) { - ShowWindow(window, SW_RESTORE); - } else { - ShowWindow(window, SW_SHOW); - } - - const HWND foreground = GetForegroundWindow(); - const DWORD foregroundThread = foreground != nullptr ? GetWindowThreadProcessId(foreground, nullptr) : 0; - const DWORD currentThread = GetCurrentThreadId(); - const bool attached = foregroundThread != 0 && foregroundThread != currentThread && - AttachThreadInput(currentThread, foregroundThread, TRUE) != FALSE; - BringWindowToTop(window); - SetForegroundWindow(window); - if (attached) { - AttachThreadInput(currentThread, foregroundThread, FALSE); - } - return GetForegroundWindow() == window; -} - -/// Waits for @p processes to signal, up to @p waitMilliseconds. Returns whether they all -/// exited and how long the wait took. -bool waitForExit(const std::vector &processes, unsigned long waitMilliseconds, unsigned long long &elapsed) -{ - const ULONGLONG start = GetTickCount64(); - const ULONGLONG deadline = start + waitMilliseconds; - for (;;) { - bool allExited = true; - for (const HANDLE process : processes) { - if (WaitForSingleObject(process, 0) != WAIT_OBJECT_0) { - allExited = false; - break; - } - } - elapsed = GetTickCount64() - start; - if (allExited || GetTickCount64() >= deadline) { - return allExited; - } - Sleep(WAIT_POLL_MILLISECONDS); - } -} -} // namespace - -bool isCockatriceImage(const std::wstring &imageName) -{ - constexpr wchar_t COCKATRICE_IMAGE[] = L"cockatrice.exe"; - return CompareStringOrdinal(imageName.c_str(), -1, COCKATRICE_IMAGE, -1, TRUE) == CSTR_EQUAL; -} - -CloseHoldersResult -closeCockatriceHolders(const std::vector &holders, unsigned long waitMilliseconds, bool askHiddenWindows) -{ - CloseHoldersResult result; - std::vector lines; - std::vector asked; - std::vector askedLines; - std::vector askedWindows; - std::vector askedWasHidden; - - for (const Holder &holder : holders) { - if (holder.isService || !isCockatriceImage(holder.imageName)) { - // Oracle and Servatrice are named by the prompt and close by hand: Oracle - // writes the card database in place. A service has no window to ask either. - lines.push_back(processLabel(holder) + L" left to you"); - continue; - } - - WindowSearch search; - search.processId = holder.processId; - EnumWindows(collectProcessWindow, reinterpret_cast(&search)); - CloseTarget target = chooseCloseTarget(search.windows, askHiddenWindows); - if (target.main == nullptr && !askHiddenWindows) { - // A launcher with no window on screen has already answered its own close and is - // stuck in shutdown (see the header): asking it again cannot reach a user who is - // not there, so it is named and the installer's wait ends it. - ++result.withoutWindow; - lines.push_back(processLabel(holder) + L" has no window of its own to close"); - continue; - } - - std::wstring line = processLabel(holder); - if (target.main == nullptr) { - // No window of the application's own at all: every window left is one of Qt's - // bookkeeping windows, which must never be asked to close. The installer socket - // below is the only thing that can reach it. - } else if (target.hidden) { - line += L" is hidden in the notification area"; - } else { - if (target.toRaise == nullptr) { - target.toRaise = target.main; - } - line += bringForward(target.toRaise) ? L" brought forward" : L" could not be brought forward"; - } - - // Kept open across the wait so a pid reused after the process exits cannot be - // mistaken for the instance that was asked to close. - const HANDLE process = OpenProcess(SYNCHRONIZE, FALSE, holder.processId); - const DWORD openError = process == nullptr ? GetLastError() : ERROR_SUCCESS; - - // Ask over the installer socket first. It is the same request as the window's close - // button - the application still answers its own confirmation - but it reaches an - // instance whose window is hidden in the notification area or is sitting on that - // confirmation, the two states a posted close cannot get through. A client from - // before the socket answers with a failed connect, so the posted close below stays - // the fallback and older versions keep behaving exactly as they did. - if (requestInstallerClose(holder.processId)) { - // "and" only when the line already says something about the instance's window. - line += target.main == nullptr ? L" asked to close over its installer socket" - : L" and asked to close over its installer socket"; - } else if (target.main == nullptr) { - // Nothing of the application's own to post a close to, and no socket to ask: the - // instance is named and the guard's end answer is what can free its files. - if (process != nullptr) { - CloseHandle(process); - } - ++result.withoutWindow; - lines.push_back(line + L" has no window of its own to close"); - continue; - } else if (PostMessageW(target.main, WM_CLOSE, 0, 0) == FALSE) { - // Posted to the instance's own window even when a question of its own is up. The - // close is what that window's close button does, and the application is the one - // that answers it: a current client refuses the close while its confirmation is - // pending, leaving the question in front, and only a client from before that guard - // accepts it and hides the window with the question. Withholding the request - // therefore protected nobody - it left the installer asking the user to close a - // program it had never asked - while the guard's end answer is what frees the files - // when the close cannot. - // - // Read the error before CloseHandle, which is free to reset it. - const DWORD postError = GetLastError(); - if (process != nullptr) { - CloseHandle(process); - } - lines.push_back(line + L", and the close request could not be sent (win32 " + std::to_wstring(postError) + - L")"); - continue; - } else { - line += L" and asked to close"; - } - ++result.askedToClose; - if (process == nullptr) { - line += L", but its exit could not be watched (win32 " + std::to_wstring(openError) + L")"; - } else { - askedLines.push_back(lines.size()); - asked.push_back(process); - askedWindows.push_back(target.main); - askedWasHidden.push_back(target.hidden); - } - lines.push_back(std::move(line)); - } - - if (!asked.empty() && waitMilliseconds > 0) { - unsigned long long elapsed = 0; - const bool allExited = waitForExit(asked, waitMilliseconds, elapsed); - for (std::size_t index = 0; index < asked.size(); ++index) { - const bool exited = allExited || WaitForSingleObject(asked[index], 0) == WAIT_OBJECT_0; - if (exited) { - lines[askedLines[index]] += L", exited after " + std::to_wstring(elapsed) + L" ms"; - } else if (askedWindows[index] == nullptr) { - // Asked over the socket with no window of its own: there is no window state - // to describe, only that it is still there. - lines[askedLines[index]] += L", still running after " + std::to_wstring(elapsed) + L" ms"; - } else { - lines[askedLines[index]] += L", still running after " + std::to_wstring(elapsed) + L" ms" + - describeWindowState(askedWindows[index], askedWasHidden[index]); - } - } - } - for (const HANDLE process : asked) { - CloseHandle(process); - } - - if (lines.empty()) { - result.report = L"nothing is holding the files"; - return result; - } - result.report = joinReport(lines); - return result; -} - -EndHoldersResult endHolders(const std::vector &holders, unsigned long waitMilliseconds) -{ - EndHoldersResult result; - std::vector lines; - std::vector ended; - std::vector endedLines; - - for (const Holder &holder : holders) { - if (!isPayloadProgram(holder.imageName)) { - // Not one of the programs the payload installs: a scanner that has a DLL open, or - // a program the user keeps in the install directory. It is named and left to the - // user, because ending it would destroy something this installer does not own. - lines.push_back(processLabel(holder) + L" left to you"); - continue; - } - - // Services included: the installer asked before getting here, and the Service - // Control Manager reports the terminated process as a stopped service afterwards. - const HANDLE process = OpenProcess(PROCESS_TERMINATE | SYNCHRONIZE, FALSE, holder.processId); - if (process == nullptr) { - lines.push_back(processLabel(holder) + L" could not be ended (win32 " + std::to_wstring(GetLastError()) + - L")"); - continue; - } - if (TerminateProcess(process, 1) == FALSE) { - // Read the error before CloseHandle, which is free to reset it. - const DWORD error = GetLastError(); - CloseHandle(process); - lines.push_back(processLabel(holder) + L" could not be ended (win32 " + std::to_wstring(error) + L")"); - continue; - } - ++result.ended; - endedLines.push_back(lines.size()); - lines.push_back(processLabel(holder) + L" ended"); - ended.push_back(process); - } - - if (!ended.empty() && waitMilliseconds > 0) { - unsigned long long elapsed = 0; - const bool allExited = waitForExit(ended, waitMilliseconds, elapsed); - for (std::size_t index = 0; index < ended.size(); ++index) { - const bool exited = allExited || WaitForSingleObject(ended[index], 0) == WAIT_OBJECT_0; - lines[endedLines[index]] += exited ? L", exited after " + std::to_wstring(elapsed) + L" ms" - : L", still running after " + std::to_wstring(elapsed) + L" ms"; - } - } - for (const HANDLE process : ended) { - CloseHandle(process); - } - - if (lines.empty()) { - result.report = L"nothing is holding the files"; - return result; - } - result.report = joinReport(lines); - return result; -} diff --git a/tools/installer-holder-probe/close_holders.h b/tools/installer-holder-probe/close_holders.h deleted file mode 100644 index 3f6c889fd..000000000 --- a/tools/installer-holder-probe/close_holders.h +++ /dev/null @@ -1,113 +0,0 @@ -#ifndef CLOSE_HOLDERS_H -#define CLOSE_HOLDERS_H - -#include "holder_line.h" - -#include -#include - -/** - * @brief What asking the holders to close did, in the form the installer logs. - * - * @c report is one line naming every holder, including the ones this deliberately - * leaves alone, because the installer repeats it in its log. - * @c withoutWindow counts the holders that had no window of their own to ask: they were - * named and left alone, and the launcher mode reads that count as "nothing to ask" rather - * than as a window the user still has to answer. - */ -struct CloseHoldersResult -{ - unsigned int askedToClose = 0; ///< holders asked to close, over the socket or with WM_CLOSE - unsigned int withoutWindow = 0; ///< holders with no window of their own to ask - std::wstring report; ///< one line, narrowed to ASCII when written -}; - -/** - * @brief Whether @p imageName is the client the update belongs to, matched without case. - * - * The close mode asks only this image to close, and the launcher mode is only allowed to - * report it as safe to end, so both read the answer from here. - * - * @param imageName the file name of a process image, empty when it could not be read - */ -bool isCockatriceImage(const std::wstring &imageName); - -/** - * @brief Brings every running Cockatrice holding an installer file forward and asks it - * to close. - * - * The set is the Restart Manager answer, so it is exactly the processes holding a file of - * the previous version: nothing here has to guess at an executable path, and an - * application that merely shares the image name is never touched. Only cockatrice.exe is - * asked to close. Oracle and Servatrice are left to the user - Oracle writes the card - * database in place - and so is every service. - * - * One thing happens per instance: it is asked to close over the local socket it listens on, - * which is the same request as clicking its close button and is the only way to reach 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, whether - * the window is on screen or only in the notification area, since the request does not need - * the window to be visible and the confirmation Cockatrice answers it with is a window Qt - * shows itself, painted, unlike one restored from outside. A window that is on screen is - * brought forward first, because that confirmation would otherwise open behind the - * installer's prompt; nothing is shown from here. Qt's own bookkeeping windows are never the - * target - posting a close to the notification-area one terminates the instance without its - * confirmation - so a process without a window of its own that does not answer the socket is - * named and left to the guard, which offers to end it. Stopping the process is never - * attempted here. - * - * Once the requests are out, this waits up to @c waitMilliseconds for the processes to - * exit, so the caller can measure the purge immediately afterwards instead of sleeping - * for a fixed time or re-asking a question this call already answered. A process that is - * still running is described further: whether its window is gone, still open, still - * hidden or hidden again, and whether it answers at all, so the log can tell an instance - * that refused the close from one that never received it. - * - * @param holders the Restart Manager answer, one entry per application or service - * @param waitMilliseconds how long to wait for the asked instances to exit, 0 to not wait - * @param askHiddenWindows whether a window that is not on screen is asked to close too. - * The guard wants that: an instance in the notification area is the one this update - * has no other way to reach. The launcher mode does not - a launcher without a - * visible window has already answered its own close and is stuck in shutdown, and - * the installer ends it instead of putting a question to a user who is not there. - */ -CloseHoldersResult -closeCockatriceHolders(const std::vector &holders, unsigned long waitMilliseconds, bool askHiddenWindows); - -/** - * @brief What ending the holders did, in the form the installer logs. - * - * @c report is one line naming every holder, including the ones this deliberately leaves - * alone, because the installer repeats it in its log. - */ -struct EndHoldersResult -{ - unsigned int ended = 0; ///< holders TerminateProcess was accepted for - std::wstring report; ///< one line, narrowed to ASCII when written -}; - -/** - * @brief Ends every running Cockatrice, Oracle and Servatrice the probe named. - * - * This is the last resort behind the installer's "end it now" answer, never a first move: a - * client that is still running may have an unsaved deck, an open game or a settings write in - * flight, Oracle may be part-way through writing the card database, and Servatrice may be - * serving a game, so the caller only gets here after the user picked it from a prompt that - * says so. - * - * Every program the payload installs is ended, services included. A service is terminated - * like a process rather than stopped through the Service Control Manager: the installer has - * asked the user first, and the SCM notices the process exit and marks the service stopped - * afterwards, so it can be started again normally. Anything else the Restart Manager named - - * a scanner holding a DLL, a program the user keeps in the install directory - is left to - * the user, because ending it would destroy something this installer does not own. - * - * Waits up to @c waitMilliseconds for what it ended to be gone, so the caller can measure the - * purge immediately afterwards instead of racing a process that is still exiting. - * - * @param holders what the probe named, one entry per application or service - * @param waitMilliseconds how long to wait for the ended instances to be gone, 0 to not wait - */ -EndHoldersResult endHolders(const std::vector &holders, unsigned long waitMilliseconds); - -#endif diff --git a/tools/installer-holder-probe/holder_line.h b/tools/installer-holder-probe/holder_line.h index 675d79fc6..7fcc92375 100644 --- a/tools/installer-holder-probe/holder_line.h +++ b/tools/installer-holder-probe/holder_line.h @@ -26,15 +26,12 @@ inline constexpr std::size_t HOLDER_LINE_MAX = 5; /** * @brief How many characters the summary may spend on the holders it names. * - * The line is not the whole string: every prompt puts it after a fixed paragraph and before - * the answers, and one NSIS string holds 1024 characters. Those paragraphs grew when the - * guard started spelling out what ending the programs costs and what is left to do by hand, - * so this budget is what keeps the longest message - the one with the longest listing, plus - * the "; and N more" the summary appends after the budget - complete, including the sentence - * that offers the other answer at its end. Windows caps one application or service name well - * below this, so it bounds the list without ever cutting a single holder short. + * The line is not the whole string: the abort dialog puts it after a fixed paragraph and + * before the path of the installer log, inside an NSIS string of 1024 characters. Windows + * caps one application or service name well below this budget, so it bounds the list + * without ever cutting a single holder short. */ -inline constexpr std::size_t HOLDER_LINE_CHAR_MAX = 320; +inline constexpr std::size_t HOLDER_LINE_CHAR_MAX = 400; /** * @brief Renders one holder the way the user has to recognise it, e.g. diff --git a/tools/installer-holder-probe/installer_close_name.cpp b/tools/installer-holder-probe/installer_close_name.cpp deleted file mode 100644 index 74659e151..000000000 --- a/tools/installer-holder-probe/installer_close_name.cpp +++ /dev/null @@ -1,12 +0,0 @@ -#include "installer_close_name.h" - -#include - -std::wstring installerCloseSocketName(unsigned long processId, const std::wstring &userName) -{ - std::wstring name = L"CockatriceInstallerClose-"; - name += std::to_wstring(processId); - name += L"-"; - name += userName; - return name; -} diff --git a/tools/installer-holder-probe/installer_close_name.h b/tools/installer-holder-probe/installer_close_name.h deleted file mode 100644 index b659f8b8f..000000000 --- a/tools/installer-holder-probe/installer_close_name.h +++ /dev/null @@ -1,24 +0,0 @@ -#ifndef INSTALLER_CLOSE_NAME_H -#define INSTALLER_CLOSE_NAME_H - -#include - -/** - * @brief The local-socket name a running instance listens on for the installer's close - * request. - * - * The name carries the process id, so every instance of the payload has its own socket - a - * second Cockatrice that was started independently, an Oracle and a Servatrice - and the - * installer can ask each one instead of guessing which is the primary. It also carries the - * account name, so one user's installer cannot reach another user's session. - * - * This is the same string the client builds with scopedLocalServerName() in - * libcockatrice_utility/local_server_name.h: `CockatriceInstallerClose--`. It is - * plain C++ so the probe and the client can be tested against each other off Windows. - * - * @param processId the id of the process that listens on the socket - * @param userName the account name scopedLocalServerName() appends - */ -std::wstring installerCloseSocketName(unsigned long processId, const std::wstring &userName); - -#endif // INSTALLER_CLOSE_NAME_H diff --git a/tools/installer-holder-probe/installer_close_request.cpp b/tools/installer-holder-probe/installer_close_request.cpp deleted file mode 100644 index 0475aedf3..000000000 --- a/tools/installer-holder-probe/installer_close_request.cpp +++ /dev/null @@ -1,96 +0,0 @@ -#include "installer_close_request.h" - -#include "installer_close_name.h" - -// clang-format off -// windows.h has to come before anything that pulls in a Windows type, as everywhere in -// this tool. -#include -// clang-format on - -#include -#include -#include -#include - -namespace -{ -/// How long a busy pipe may take before this gives up. The client answers in milliseconds; -/// the wait only covers a machine under load. -constexpr DWORD PIPE_WAIT_MILLISECONDS = 1000; - -std::wstring environmentVariable(const wchar_t *name) -{ - for (DWORD size = MAX_PATH; size <= 32768U; size *= 2) { - std::vector buffer(size); - const DWORD length = GetEnvironmentVariableW(name, buffer.data(), size); - if (length == 0) { - return {}; - } - if (length < size) { - return std::wstring(buffer.data(), length); - } - } - return {}; -} - -/// The account name scopedLocalServerName() appends: USER, then USERNAME, then the last -/// component of the profile path - the same order and the same fallbacks the client uses. -std::wstring currentUserName() -{ - for (const wchar_t *name : {L"USER", L"USERNAME"}) { - const std::wstring value = environmentVariable(name); - if (!value.empty()) { - return value; - } - } - const std::wstring profile = environmentVariable(L"USERPROFILE"); - const std::size_t separator = profile.find_last_of(L"\\/"); - if (separator == std::wstring::npos) { - return profile; - } - return profile.substr(separator + 1); -} - -/// The named pipe QLocalServer creates for the client's socket, or empty when the account -/// name could not be read and there is nothing to connect to. -std::wstring pipePath(unsigned long processId) -{ - const std::wstring userName = currentUserName(); - if (userName.empty()) { - return {}; - } - return L"\\\\.\\pipe\\" + installerCloseSocketName(processId, userName); -} -} // namespace - -bool requestInstallerClose(unsigned long processId) -{ - const std::wstring path = pipePath(processId); - if (path.empty()) { - return false; - } - // A client that is still serving the previous connection answers ERROR_PIPE_BUSY; one - // retry covers that, while a client that is not listening at all (an older version, or - // one that has already exited) is answered with a plain failure and the caller falls - // back to WM_CLOSE. - for (int attempt = 0; attempt < 2; ++attempt) { - const HANDLE pipe = CreateFileW(path.c_str(), GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr); - if (pipe == INVALID_HANDLE_VALUE) { - if (GetLastError() == ERROR_PIPE_BUSY && WaitNamedPipeW(path.c_str(), PIPE_WAIT_MILLISECONDS) != FALSE) { - continue; - } - return false; - } - DWORD written = 0; - const BOOL ok = - WriteFile(pipe, INSTALLER_CLOSE_COMMAND, sizeof(INSTALLER_CLOSE_COMMAND) - 1, &written, nullptr); - CloseHandle(pipe); - // The request is in the pipe buffer, so the client reads it even though this handle - // is closed immediately. No acknowledgment is waited for: the caller only needs to - // know whether there was a listening instance to ask, and a hung client must not - // stall the guard. - return ok != FALSE && written == sizeof(INSTALLER_CLOSE_COMMAND) - 1; - } - return false; -} diff --git a/tools/installer-holder-probe/installer_close_request.h b/tools/installer-holder-probe/installer_close_request.h deleted file mode 100644 index f30c49b2c..000000000 --- a/tools/installer-holder-probe/installer_close_request.h +++ /dev/null @@ -1,21 +0,0 @@ -#ifndef INSTALLER_CLOSE_REQUEST_H -#define INSTALLER_CLOSE_REQUEST_H - -/** - * @brief Asks one running payload instance to close over its installer socket. - * - * A 3.1.3 client (and later) listens on a local socket named by - * installerCloseSocketName(). Connecting to it and writing one line is the same request as - * the window's close button, but it reaches an instance whose window is hidden in the - * notification area or is sitting on its own confirmation - the two states WM_CLOSE cannot - * get through. Older clients have no socket, so the caller still falls back to WM_CLOSE. - * - * Best effort: a failed connect or write is not an error. The caller has the window close - * and the guard's force-close answer left. - * - * @param processId the process to ask, as the Restart Manager named it - * @return true when the request was written to a listening instance - */ -bool requestInstallerClose(unsigned long processId); - -#endif // INSTALLER_CLOSE_REQUEST_H diff --git a/tools/installer-holder-probe/main.cpp b/tools/installer-holder-probe/main.cpp index 31901a0ec..3b5bb5d22 100644 --- a/tools/installer-holder-probe/main.cpp +++ b/tools/installer-holder-probe/main.cpp @@ -1,11 +1,6 @@ -#include "close_holders.h" #include "holder_line.h" -#include "payload_locks.h" -#include "payload_paths.h" #include "restart_manager_holders.h" -#include -#include #include #include #include @@ -17,88 +12,16 @@ namespace /// trailing backslash then never has to survive NSIS quoting or a command line. constexpr wchar_t INSTALL_DIR_VARIABLE[] = L"COCKATRICE_GUARD_DIR"; -/// The installer sets this to the process it waits on (/PID=), so the launcher mode reads -/// the same process id the wait does instead of one that has to survive NSIS quoting. -constexpr wchar_t LAUNCHER_PID_VARIABLE[] = L"COCKATRICE_LAUNCHER_PID"; - -/// How long the close mode waits for the instance it asked to exit before it reports the -/// process as still running. The installer measures the purge right after this call, so -/// the wait belongs here: it is the only place that holds a handle on the process whose -/// exit it is waiting for. -constexpr unsigned long CLOSE_WAIT_MILLISECONDS = 30000; - -/// How long the end mode waits for what it terminated to be gone. TerminateProcess is -/// asynchronous, and the installer measures the purge right after this returns, so a process -/// that is still tearing down would otherwise be reported as a holder for one more round. -constexpr unsigned long END_WAIT_MILLISECONDS = 5000; - /// Exit codes the installer reads. "Something was found" is deliberately not 0, so an /// answer of "nothing holds the files" stays distinguishable from a probe that failed. -/// In the close mode 1 means a close was actually sent and 0 that every holder was left -/// to the user, which is what the report line next to the code spells out. 3 is for an -/// answer that could not name what it found: a lookup that failed and named nobody, or the -/// lookups naming nobody while a payload program refuses to be replaced, which is proof one -/// runs from the directory. 4 is only the launcher mode: the process the installer waits on -/// has no window a user could answer, which is the one answer the installer acts on by -/// itself instead of asking the user. enum ExitCode { EXIT_NO_HOLDERS = 0, EXIT_HOLDERS_FOUND = 1, EXIT_NOT_CONFIGURED = 2, EXIT_PROBE_FAILED = 3, - EXIT_NO_WINDOW = 4, }; -/// The probe has four modes, selected by the first argument: -/// -/// (none) name what holds the payload, one line, for the guard prompt and the -/// abort dialog/log -/// --close-holders bring the running Cockatrice forward, ask it to close, and wait for -/// it to exit, for the offer to close after a failed purge -/// --end-holders end the running Cockatrice, Oracle and Servatrice and wait for them to -/// be gone, for the last resort the guard prompt offers when a close did -/// not free the files -/// --close-launcher ask the one process the installer waits on (/PID=) to close and say -/// whether there was a window to ask at all, because an instance with -/// no window has answered its own close and is stuck in shutdown -/// -/// The guard's three modes answer from three questions that cover each other's blind spots: -/// the Restart Manager names whoever has a payload file open, every payload program running -/// from the directory itself is named as well, because a running instance holds its own image -/// and the modules it loaded as mappings rather than open files and the Restart Manager does -/// not always report those, and a payload executable that refuses to be written is running -/// even when neither lookup could name it (see payloadExecutablesInUse). No one of them is -/// the guard's authority: an instance the process lookups do not name, while its executable -/// is locked, is exactly the case that left a half-replaced install behind, so it is reported -/// as a check that could not name what it found instead of as a free payload. -/// A program in a folder below the directory is not named by the second lookup: it is not an -/// instance of this payload, and the guard could neither close nor end it. -enum class Mode -{ - Name, ///< name the holders - Close, ///< ask the holders to close - End, ///< end the holders - CloseLauncher, ///< ask the one process the installer waits on to close -}; - -Mode requestedMode(int argumentCount, char **arguments) -{ - if (argumentCount > 1) { - const std::string mode(arguments[1]); - if (mode == "--close-holders") { - return Mode::Close; - } - if (mode == "--end-holders") { - return Mode::End; - } - if (mode == "--close-launcher") { - return Mode::CloseLauncher; - } - } - return Mode::Name; -} - /// Writes one line to the pipe nsExec reads. Narrowed to ASCII on purpose: the bytes /// come back to the installer as an ANSI string, so a non-ASCII process name would be /// mojibake in the log; a '?' at least stays honest. Control characters are dropped @@ -115,29 +38,11 @@ void writeLine(const std::wstring &text) WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), narrow.data(), static_cast(narrow.size()), &written, nullptr); } -/// The line for a payload executable that is in use while no lookup could name the program -/// holding it: the answer the guard shows, so it names the file the user has to free and -/// says that the program doing it is still to be closed by hand. -std::wstring inUseLine(const std::vector &files) -{ - std::wstring names; - for (const std::wstring &file : files) { - if (!names.empty()) { - names += L", "; - } - names += fileNameOf(file); - } - if (files.size() == 1) { - return names + L" is in use and the program holding it could not be identified"; - } - return names + L" are in use and the programs holding them could not be identified"; -} - -std::wstring environmentVariable(const wchar_t *name) +std::wstring installDirFromEnvironment() { for (DWORD size = MAX_PATH; size <= 32768U; size *= 2) { std::vector buffer(size); - const DWORD length = GetEnvironmentVariableW(name, buffer.data(), size); + const DWORD length = GetEnvironmentVariableW(INSTALL_DIR_VARIABLE, buffer.data(), size); if (length == 0) { return {}; } @@ -147,71 +52,9 @@ std::wstring environmentVariable(const wchar_t *name) } return {}; } - -std::wstring installDirFromEnvironment() -{ - return environmentVariable(INSTALL_DIR_VARIABLE); -} - -/// The process id the installer waits on, or false when it was not passed or is not a -/// number. A pid of 0 is refused because the installer never passes one. -bool launcherProcessIdFromEnvironment(unsigned long &processId) -{ - const std::wstring text = environmentVariable(LAUNCHER_PID_VARIABLE); - if (text.empty()) { - return false; - } - wchar_t *end = nullptr; - const unsigned long parsed = std::wcstoul(text.c_str(), &end, 10); - if (parsed == 0 || end == nullptr || *end != L'\0') { - return false; - } - processId = parsed; - return true; -} - -/// Asks the one process the installer waits on to close and answers what it could do. -/// -/// The guard closes a set and reports every holder; here there is exactly one process, and -/// the installer needs an answer it can act on rather than a sentence: a launcher with no -/// visible window has already answered its own close and is stuck in shutdown, so no answer -/// the installer offers would ever reach it. A window that was asked - including one the -/// instance is showing a question in - is still the user's decision. -int closeLauncher(const std::wstring &installDir) -{ - unsigned long processId = 0; - if (!launcherProcessIdFromEnvironment(processId)) { - writeLine(L"the probe was not given the launcher process id"); - return EXIT_NOT_CONFIGURED; - } - - // Only an instance that still runs from the directory being replaced is asked: a bare - // process id names anything once it is reused, while a process running from the - // directory is the one the installer started this update from. - for (const Holder &holder : findProcessesRunningFrom(installDir)) { - if (holder.processId != processId) { - continue; - } - if (!isCockatriceImage(holder.imageName)) { - writeLine(L"the launcher is not cockatrice.exe, left to you"); - return EXIT_PROBE_FAILED; - } - const CloseHoldersResult result = closeCockatriceHolders({holder}, CLOSE_WAIT_MILLISECONDS, false); - writeLine(result.report); - if (result.askedToClose > 0) { - return EXIT_HOLDERS_FOUND; - } - return result.withoutWindow > 0 ? EXIT_NO_WINDOW : EXIT_HOLDERS_FOUND; - } - // Not an answer: the lookup may have failed or refused the image, and the installer - // holds a live handle on that process, so "not found" must not read as "gone". The - // installer shows its dialog for this code instead of acting on it. - writeLine(L"the launcher is not running from the directory being replaced, left to you"); - return EXIT_PROBE_FAILED; -} } // namespace -int main(int argumentCount, char **arguments) +int main() { const std::wstring installDir = installDirFromEnvironment(); if (installDir.empty()) { @@ -219,82 +62,17 @@ int main(int argumentCount, char **arguments) return EXIT_NOT_CONFIGURED; } - const Mode mode = requestedMode(argumentCount, arguments); - // The launcher mode answers about exactly one process, so the holder set below is what it - // does not need - and the Restart Manager call behind it is the slowest thing this tool - // does, which must not delay a wait that started with a ten-second grace. - if (mode == Mode::CloseLauncher) { - return closeLauncher(installDir); + std::vector holders; + const unsigned long error = findProcessesUsingFiles(collectProbeFiles(installDir), holders); + if (error != ERROR_SUCCESS) { + writeLine(L"the Restart Manager lookup failed with win32 error " + std::to_wstring(error)); + return EXIT_PROBE_FAILED; } - - // The payload programs running from the directory first: they are the instance the user - // is asked to close, and the Restart Manager may not name any of them (see - // findProcessesRunningFrom). The file holders follow, deduplicated by process, so an - // instance that both runs from the directory and holds a payload file is named once. - // Neither lookup may name the probe or the process that started it, whichever one found - // it: an installer or uninstaller running from the directory it is replacing would - // otherwise be named as a holder, and closing it is a request nobody can answer. - std::vector holders = findProcessesRunningFrom(installDir); - const std::vector asking = askingProcessIds(); - std::vector fileHolders; - const unsigned long error = findProcessesUsingFiles(collectPayloadFiles(installDir), fileHolders); - for (const Holder &holder : fileHolders) { - if (std::find(asking.begin(), asking.end(), holder.processId) != asking.end()) { - continue; - } - const bool alreadyNamed = std::any_of(holders.begin(), holders.end(), [&holder](const Holder &named) { - return named.processId == holder.processId; - }); - if (!alreadyNamed) { - holders.push_back(holder); - } - } - - // Nobody was named. That is not yet the answer that nothing holds the payload: a lookup - // that failed only makes the probe unable to answer when it also named nobody, and a - // payload executable that refuses to be replaced is running even when neither lookup - // could name it. if (holders.empty()) { - // Neither process lookup is proof that the payload is free: the Restart Manager does - // not report every mapping, and a program running under another account cannot be - // read from here, so "nobody was named" is not the answer "nothing is running". The - // file cannot be missed - a running image refuses the write the replacement needs - - // and an answer of "nothing is holding them" over that locked file is what sent - // updates into an uninstall that could only fail halfway. Reported as a check that - // could not name what it found, so the guard asks the user to close it and retry - // instead of replacing a payload that is still running. - const std::vector inUse = payloadExecutablesInUse(installDir); - if (!inUse.empty()) { - writeLine(inUseLine(inUse)); - return EXIT_PROBE_FAILED; - } - - if (error != ERROR_SUCCESS) { - writeLine(L"the Restart Manager lookup failed with win32 error " + std::to_wstring(error)); - return EXIT_PROBE_FAILED; - } - - // No output in the naming mode: the installer words the "nothing is holding them" - // case, and it owns the message either way. The acting modes say it themselves, - // because their line is logged verbatim next to what they were asked to do. - if (mode != Mode::Name) { - writeLine(L"nothing is holding the files"); - } + // No output: the installer words the "nothing is holding them" case, and it + // owns the message either way. return EXIT_NO_HOLDERS; } - if (mode == Mode::Name) { - writeLine(formatHolders(holders)); - return EXIT_HOLDERS_FOUND; - } - if (mode == Mode::End) { - const EndHoldersResult result = endHolders(holders, END_WAIT_MILLISECONDS); - writeLine(result.report); - return result.ended > 0 ? EXIT_HOLDERS_FOUND : EXIT_NO_HOLDERS; - } - - // Hidden windows are asked to close here: an instance in the notification area is the one - // this update has no other way to reach, and the guard's end answer is behind that attempt. - const CloseHoldersResult result = closeCockatriceHolders(holders, CLOSE_WAIT_MILLISECONDS, true); - writeLine(result.report); - return result.askedToClose > 0 ? EXIT_HOLDERS_FOUND : EXIT_NO_HOLDERS; + writeLine(formatHolders(holders)); + return EXIT_HOLDERS_FOUND; } diff --git a/tools/installer-holder-probe/payload_locks.cpp b/tools/installer-holder-probe/payload_locks.cpp deleted file mode 100644 index 83b77ac58..000000000 --- a/tools/installer-holder-probe/payload_locks.cpp +++ /dev/null @@ -1,67 +0,0 @@ -#include "payload_locks.h" - -#include "payload_paths.h" - -#include - -namespace -{ -/// How long the second look waits before it re-tests the files that refused the write open. -/// Only those are re-tested, so the one answer costs this at most once: a scanner, or a -/// program that is only starting, holds a file without write sharing for a moment, and that -/// must not turn a free payload into a run the user has to answer. -constexpr DWORD RECHECK_DELAY_MILLISECONDS = 300; - -/// Whether @p path refuses an open that asks to write it. -/// -/// Windows opens a program's image, and every module loaded into it, with read and delete -/// sharing but not write sharing, so this is refused for as long as the program runs and by -/// nothing else that merely reads the file. A read-only file is skipped: it refuses a write -/// open for its own reason and would answer yes for a payload nobody is running, and the -/// purge clears that attribute before it deletes anything. Any other error is no answer at -/// all - access this probe does not have says nothing about whether the file is in use, and -/// still answering "in use" would stop installs that could have finished. -bool refusesWrite(const std::wstring &path) -{ - const DWORD attributes = GetFileAttributesW(path.c_str()); - if (attributes == INVALID_FILE_ATTRIBUTES || - (attributes & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_READONLY)) != 0) { - return false; - } - const HANDLE file = CreateFileW(path.c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, - nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); - if (file == INVALID_HANDLE_VALUE) { - return GetLastError() == ERROR_SHARING_VIOLATION; - } - // Nothing was written: the open is the whole test, and closing the handle leaves the - // file exactly as it was. - CloseHandle(file); - return false; -} -} // namespace - -std::vector payloadExecutablesInUse(const std::wstring &installDir) -{ - std::vector refused; - for (const wchar_t *name : PAYLOAD_PROGRAMS) { - const std::wstring path = installDir + L"\\" + name; - if (refusesWrite(path)) { - refused.push_back(path); - } - } - if (refused.empty()) { - return refused; - } - - // One re-test of the files that refused, and only those: the answer is shown to a user - // who then has to close a program and run the installer again, so it has to be the - // settled state rather than a moment in which something happened to hold the file. - Sleep(RECHECK_DELAY_MILLISECONDS); - std::vector settled; - for (const std::wstring &path : refused) { - if (refusesWrite(path)) { - settled.push_back(path); - } - } - return settled; -} diff --git a/tools/installer-holder-probe/payload_locks.h b/tools/installer-holder-probe/payload_locks.h deleted file mode 100644 index 1d23648a2..000000000 --- a/tools/installer-holder-probe/payload_locks.h +++ /dev/null @@ -1,36 +0,0 @@ -#ifndef PAYLOAD_LOCKS_H -#define PAYLOAD_LOCKS_H - -#include -#include - -/** - * @brief The payload executables in @p installDir that refuse to be written, as full paths: - * the programs that are running from it right now. - * - * The guard asks two questions and both can come back empty for a running instance: the - * Restart Manager does not reliably report a program's own image or the modules it mapped, - * and the image path of a process running under another account cannot be read from here, - * so the process is not named either. The file itself is the evidence neither question - * depends on: a program's image is opened without write sharing for as long as it runs, so - * the write access replacing it needs is refused for exactly that long, while a file that is - * merely being read still accepts it. - * - * An installer that answers "nothing is holding the payload" from two lookups that both - * missed is what sent updates into an uninstall that could only fail halfway, leaving the - * previous version half removed. This answer is what keeps that from being said: a payload - * executable that refuses the write is running, whether or not this process may name it. - * - * Only the programs the payload installs are tested, not every file the purge removes. A - * payload executable refusing the write proves that it is running; a mapped module belongs - * to a running executable, which is tested. A DLL or qt.conf on its own can be held by the - * program that is asking - the uninstaller runs in place from the directory it empties - and - * naming that as a holder is the loop the guard already had. - * - * @return one full path per executable that still refused after the re-test, in the order - * PAYLOAD_PROGRAMS names them; empty when every one of them can be written, or none - * is installed. - */ -std::vector payloadExecutablesInUse(const std::wstring &installDir); - -#endif diff --git a/tools/installer-holder-probe/payload_paths.cpp b/tools/installer-holder-probe/payload_paths.cpp deleted file mode 100644 index 8d6274e91..000000000 --- a/tools/installer-holder-probe/payload_paths.cpp +++ /dev/null @@ -1,48 +0,0 @@ -#include "payload_paths.h" - -#include -#include -#include - -namespace -{ -/// Compares two file names the way the file system does, without a locale. -/// -/// Only the names above are ever compared against, and all of them are ASCII, so folding -/// the ASCII letters is the whole answer. The Windows ordinal comparison is not used here -/// on purpose: this file is compiled into a test that runs everywhere, and the question -/// "is this image a program the payload installs?" must be answerable without Windows. -bool equalsIgnoringAsciiCase(const std::wstring &left, const std::wstring &right) -{ - if (left.size() != right.size()) { - return false; - } - return std::equal(left.begin(), left.end(), right.begin(), [](wchar_t a, wchar_t b) { - const wchar_t foldedA = a >= L'A' && a <= L'Z' ? static_cast(a - L'A' + L'a') : a; - const wchar_t foldedB = b >= L'A' && b <= L'Z' ? static_cast(b - L'A' + L'a') : b; - return foldedA == foldedB; - }); -} -} // namespace - -std::wstring fileNameOf(const std::wstring &path) -{ - const std::size_t separator = path.find_last_of(L"\\/"); - return separator == std::wstring::npos ? path : path.substr(separator + 1); -} - -std::wstring parentDirectoryOf(const std::wstring &path) -{ - const std::size_t separator = path.find_last_of(L"\\/"); - return separator == std::wstring::npos ? std::wstring() : path.substr(0, separator); -} - -bool isPayloadProgram(const std::wstring &imageName) -{ - for (const wchar_t *program : PAYLOAD_PROGRAMS) { - if (equalsIgnoringAsciiCase(imageName, program)) { - return true; - } - } - return false; -} diff --git a/tools/installer-holder-probe/payload_paths.h b/tools/installer-holder-probe/payload_paths.h deleted file mode 100644 index ea63732f5..000000000 --- a/tools/installer-holder-probe/payload_paths.h +++ /dev/null @@ -1,51 +0,0 @@ -#ifndef PAYLOAD_PATHS_H -#define PAYLOAD_PATHS_H - -#include - -/** - * @brief The file names of the programs the payload installs, in the order the guard names - * them. - * - * The single list behind isPayloadProgram(), so the guard acts on exactly the programs it - * names, and so a payload program added later is closed, ended and tested for a lock - * without a second list to update. A payload executable that refuses to be written is - * running, which is what makes this list the evidence the two process lookups cannot - * provide - see payloadExecutablesInUse(). - */ -inline constexpr const wchar_t *PAYLOAD_PROGRAMS[] = {L"cockatrice.exe", L"oracle.exe", L"servatrice.exe"}; - -/** - * @brief The last name in @p path: what follows its last separator, or @p path itself when - * it has none. - * - * Empty for an empty path and for one that ends in a separator, which has no name of its - * own - a caller that hands in a directory there gets no name rather than the directory's - * own last component. - */ -std::wstring fileNameOf(const std::wstring &path); - -/** - * @brief The directory @p path lies in, without a trailing separator. - * - * Empty when @p path has no directory part. Used to tell where a process was started from: - * an image running directly in the install directory has that directory as its parent, - * while one started from a folder below it has that folder, and the two are not the same - * answer to "does this process belong to the installation being replaced?". - */ -std::wstring parentDirectoryOf(const std::wstring &path); - -/** - * @brief Whether @p imageName is one of the programs the payload installs. - * - * The single place that answers which images are Cockatrice's own, so the guard acts on the - * same set it names, and so a program a user happens to keep in the install directory is - * never treated as an instance of the application. Matched on the image name alone, without - * case: the names are the payload's own, and the comparison is deliberately free of Windows - * API so it can be exercised off Windows. - * - * @param imageName the file name of a process image, empty when it could not be read - */ -bool isPayloadProgram(const std::wstring &imageName); - -#endif diff --git a/tools/installer-holder-probe/restart_manager_holders.cpp b/tools/installer-holder-probe/restart_manager_holders.cpp index 21ddd6988..39f6a0049 100644 --- a/tools/installer-holder-probe/restart_manager_holders.cpp +++ b/tools/installer-holder-probe/restart_manager_holders.cpp @@ -5,12 +5,10 @@ // here is a requirement; Regroup would sort them the other way round. #include #include -#include // clang-format on #include #include -#include namespace { @@ -22,11 +20,6 @@ constexpr std::size_t MAX_PROBED_FILES = 2000; /// How deep the Plugins walk goes. Its real shape is Plugins\\.dll. constexpr int MAX_PLUGIN_DEPTH = 4; -/// How many parents askingProcessIds() walks up before it stops on its own. A real chain -/// is the probe, the installer or uninstaller, and the shell that started it; the bound -/// only keeps a snapshot with a cycle in it from walking forever. -constexpr int MAX_ASKER_LEVELS = 8; - void collectFilesOf(const std::wstring &directory, int depth, int maxDepth, std::vector &files) { if (files.size() >= MAX_PROBED_FILES) { @@ -77,30 +70,10 @@ std::wstring imagePathOf(unsigned long processId) return imagePath; } -/// @p directory with exactly one trailing separator, so that two spellings of the same -/// directory compare equal and a sibling whose name merely begins with the same letters -/// ("Cockatrice-old" next to "Cockatrice") cannot pass for it. -std::wstring directoryPrefixOf(const std::wstring &directory) +std::wstring fileNameOf(const std::wstring &path) { - std::wstring prefix = directory; - while (!prefix.empty() && (prefix.back() == L'\\' || prefix.back() == L'/')) { - prefix.pop_back(); - } - prefix += L'\\'; - return prefix; -} - -/// Whether @p left and @p right name the same directory, compared the way the file system -/// does: a difference in case is not a difference in directory, and a trailing separator -/// changes nothing. -bool isSameDirectory(const std::wstring &left, const std::wstring &right) -{ - if (left.empty() || right.empty()) { - return false; - } - const std::wstring leftPrefix = directoryPrefixOf(left); - const std::wstring rightPrefix = directoryPrefixOf(right); - return CompareStringOrdinal(leftPrefix.c_str(), -1, rightPrefix.c_str(), -1, TRUE) == CSTR_EQUAL; + const std::size_t separator = path.find_last_of(L"\\/"); + return separator == std::wstring::npos ? path : path.substr(separator + 1); } /// Copies a fixed-size, NUL-terminated WCHAR array the API filled in. @@ -123,44 +96,13 @@ Holder holderOf(const RM_PROCESS_INFO &info) } } // namespace -std::vector collectPayloadFiles(const std::wstring &installDir) +std::vector collectProbeFiles(const std::wstring &installDir) { std::vector files; - // The programs the payload installs, by name, plus the configuration file next to - // them: what the purge deletes by name, and therefore what an instance can have - // mapped. - std::vector replaced(std::begin(PAYLOAD_PROGRAMS), std::end(PAYLOAD_PROGRAMS)); - replaced.emplace_back(L"qt.conf"); - for (const std::wstring &name : replaced) { - const std::wstring path = installDir + L"\\" + name; - const DWORD attributes = GetFileAttributesW(path.c_str()); - if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) == 0) { - files.push_back(path); - } - } - - // Every top-level DLL: the set the purge deletes and therefore the set a running - // instance can have mapped. Matched by mask rather than by a list of expected names, - // so a transitive dependency nobody thought of is covered as well. - WIN32_FIND_DATAW entry{}; - const HANDLE dlls = FindFirstFileW((installDir + L"\\*.dll").c_str(), &entry); - if (dlls != INVALID_HANDLE_VALUE) { - do { - if (files.size() >= MAX_PROBED_FILES) { - break; - } - if ((entry.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0 || - (entry.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { - continue; - } - files.push_back(installDir + L"\\" + entry.cFileName); - } while (FindNextFileW(dlls, &entry) != 0); - FindClose(dlls); - } - - // Plugins is the other tree the purge removes. Its data directory is not: it holds - // downloaded pictures, which the purge does not touch and which would turn this into - // a walk over thousands of files. + // depth 0, maxDepth 0: the install root's files and nothing below it. Its data + // directory holds downloaded pictures, which the purge does not touch and which + // would turn this into a walk over thousands of files. + collectFilesOf(installDir, 0, 0, files); if (files.size() < MAX_PROBED_FILES) { collectFilesOf(installDir + L"\\Plugins", 0, MAX_PLUGIN_DEPTH, files); } @@ -216,90 +158,3 @@ unsigned long findProcessesUsingFiles(const std::vector &files, st RmEndSession(session); return error; } - -std::vector askingProcessIds() -{ - std::vector asking; - asking.push_back(GetCurrentProcessId()); - - // One snapshot for the whole walk: the parent ids come from it, so a level costs no - // second lookup. - std::vector processes; - const HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); - if (snapshot == INVALID_HANDLE_VALUE) { - return asking; - } - PROCESSENTRY32W entry{}; - entry.dwSize = sizeof(entry); - if (Process32FirstW(snapshot, &entry) != FALSE) { - do { - processes.push_back(entry); - } while (Process32NextW(snapshot, &entry) != FALSE); - } - CloseHandle(snapshot); - - unsigned long current = asking.back(); - for (int level = 0; level < MAX_ASKER_LEVELS; ++level) { - const auto named = std::find_if(processes.begin(), processes.end(), [current](const PROCESSENTRY32W &process) { - return process.th32ProcessID == current; - }); - if (named == processes.end() || named->th32ParentProcessID == 0) { - break; - } - const unsigned long parent = named->th32ParentProcessID; - if (isPayloadProgram(fileNameOf(imagePathOf(parent)))) { - break; - } - asking.push_back(parent); - current = parent; - } - return asking; -} - -std::vector findProcessesRunningFrom(const std::wstring &installDir) -{ - std::vector holders; - if (installDir.empty()) { - return holders; - } - - // Never the probe, and never the installer or uninstaller that started it: either can - // run from the directory being replaced, and naming one asks the user to close the - // program that is asking (see askingProcessIds). - const std::vector asking = askingProcessIds(); - - const HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); - if (snapshot == INVALID_HANDLE_VALUE) { - return holders; - } - PROCESSENTRY32W entry{}; - entry.dwSize = sizeof(entry); - if (Process32FirstW(snapshot, &entry) != FALSE) { - do { - if (std::find(asking.begin(), asking.end(), entry.th32ProcessID) != asking.end()) { - continue; - } - // One process at a time, because the path is per process; a process that - // refuses to be read is not named here and is still named by Restart - // Manager when it holds a handle. The idle process has no image at all. - const std::wstring imagePath = imagePathOf(entry.th32ProcessID); - const std::wstring imageName = fileNameOf(imagePath); - // Only the programs the payload installs, and only those started from the - // directory itself: a folder a user installs into can hold other applications, - // each in its own subfolder, and one of those is not an instance of this - // payload. Naming it would ask for a close the probe cannot send and an end it - // would refuse to perform, so the guard could only ever abort. A program below - // the directory that really does hold a payload file is named by - // findProcessesUsingFiles, which is the lookup for that question. - if (!isPayloadProgram(imageName) || !isSameDirectory(parentDirectoryOf(imagePath), installDir)) { - continue; - } - Holder holder; - holder.processId = entry.th32ProcessID; - holder.imageName = imageName; - holders.push_back(std::move(holder)); - } while (Process32NextW(snapshot, &entry) != FALSE); - } - CloseHandle(snapshot); - return holders; -} diff --git a/tools/installer-holder-probe/restart_manager_holders.h b/tools/installer-holder-probe/restart_manager_holders.h index e966b70b8..d69fc0259 100644 --- a/tools/installer-holder-probe/restart_manager_holders.h +++ b/tools/installer-holder-probe/restart_manager_holders.h @@ -2,25 +2,22 @@ #define RESTART_MANAGER_HOLDERS_H #include "holder_line.h" -#include "payload_paths.h" #include #include /** - * @brief Lists the payload files, as full paths. + * @brief Lists the files a failed purge can be holding up, as full paths. * - * The set is what the installer replaces and the purge removes: the three executables, - * qt.conf, every top-level DLL 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, - * so it cannot be mistaken for a running instance and block an upgrade. + * Everything directly in @p installDir plus everything below its Plugins directory: + * those are the two things the purge removes, so they are the two sets of files a + * holder can be holding. Restart Manager refuses a directory with ERROR_ACCESS_DENIED, + * which is why the probe registers files and this has to name them first. * - * Restart Manager refuses a directory with ERROR_ACCESS_DENIED, which is why the probe - * registers files and this has to name them first. Missing files and unreadable - * directories are skipped rather than reported, and the list is capped: this runs before - * the first page and on the abort path, and must not walk a tree of downloaded pictures. + * Missing or unreadable directories are skipped rather than reported, and the list is + * capped: this runs on the abort path and must not walk a tree of downloaded pictures. */ -std::vector collectPayloadFiles(const std::wstring &installDir); +std::vector collectProbeFiles(const std::wstring &installDir); /** * @brief Names the applications and services that have one of @p files open or mapped. @@ -35,54 +32,4 @@ std::vector collectPayloadFiles(const std::wstring &installDir); */ unsigned long findProcessesUsingFiles(const std::vector &files, std::vector &holders); -/** - * @brief Lists every payload program whose image lies in @p installDir itself. - * - * Restart Manager answers the question "who has this file open", and a running instance - * does not necessarily have one open: its own image and the modules it loaded are mapped - * into it, not opened by it, and RmGetList does not reliably report a mapping. 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 - so an answer built on file handles alone cannot be the guard's authority. - * - * Only the programs the payload installs are named, and only when the image lies directly - * in @p installDir. A folder a user installs into can hold other applications, each in its - * own subfolder, and those are not instances of this one: they hold none of the payload, - * neither closing nor ending them would free a file the purge removes, and naming them - * leaves the guard with an answer that can never be carried out. A program below the - * directory that does hold a payload file is still named, by findProcessesUsingFiles. - * - * Reading the image path needs PROCESS_QUERY_LIMITED_INFORMATION and - * QueryFullProcessImageNameW, neither of which depends on the bitness of either process, - * so a 64-bit instance is named from this 32-bit stub. The comparison is on the directory, - * not on an image name alone, so a program that merely shares a name with a payload program - * is never named. - * - * @param installDir the directory being replaced, as a full path. - * @return one entry per payload program running from it, in snapshot order, without the - * processes named by askingProcessIds(); empty when the directory is empty or no - * payload program runs from it. - */ -std::vector findProcessesRunningFrom(const std::wstring &installDir); - -/** - * @brief The processes that must never be named as holders: the probe and the ones that - * started it. - * - * The installer or uninstaller doing the asking can legitimately run from the directory it - * is replacing - the old uninstaller is started with _?= so it stays in $INSTDIR, and an - * installer can be downloaded into the folder - and naming it asks the user to close the - * program that is asking. That cannot be answered: the close skips it, the end offer skips - * it, and the guard loops until the user cancels, which is what a run that named itself - * did. - * - * The walk up the parent chain stops at the first program the payload installs, because in - * an in-app update the installer's own parent is the Cockatrice instance being replaced - - * the one the guard exists to name. - * - * @return process ids, the probe first, then its ancestors while they are not payload - * programs; only the probe itself when the snapshot fails. - */ -std::vector askingProcessIds(); - #endif diff --git a/vcpkg b/vcpkg index 9eec002df..9e593bb18 160000 --- a/vcpkg +++ b/vcpkg @@ -1 +1 @@ -Subproject commit 9eec002df1cd109f8f104cd966a763daf9751489 +Subproject commit 9e593bb18ea69cc5095e012465dcd675a822ed0d diff --git a/vcpkg.json b/vcpkg.json index 024293d34..3476cf9d2 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json", - "builtin-baseline": "9eec002df1cd109f8f104cd966a763daf9751489", + "builtin-baseline": "9e593bb18ea69cc5095e012465dcd675a822ed0d", "dependencies": [ "gtest", "liblzma",