diff --git a/.ci/compile.sh b/.ci/compile.sh index 9be8ba741..63c027473 100755 --- a/.ci/compile.sh +++ b/.ci/compile.sh @@ -311,24 +311,16 @@ if [[ $RUNNER_OS == macOS ]]; then if [[ $MAKE_PACKAGE ]]; then # Workaround https://github.com/actions/runner-images/issues/7522 - # have hdiutil repeat the command 10 times in hope of success. - # The wrapper has to exit non-zero once it gives up: CPack reads that status to - # decide whether the disk image was created, so a zero exit turns a failed hdiutil - # into a much later, misleading "Problem copying the package" error that hides - # hdiutil's own message. + # have hdiutil repeat the command 10 times in hope of success hdiutil_script="/tmp/hdiutil.sh" # shellcheck disable=SC2016 echo '#!/bin/bash - attempts=10 i=0 - while true; do - hdiutil "$@" && exit 0 - rc=$? - if (( ++i >= attempts )); then - echo "::error::hdiutil $1 failed $i times (last status $rc), giving up" >&2 - exit "$rc" + while ! hdiutil "$@"; do + if (( ++i >= 10 )); then + echo "Error: hdiutil failed $i times!" >&2 + break fi - echo "hdiutil $1 attempt $i/$attempts failed (status $rc), retrying" >&2 sleep 1 done' >"$hdiutil_script" chmod +x "$hdiutil_script" @@ -467,206 +459,6 @@ if [[ $MAKE_COVERAGE ]]; then echo "::endgroup::" fi -if [[ $RUNNER_OS == Windows && ! $MAKE_NO_CLIENT ]]; then - echo "::group::Probe the installer's holder lookup" - # The working directory is still the build dir here: the package steps below are what - # leave and re-enter it, and that is what made a $BUILD_DIR-prefixed path name - # build/build. Both paths are relative to the dir the probe was built into. - probe="./installer-holder-probe.exe" - if [[ ! -f $probe ]]; then - echo "::error file=$0::installer-holder-probe.exe was not built" - exit 1 - fi - # The probe exists to name a holder that a process-and-module scan cannot see: one - # that has a file open without having loaded it. Holding a temporary DLL with - # FileShare.None is exactly that case, so this asserts the reason the probe was - # added instead of only that the Restart Manager answers at all. It needs no NSIS - # and no packaging, so it runs on every Windows build that gets this far. - # The holder is a process of its own: the probe never names the process that started it, - # which is what keeps an in-place uninstaller from being named as a holder of the - # directory it is emptying. The same DLL held by the script that starts the probe - # therefore has to come back as nothing, and that is the second case below. - probe_check="./holder-probe-check.ps1" - cat >"$probe_check" <<'POWERSHELL' -$ErrorActionPreference = "Stop" -$dir = Join-Path ([IO.Path]::GetTempPath()) "cockatrice-holder-probe-$PID" -Remove-Item -Recurse -Force $dir -ErrorAction SilentlyContinue -New-Item -ItemType Directory -Force $dir | Out-Null -$probe = (Resolve-Path -LiteralPath $env:PROBE_EXE).Path - -function Invoke-Probe([string] $guardDir) { - $start = New-Object System.Diagnostics.ProcessStartInfo - $start.FileName = $probe - $start.UseShellExecute = $false - $start.RedirectStandardOutput = $true - $start.EnvironmentVariables["COCKATRICE_GUARD_DIR"] = $guardDir - $process = [System.Diagnostics.Process]::Start($start) - $read = $process.StandardOutput.ReadToEndAsync() - if (-not $process.WaitForExit(20000)) { - try { $process.Kill() } catch { } - throw "the holder probe did not finish within 20 s for $guardDir" - } - $output = $read.Result - $code = $process.ExitCode - $process.Dispose() - return @{ Code = $code; Output = $output } -} - -function Wait-UntilLocked([string] $path) { - $deadline = (Get-Date).AddSeconds(15) - while ((Get-Date) -lt $deadline) { - try { - $open = [IO.File]::Open($path, "Open", "ReadWrite", "None") - $open.Dispose() - } catch { - return - } - Start-Sleep -Milliseconds 100 - } - throw "the holder never locked $path" -} - -try { - $file = Join-Path $dir "locked.dll" - [IO.File]::WriteAllText($file, "x") - - # The holder is a process of its own, not this script: the probe never names the process - # that started it (the case below asserts that), so a file held here would be left out on - # purpose and the check would pass for the wrong reason. - $holderScript = Join-Path $dir "hold.ps1" - Set-Content -LiteralPath $holderScript -Value @' -param([string] $Path) -$handle = [IO.File]::Open($Path, "Open", "ReadWrite", "None") -Start-Sleep -Seconds 120 -'@ - $powershell = Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0\powershell.exe" - $holder = Start-Process -FilePath $powershell -PassThru -ArgumentList @( - "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "`"$holderScript`"", "-Path", "`"$file`"") - try { - Wait-UntilLocked $file - $locked = Invoke-Probe $dir - if ($locked.Code -ne 1) { throw "expected exit 1 for a locked file, got $($locked.Code) : $($locked.Output)" } - if ($locked.Output -notmatch "pid\s+$($holder.Id)\b") { - throw "the lookup did not name the process holding the file: $($locked.Output)" - } - } finally { - try { $holder.Kill() } catch { } - try { $holder.WaitForExit(5000) | Out-Null } catch { } - } - - # The probe is started by the installer, or by an uninstaller that runs in place from the - # directory it is emptying, so the asking process holds files there itself. Naming it would - # ask the user to close the program that is asking - a question neither the close nor the - # end offer can answer, so the guard repeated it until the update was cancelled. A holder - # that is only the asking process has to come back as nothing. - $handle = [IO.File]::Open($file, "Open", "ReadWrite", "None") - try { - $self = Invoke-Probe $dir - } finally { - $handle.Dispose() - } - if ($self.Code -ne 0) { throw "expected exit 0 when the asking process is the only holder, got $($self.Code) : $($self.Output)" } - if ($self.Output -match "pid\s+$PID\b") { throw "the lookup named the process that started it: $($self.Output)" } - - # A payload executable that refuses to be replaced is running, whether or not this probe - # may name it: the Restart Manager does not report every mapping, and the image path of a - # process under another account cannot be read from here, so both process lookups can - # come back empty while the program is still there. Held here by the asking process, so - # that it cannot be named - the answer has to be "could not name it", never "nothing is - # holding it", which is what sent an update into an uninstall that could only fail - # halfway. Exit 3 is the code the guard answers by asking the user to close it and retry. - $image = Join-Path $dir "oracle.exe" - [IO.File]::WriteAllText($image, "x") - $handle = [IO.File]::Open($image, "Open", "ReadWrite", "None") - try { - $unnameable = Invoke-Probe $dir - } finally { - $handle.Dispose() - } - if ($unnameable.Code -ne 3) { - throw "expected exit 3 for a payload executable that is in use but cannot be named, got $($unnameable.Code) : $($unnameable.Output)" - } - if ($unnameable.Output -notmatch "oracle\.exe is in use") { - throw "the answer did not name the executable it could not replace: $($unnameable.Output)" - } - - $empty = Join-Path $dir "empty" - New-Item -ItemType Directory -Force $empty | Out-Null - $nothing = Invoke-Probe $empty - if ($nothing.Code -ne 0) { throw "expected exit 0 for a directory with nothing open, got $($nothing.Code) : $($nothing.Output)" } - # A process running from the directory is a holder even when it has no file open: an - # instance holds its payload as mapped images, not as handles, and that is the state - # the guard used to miss - it passed, the purge failed, and the install ended in a - # warning with no offer to close and try again. The image is a copy of a native - # Windows program, so it starts without its usual directory next to it, and it carries - # one of the payload's own names because that is what the lookup names. - $copy = Join-Path $dir "cockatrice.exe" - Copy-Item -LiteralPath (Join-Path $env:SystemRoot "System32\cmd.exe") -Destination $copy - $running = Start-Process -FilePath $copy -ArgumentList "/c", "ping -n 20 127.0.0.1 >nul" -PassThru - try { - Start-Sleep -Milliseconds 1500 - $byImage = Invoke-Probe $dir - } finally { - try { $running.Kill() } catch { } - # Wait for the image to be released, or the cleanup below leaves the copy behind. - try { $running.WaitForExit(5000) | Out-Null } catch { } - } - if ($byImage.Code -ne 1) { throw "expected exit 1 for a process running from the directory, got $($byImage.Code) : $($byImage.Output)" } - if ($byImage.Output -notmatch "cockatrice\.exe \(pid\s+$($running.Id)") { - throw "the lookup did not name the process running from the directory: $($byImage.Output)" - } - - # A program in a folder below the directory is not an instance of the installation, even - # when it carries a payload program's name: the install directory can be a folder the - # user keeps other applications in, each in its own subfolder, and naming one of those - # asked for a close the probe never sends and an end it refuses to perform, so the guard - # could only abort the update. A program that really does hold a payload file is still - # named, by the Restart Manager lookup exercised above. - $nested = Join-Path $dir "other" - New-Item -ItemType Directory -Force $nested | Out-Null - $nestedImage = Join-Path $nested "cockatrice.exe" - Copy-Item -LiteralPath (Join-Path $env:SystemRoot "System32\cmd.exe") -Destination $nestedImage - $nestedRunning = Start-Process -FilePath $nestedImage -ArgumentList "/c", "ping -n 20 127.0.0.1 >nul" -PassThru - try { - Start-Sleep -Milliseconds 1500 - $inSubfolder = Invoke-Probe $dir - } finally { - try { $nestedRunning.Kill() } catch { } - try { $nestedRunning.WaitForExit(5000) | Out-Null } catch { } - } - if ($inSubfolder.Code -ne 0) { - throw "expected exit 0 for a program in a folder below the directory, got $($inSubfolder.Code) : $($inSubfolder.Output)" - } - - # A program the user keeps in the directory itself is not a payload program either: the - # guard ends only what the payload installs, and naming anything else asks for an answer - # that cannot free a single file. - $foreign = Join-Path $dir "tool.exe" - Copy-Item -LiteralPath (Join-Path $env:SystemRoot "System32\cmd.exe") -Destination $foreign - $foreignRunning = Start-Process -FilePath $foreign -ArgumentList "/c", "ping -n 20 127.0.0.1 >nul" -PassThru - try { - Start-Sleep -Milliseconds 1500 - $ignored = Invoke-Probe $dir - } finally { - try { $foreignRunning.Kill() } catch { } - try { $foreignRunning.WaitForExit(5000) | Out-Null } catch { } - } - if ($ignored.Code -ne 0) { - throw "expected exit 0 for a program the payload does not install, got $($ignored.Code) : $($ignored.Output)" - } - Write-Host "holder lookup named pid $($holder.Id) for a file another process holds, named nothing for a file only the asking process holds, reported a payload executable it could not name as in use instead of free, named nothing for an empty directory, named pid $($running.Id) for a payload program running from the directory, and named nothing for a payload program in a folder below it or for a program the payload does not install" -} finally { - Remove-Item -Recurse -Force $dir -ErrorAction SilentlyContinue -} -POWERSHELL - if ! PROBE_EXE="$probe" powershell -NoProfile -File "$probe_check"; then - rm -f "$probe_check" - exit 1 - fi - rm -f "$probe_check" - echo "::endgroup::" -fi - if [[ $MAKE_INSTALL ]]; then echo "::group::Install" cmake --build . --target install --config "$BUILDTYPE" diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index df054ea26..c97b48342 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -101,36 +101,6 @@ jobs: --title "$release_name" \ --notes-file "$body_path" - vcpkg-baseline: - name: vcpkg baseline - runs-on: ubuntu-slim - steps: - - name: "Checkout" - uses: actions/checkout@v7 - - # vcpkg-tool reads versions/baseline.json out of the commit named by - # `builtin-baseline`, through the `vcpkg` submodule's git history. A submodule - # pointer that lags the manifest - e.g. a stale `vcpkg/` worktree captured by - # `git add` in a PR - makes every vcpkg leg fail in configure with "failed to - # `git show` versions/baseline.json". That reads like a baseline or network - # problem, when the pointer is what has to move. Check the two agree here in - # seconds instead of on five build legs. - - name: "Check the submodule pointer against the manifest baseline" - shell: bash - run: | - pointer="$(git rev-parse HEAD:vcpkg 2>/dev/null || true)" - baseline="$(sed -nE 's/.*"builtin-baseline"[[:space:]]*:[[:space:]]*"([0-9a-f]{40})".*/\1/p' vcpkg.json)" - if [[ -z "$pointer" || -z "$baseline" ]]; then - echo "::error file=vcpkg.json::could not read the vcpkg submodule pointer or the builtin-baseline" - exit 1 - fi - if [[ "$pointer" != "$baseline" ]]; then - echo "::error file=vcpkg.json::vcpkg submodule is at $pointer but builtin-baseline is $baseline" - echo "Move them together: git -C vcpkg checkout $baseline && git add vcpkg" - exit 1 - fi - echo "vcpkg submodule and builtin-baseline agree on $baseline" - build-linux: strategy: fail-fast: false @@ -496,7 +466,7 @@ jobs: vcpkg_upload: 1 # only one leg per vcpkg_scope hands its archives over name: ${{ matrix.os }} ${{ matrix.target }}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }} - needs: [configure, vcpkg-baseline] + needs: configure runs-on: ${{ matrix.runner }} timeout-minutes: 100 env: @@ -614,17 +584,11 @@ jobs: # job. A scope nobody has written to yet therefore leaves the directory missing # and fails the job, which is exactly the state every scope starts in. Make the # directory up front so an empty scope is a cache miss instead of an error. - # always() covers the cache step itself failing: the action's post save still runs - # then, and it should report the real error instead of ENOENT. It cannot rescue a - # failure before the cache step, since a post step only runs for an action whose - # main step did. The fallback keeps this guard from failing when the cache step was - # skipped and left no path output. - name: "Create vcpkg cache directory" - if: always() shell: bash env: CACHE_DIR: '${{ steps.vcpkg-cache.outputs.path }}' - run: mkdir -p "${CACHE_DIR:-$GITHUB_WORKSPACE/.vcpkg-cache}" + run: mkdir -p "$CACHE_DIR" # Uses environment variables, see compile.sh for more details - name: "Build Cockatrice" diff --git a/CMakeLists.txt b/CMakeLists.txt index 8a25fe2c6..97b87a1d8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -90,7 +90,7 @@ endif() # A project name is needed for CPack # Version can be overriden by git tags, see cmake/getversion.cmake -project("Cockatrice" VERSION 3.2.0) +project("Cockatrice" VERSION 3.1.2) # Set release name if not provided via env/cmake var if(NOT DEFINED GIT_TAG_RELEASENAME) @@ -250,22 +250,7 @@ message(STATUS "Update Translations: ${UPDATE_TRANSLATIONS}") include(FindQtRuntime) -# Reject APIs that were deprecated before the oldest Qt release we support -# (6.4). Qt 6.5 renamed the switch, so use whichever spelling the found -# version understands while still compiling everywhere. -if(Qt6_VERSION VERSION_GREATER_EQUAL 6.5) - add_compile_definitions(QT_DISABLE_DEPRECATED_UP_TO=0x060400) -else() - add_compile_definitions(QT_DISABLE_DEPRECATED_BEFORE=0x060400) -endif() - -# Set once here for every target: these variables are inherited by the -# subdirectories, so the per-directory copies were redundant. Only the -# variables are set; qt_standard_project_setup would also change the runtime -# output directories and the install RPATH. set(CMAKE_AUTOMOC TRUE) -set(CMAKE_AUTOUIC TRUE) -set(CMAKE_AUTORCC TRUE) # Find other needed libraries find_package(Protobuf CONFIG) @@ -435,9 +420,6 @@ if(WITH_ORACLE OR WITH_CLIENT) add_subdirectory(${CMAKE_SOURCE_DIR}/libcockatrice_models ${CMAKE_BINARY_DIR}/libcockatrice_models) add_subdirectory(${CMAKE_SOURCE_DIR}/libcockatrice_filters ${CMAKE_BINARY_DIR}/libcockatrice_filters) endif() -if(WITH_CLIENT) - add_subdirectory(${CMAKE_SOURCE_DIR}/libcockatrice_game ${CMAKE_BINARY_DIR}/libcockatrice_game) -endif() if(WITH_SERVER) add_subdirectory(servatrice) @@ -449,13 +431,6 @@ if(WITH_CLIENT) set(CPACK_INSTALL_CMAKE_PROJECTS "Cockatrice;Cockatrice;ALL;/" ${CPACK_INSTALL_CMAKE_PROJECTS}) endif() -if(WIN32 AND WITH_CLIENT) - # Runs from the installer's $PLUGINSDIR when a purge fails, and is never installed - # (see LogProcessesUsingInstallDir in cmake/NSIS.template.in), so it has no install - # rules and is not part of the client's staged tree. - add_subdirectory(${CMAKE_SOURCE_DIR}/tools/installer-holder-probe ${CMAKE_BINARY_DIR}/tools/installer-holder-probe) -endif() - if(WITH_ORACLE) add_subdirectory(oracle) set(CPACK_INSTALL_CMAKE_PROJECTS "Oracle;Oracle;ALL;/" ${CPACK_INSTALL_CMAKE_PROJECTS}) @@ -468,5 +443,5 @@ endif() if(Qt6_FOUND AND Qt6_VERSION_MINOR GREATER_EQUAL 3) # Qt6.3+ requires project finalization to support translations - qt_finalize_project() + qt6_finalize_project() endif() diff --git a/README.md b/README.md index fa9272c0f..f2a170868 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,9 @@ ---

-To get started with Cockatrice ⇢ [view our webpage](https://cockatrice.github.io/) -To get support, or suggest changes to the app ⇢ [file an issue](https://github.com/Cockatrice/Cockatrice/issues) ([How?](https://github.com/Cockatrice/Cockatrice/wiki/How-to-Create-a-GitHub-Ticket-Regarding-Cockatrice))
-To learn about the app and its code ⇢ [read our code documentation](https://cockatrice.github.io/docs/) -To help with development ⇢ learn [how to contribute](#contribute) +To get started with Cockatrice ⇢ [view our webpage](https://cockatrice.github.io/)
+To get support, or suggest changes to the app ⇢ [file an issue](https://github.com/Cockatrice/Cockatrice/issues) ([How?](https://github.com/Cockatrice/Cockatrice/wiki/How-to-Create-a-GitHub-Ticket-Regarding-Cockatrice)) +To help with development ⇢ learn [how to contribute](#contribute-)


@@ -84,7 +83,7 @@ Join our [Discord community](https://discord.gg/3Z9yzmA) to connect with other p ### Code -To commit code to the project, please review our [Contribution Guidelines](https://github.com/Cockatrice/Cockatrice/blob/master/.github/CONTRIBUTING.md) first.
+To contribute code to the project, please review our [guidelines](https://github.com/Cockatrice/Cockatrice/blob/master/.github/CONTRIBUTING.md) first.
We maintain two tags for contributors to easier find issues to potentially work on: - [![Good first issue](https://img.shields.io/github/issues/cockatrice/cockatrice/Good%20First%20Issue)](https://github.com/Cockatrice/Cockatrice/issues?utf8=%E2%9C%93&q=is%3Aopen%20is%3Aissue%20label%3A%22Good%20first%20issue%22%20)
Issues tagged in this way provide a simple way to get started. They don't require much experience to be worked on. @@ -96,7 +95,7 @@ We'll happily advice on how best to implement a feature, or we can show you wher You can also have a look at our `Todo List` in our [Code Documentation](https://cockatrice.github.io/docs) or search the repo for [`\todo` comments](https://github.com/search?q=repo%3ACockatrice%2FCockatrice%20%5Ctodo&type=code). -### Documentation [![CI Docs](https://github.com/Cockatrice/Cockatrice/actions/workflows/documentation-build.yml/badge.svg?event=push)](https://github.com/Cockatrice/Cockatrice/actions/workflows/documentation-build.yml?query=event%3Arelease+event%3Aworkflow_dispatch) +### Documentation [![CI Docs](https://github.com/Cockatrice/Cockatrice/actions/workflows/documentation-build.yml/badge.svg?event=push)](https://github.com/Cockatrice/Cockatrice/actions/workflows/documentation-build.yml?query=event%3Apush) There are various places where useful information for different needs are maintained: - [Official Code Documentation](https://cockatrice.github.io/docs/) @@ -105,8 +104,7 @@ There are various places where useful information for different needs are mainta - [Official README](https://github.com/Cockatrice/Cockatrice/blob/master/README.md) `This file` - [Official CONTRIBUTING](https://github.com/Cockatrice/Cockatrice/blob/master/.github/CONTRIBUTING.md) -Cockatrice tries to use the [Google Developer Documentation Style Guide](https://developers.google.com/style/) to ensure consistent documentation. -We encourage everybody to improve the documentation by suggesting edits based on this guide. +Cockatrice tries to use the [Google Developer Documentation Style Guide](https://developers.google.com/style/) to ensure consistent documentation. We encourage you to improve the documentation by suggesting edits based on this guide. ### Translation [![Transifex Project](https://img.shields.io/badge/translate-on%20transifex-brightgreen)](https://explore.transifex.com/cockatrice/cockatrice/) diff --git a/cmake/NSIS.template.in b/cmake/NSIS.template.in index 923aded9d..0ef746430 100644 --- a/cmake/NSIS.template.in +++ b/cmake/NSIS.template.in @@ -12,24 +12,10 @@ Var NormalDestDir Var PortableDestDir Var PortableMode Var ReinstallMode +Var AllowForceClose Var GuardDir Var OldUninstallString Var OldInstallDir -; What ExecWait reported for the previous version's uninstaller, or "missing" / -; "launch-failed" when RunOldUninstaller could not start it. Read by WaitForOldPayloadToDisappear: an uninstaller that ran in place -; (_?=) and failed or was cancelled will not remove anything more, however long we wait. -Var OldUninstallExit -; Executable path parsed out of $OldUninstallString by ResolveOldUninstallExe. Used to -; tell an uninstaller that is missing or cannot be started ("missing" / "launch-failed" -; in $OldUninstallExit) from one that ran and failed. -Var OldUninstallExe -; Set to 1 by RemoveOldPayloadLeftovers only when every leftover it is responsible for is -; gone; WaitForOldPayloadToDisappear aborts while it is still 0. -Var OldPayloadRemoved -; Scratch state of AppendUninstallerLog. -Var UninstLogHandle -Var UninstLogLine -Var UninstLogChar Var DataBackupDir Var DataBackupDone Var DataBackupForDir @@ -41,48 +27,9 @@ Var StaleFindName Var DataBackupFindHandle Var DataBackupFindName Var IsUpgrade -; Set together by whichever purge block ran: the verdict RetryDllPurge measured -; and whether a purge ran at all - a portable install into a directory without -; portable.dat is neither purge block, and the abort below measures it itself. -Var StaleRuntimeAfterPurge -Var DllPurgeRan Var AbortReason -; Diagnostics for a failed purge: the holder probe in LogProcessesUsingInstallDir writes -; $HolderOutput (one log line) and sets $HolderFound when it named anything. -Var HolderExit -Var HolderOutput -Var HolderFound -; What the last RunHolderProbe call answered: its exit code and its line. Kept -; apart from $HolderExit/$HolderOutput because the guard only wants to know -; whether the payload is free, while the abort dialog reports what survived. -Var GuardExit -Var GuardOutput -; How many times the guard has asked the payload programs to close by itself. Bounded, and -; only repeated while the listing changed between rounds (see GuardAppsNotRunning). -Var GuardCloseRounds -; The listing the last automatic close was sent against, so a second round is spent only -; when something actually exited and the rest is worth asking again. Empty before round one. -Var GuardLastHolders -; How many times the user asked to check again after closing programs by hand (logged only). -Var GuardRecheckCount -; The question CockEndPrompt/CockEndRecheckPrompt assemble: fixed sentences around the -; probe's line, and the result has to stay inside the characters one NSIS string holds. -Var GuardPrompt Var WaitPid Var LauncherExited -; How long one WaitForSingleObject on the launcher's handle waits before asking the user -; (see WaitForLauncherToExit). A variable because the first slice is short and the ones -; after the first question are not. -Var WaitSlice -; Whether that handle carries PROCESS_TERMINATE as well as SYNCHRONIZE, and can therefore -; be ended from the answer the dialog offers. Cleared when only the synchronize right could -; be had (see WaitForLauncherToExit). -Var WaitCanEnd -; How far the launcher wait got with a launcher that has no window to ask: 0 before the -; first round, 1 after the grace it was given, 2 after it was ended. A launcher whose -; window is gone has answered its own close and is stuck in shutdown, so the dialog cannot -; reach it and the wait may end it instead of asking the user (see WaitForLauncherToExit). -Var WaitWindowless Var InstallLogPath Var LogHandle Var LogMessage @@ -147,10 +94,7 @@ Page Custom PortableModePageCreate PortableModePageLeave Call CockLogWrite !macroend -; The body is shared because the uninstaller needs its own copy: un.* functions are -; compiled into a separate binary, and inside one a Call may only name an un. function, -; so CockLog's Call to CockLogWrite cannot be used there. -!macro CockLogWriteBody +Function CockLogWrite Pop $LogMessage FileOpen $LogHandle "$InstallLogPath" a ${If} $LogHandle == "" @@ -162,20 +106,6 @@ Page Custom PortableModePageCreate PortableModePageLeave FileSeek $LogHandle 0 END FileWrite $LogHandle "$LogMessage$\r$\n" FileClose $LogHandle -!macroend - -Function CockLogWrite - !insertmacro CockLogWriteBody -FunctionEnd - -; CockLog for un.* code: same lines, prefixed call target. -!macro CockLogUn text - Push "${text}" - Call un.CockLogWrite -!macroend - -Function un.CockLogWrite - !insertmacro CockLogWriteBody FunctionEnd ; Logs every file matching $INSTDIR\. Used to record the DLL inventory @@ -218,11 +148,6 @@ StrCpy $LooseDeckNoticeShown "" ; No old install location resolved yet; Backup/Restore fall back to $INSTDIR ; when this is empty (the /R path where $INSTDIR already is the old dir). StrCpy $OldInstallDir "" -StrCpy $OldUninstallExit "" -; No purge has run and nothing has been measured yet. The section sets both -; after a purge, or measures the directory itself when neither purge applies. -StrCpy $StaleRuntimeAfterPurge 0 -StrCpy $DllPurgeRan 0 ; Start each run from a clean file so the log that gets sent back is the last ; run, not every past run glued together. FileOpen $LogHandle "$InstallLogPath" w @@ -289,10 +214,10 @@ ${EndIf} ; /PID= carries the process id of the application that started this installer. ; The in-app updater launches the installer and only then quits itself, so ; without it the installer has to guess whether the launching process is still -; alive from the files it still has open - a guess that can come back "nothing -; is holding them" while the client is very much alive and still has its Qt DLLs -; mapped. An older client does not pass /PID= and an older installer ignores it, -; so both sides of the pair have to keep working. +; alive by polling powershell.exe for an image name - a guess that can come back +; "not running" or "could not determine" while the process is very much alive and +; still has its Qt DLLs mapped. An older client does not pass /PID= and an older +; installer ignores it, so both sides of the pair have to keep working. ; Note that /D= must stay the last parameter on the command line, which is why ; the client puts it last and this one anywhere. StrCpy $WaitPid "" @@ -330,16 +255,18 @@ ${Else} ${EndIf} ; Wait for the process that launched this installer to exit before anything is -; replaced or deleted. It goes first because a real handle on that process is -; authority: the holder probe that follows asks the same question indirectly, by -; looking at which files are still open. +; replaced or deleted. It goes first because it is the one check here that needs +; no powershell.exe: on a machine where the process lookup cannot run at all, the +; launcher is still proven gone here instead of the whole upgrade being cancelled +; by a check that was only ever a proxy for this one. ; ; Then make sure no *other* application instance is still running (and holding -; file locks) before the previous version is uninstalled. The probe names them -; and the user is asked to close them; nothing is force-stopped without that -; answer, because Cockatrice may hold an unsaved deck and Oracle writes the card -; database in place. On /R the old version is about to be removed, so an answer -; the probe cannot give cancels the upgrade before it starts rather than after it. +; file locks) before the previous version is uninstalled. On a silent update +; (/R /S) the running processes are asked to close gracefully and waited for; +; Cockatrice itself is never force-killed (data loss, e.g. an unsaved deck) so the +; update is aborted instead, while the console applications Oracle and Servatrice +; are force-stopped by PID once the grace period is over. On interactive installs +; the user is prompted to close them instead. ; Only needed this early for /R, where AutoUninstallIfNeeded runs below; for ; interactive installs the section re-checks after the directory page, when ; $INSTDIR is the directory the user actually picked. @@ -363,14 +290,6 @@ ${If} ${NSIS_IS_64_BIT} == 1 SetRegView 64 ${EndIf} -; The uninstaller logs to its own file: the installer truncates its log on every run, so -; sharing one would mean either side erasing the other's account of a failed upgrade. -StrCpy $InstallLogPath "$TEMP\Cockatrice-uninstaller.log" -FileOpen $LogHandle "$InstallLogPath" w -FileClose $LogHandle -!insertmacro CockLogUn "==================================================================" -!insertmacro CockLogUn "uninstaller start: @CPACK_PACKAGE_NAME@ @CPACK_PACKAGE_VERSION@ dir=$INSTDIR" - ; Ensure no application instance is still running before removing files. Call un.EnsureAppsNotRunning @@ -401,61 +320,6 @@ ${If} $ReinstallMode = 1 ${EndIf} FunctionEnd -; Sets $OldUninstallExe to the executable named in $OldUninstallString (everything up to -; and including the first ".exe", without a leading quote), or "" when there is none. -Function ResolveOldUninstallExe - Push $R7 - Push $R8 - StrLen $R8 $OldUninstallString - StrCpy $R7 0 - roue_loop: - ${If} $R7 >= $R8 - StrCpy $OldUninstallExe "" - Goto roue_out - ${EndIf} - StrCpy $OldUninstallExe $OldUninstallString 4 $R7 - ${If} $OldUninstallExe == ".exe" - IntOp $R7 $R7 + 4 - StrCpy $OldUninstallExe $OldUninstallString $R7 - StrCpy $R8 $OldUninstallExe 1 - ${If} $R8 == '"' - StrCpy $OldUninstallExe $OldUninstallExe "" 1 - ${EndIf} - Goto roue_out - ${EndIf} - IntOp $R7 $R7 + 1 - Goto roue_loop - roue_out: - Pop $R8 - Pop $R7 -FunctionEnd - -; Runs "$OldUninstallString _?=$OldInstallDir" in place and sets $OldUninstallExit: -; the uninstaller's own exit code when it ran, "missing" when the executable the -; registry names does not exist, "launch-failed" when ExecWait could not start it. -; The last two used to leave the code empty, which WaitForOldPayloadToDisappear read -; as a cancelled uninstall and aborted on, although nobody had cancelled anything. -; Uses $R5 for the exit code, as the callers did before. -Function RunOldUninstaller - Call ResolveOldUninstallExe - StrCpy $R5 "" - ${If} $OldUninstallExe == "" - ${OrIfNot} ${FileExists} "$OldUninstallExe" - StrCpy $OldUninstallExit "missing" - !insertmacro CockLog "old uninstall: uninstaller '$OldUninstallExe' does not exist (command '$OldUninstallString')" - Return - ${EndIf} - ClearErrors - ExecWait '$OldUninstallString _?=$OldInstallDir' $R5 - ${If} ${Errors} - ${OrIf} $R5 == "" - StrCpy $OldUninstallExit "launch-failed" - !insertmacro CockLog "old uninstall: could not start '$OldUninstallString _?=$OldInstallDir'" - Return - ${EndIf} - StrCpy $OldUninstallExit $R5 -FunctionEnd - Function AutoUninstallIfNeeded SetShellVarContext all @@ -480,10 +344,7 @@ StrCpy $OldInstallDir $INSTDIR StrCmp $R0 "" done32 DetailPrint "Removing previous version (32-bit)..." Call BackupUserDataBeforeUninstall -StrCpy $OldUninstallString $R0 -Call RunOldUninstaller -!insertmacro CockLog "uninstall (32-bit): exit=$OldUninstallExit" -Call WaitForOldPayloadToDisappear +ExecWait '$R0 _?=$OldInstallDir' Call RestoreUserDataAfterUninstall done32: @@ -498,10 +359,7 @@ ${If} ${RunningX64} StrCmp $R0 "" done64 DetailPrint "Removing previous version (64-bit)..." Call BackupUserDataBeforeUninstall - StrCpy $OldUninstallString $R0 - Call RunOldUninstaller - !insertmacro CockLog "uninstall (64-bit): exit=$OldUninstallExit" - Call WaitForOldPayloadToDisappear + ExecWait '$R0 _?=$OldInstallDir' Call RestoreUserDataAfterUninstall done64: @@ -509,192 +367,6 @@ ${EndIf} FunctionEnd -; Waits for the previous installation's payload to be gone, so the sections below never -; write into a directory an old uninstaller is still emptying. -; -; ExecWait only waits for as long as the uninstaller runs in place: an NSIS uninstaller -; that did not get a usable _?= copies itself to %TEMP%, starts that copy and exits, so -; ExecWait returns at once while the copy keeps deleting - and it finishes after this -; window has closed, removing the version that was just installed. Waiting for -; cockatrice.exe to disappear covers both cases, and stops with a message instead of -; -; When the payload is still there after the wait, $OldUninstallExit decides: a non-zero -; code means the uninstaller was cancelled or failed, and the install aborts saying so. -; A zero code means it finished but skipped files someone was holding (NSIS Delete does -; that silently), so RemoveOldPayloadLeftovers frees and removes them, and the install -; only aborts - naming the holders - when even that fails. -; installing over a directory that is still being emptied. -Function WaitForOldPayloadToDisappear -${If} $OldInstallDir == "" - Return -${EndIf} -; The registered uninstaller could not be run at all (missing, or ExecWait could not -; start it): nothing is going to empty the directory, so waiting is pointless and -; aborting as "cancelled" was wrong. Remove the previous payload the same way the -; skipped-files recovery below does, and abort - naming why - only if that fails. -${If} $OldUninstallExit == "missing" -${OrIf} $OldUninstallExit == "launch-failed" - ${IfNot} ${FileExists} "$OldInstallDir\*.*" - !insertmacro CockLog "old uninstall: uninstaller $OldUninstallExit and $OldInstallDir is already gone" - Return - ${EndIf} - !insertmacro CockLog "old uninstall: uninstaller $OldUninstallExit ('$OldUninstallString'); removing $OldInstallDir payload directly" - Call AppendUninstallerLog - Call RemoveOldPayloadLeftovers - ${If} $OldPayloadRemoved != "1" - StrCpy $AbortReason \ - "The previous version in $OldInstallDir could not be removed: its uninstaller ($OldUninstallExe) could not be run ($OldUninstallExit), and removing the files directly failed.$\r$\n$\r$\nStill in use by: $HolderOutput$\r$\n$\r$\nClose anything still using those files and run this installer again." - Call RestoreUserDataAfterUninstall - Call AbortInstall - ${EndIf} - !insertmacro CockLog "old uninstall: continuing after removing the payload of an uninstaller that could not run" - Return -${EndIf} -StrCpy $R2 0 -old_uninstall_wait: -IfFileExists "$OldInstallDir\cockatrice.exe" 0 old_uninstall_gone -IntOp $R2 $R2 + 1 -${If} $R2 > 60 - !insertmacro CockLog "old uninstall: $OldInstallDir still holds cockatrice.exe after 30 s (uninstaller exit=$OldUninstallExit)" - ; The uninstaller's own account of what it skipped or why it stopped lives in its - ; separate log; copy it here so the report sent from AbortInstall carries it. - Call AppendUninstallerLog - ${If} $OldUninstallExit != "0" - ; Cancelled or failed with a non-zero exit code (an uninstaller that could not be - ; started is handled above): the old version was not meant to be removed, so - ; nothing is deleted on its behalf. - StrCpy $AbortReason \ - "The previous version in $OldInstallDir is still there because its uninstaller was cancelled or failed (exit code '$OldUninstallExit').$\r$\n$\r$\nClose anything still using those files and run this installer again." - ; The backup was taken before the uninstaller ran; an abort must not leave the - ; only copy of the user data in %TEMP%, where the next run clears it. - Call RestoreUserDataAfterUninstall - Call AbortInstall - ${EndIf} - ; The uninstaller finished but skipped files - a running instance or a scanner holding - ; them, which NSIS Delete passes over silently. Free them and remove what is left. - Call RemoveOldPayloadLeftovers - ; The removal reports success only when every payload executable is gone and the - ; DLL/Plugins purge came back clean; cockatrice.exe alone would hide a locked - ; oracle.exe or a stale DLL the new payload is about to be written over. - ${If} $OldPayloadRemoved != "1" - StrCpy $AbortReason \ - "The previous version in $OldInstallDir could not be fully removed after its uninstaller ran.$\r$\n$\r$\nStill in use by: $HolderOutput$\r$\n$\r$\nClose anything still using those files and run this installer again." - Call RestoreUserDataAfterUninstall - Call AbortInstall - ${EndIf} - ; The payload is gone and ExecWait already waited for the uninstaller; keep the - ; short settle the other paths use, then hand back to the caller's restore. - Sleep 1000 - !insertmacro CockLog "old uninstall: continuing after removing the leftovers the uninstaller skipped" - Return -${EndIf} -Sleep 500 -Goto old_uninstall_wait -old_uninstall_gone: -${If} $R2 > 0 - ; ExecWait returned while the old payload was still there: that uninstaller - ; copied itself to %TEMP%, and the copy is still working through its file list. - ; cockatrice.exe is gone before the rest of it, so let the copy settle before - ; the sections below start writing files it would otherwise remove. - Sleep 1000 - !insertmacro CockLog "old uninstall: ExecWait returned early, settled 1 s after $R2 * 500 ms" -${Else} - !insertmacro CockLog "old uninstall: $OldInstallDir was already clear when ExecWait returned" -${EndIf} -Return -FunctionEnd - -; Called when the old uninstaller exited successfully but left its payload behind, or -; could not be run at all (missing or not startable) and the payload is still there. -; Frees the files the same way the guard does (close, then offer to end), deletes what -; the uninstaller skipped, and on failure records which files survived and who holds -; them in $HolderOutput, for the abort message. Sets $OldPayloadRemoved to 1 only when -; every payload executable is gone and the DLL/Plugins purge is clean. -Function RemoveOldPayloadLeftovers - !insertmacro CockLog "old uninstall: removing leftovers of $OldInstallDir" - StrCpy $OldPayloadRemoved 0 - StrCpy $HolderOutput "nothing could be named" - StrCpy $GuardDir $OldInstallDir - Call GuardAppsNotRunning - ; The purge helpers work on $INSTDIR; point them at the old directory for the - ; duration (on /R the two are the same anyway). - Push $INSTDIR - StrCpy $INSTDIR $OldInstallDir - StrCpy $R2 0 - rop_loop: - SetFileAttributes "$INSTDIR\cockatrice.exe" NORMAL - SetFileAttributes "$INSTDIR\oracle.exe" NORMAL - SetFileAttributes "$INSTDIR\servatrice.exe" NORMAL - Delete "$INSTDIR\cockatrice.exe" - Delete "$INSTDIR\oracle.exe" - Delete "$INSTDIR\servatrice.exe" - Delete "$INSTDIR\qt.conf" - Call RetryDllPurge - ; $R0 is RetryDllPurge's verdict (1 = a DLL or the Plugins directory survived). - ; Every payload executable has to be gone as well: a locked oracle.exe or - ; servatrice.exe is just as fatal to the install that follows as cockatrice.exe. - IfFileExists "$INSTDIR\cockatrice.exe" rop_retry - IfFileExists "$INSTDIR\oracle.exe" rop_retry - IfFileExists "$INSTDIR\servatrice.exe" rop_retry - ${If} $R0 == 0 - Goto rop_done - ${EndIf} - rop_retry: - IntOp $R2 $R2 + 1 - ${If} $R2 < 3 - !insertmacro CockLog "old uninstall: leftovers of $OldInstallDir survived removal attempt $R2, retrying in 2 s" - Sleep 2000 - Goto rop_loop - ${EndIf} - !insertmacro CockLog "old uninstall: leftovers of $OldInstallDir still present after 3 removal attempts" - !insertmacro CockLogListing "old-leftovers" "*.*" - ; Per-file Windows codes for the DLLs plus the holder probe's line ($HolderOutput). - Call LogStaleRuntimeDiagnostics - Pop $INSTDIR - Return - rop_done: - StrCpy $OldPayloadRemoved 1 - ; Only now that the recovery has succeeded: deleting the old uninstaller on a - ; failed attempt would leave a directory whose UninstallString points at nothing, - ; so no later run could remove it. WriteUninstaller recreates it on success anyway. - SetFileAttributes "$INSTDIR\uninstall.exe" NORMAL - Delete "$INSTDIR\uninstall.exe" - !insertmacro CockLog "old uninstall: leftovers of $OldInstallDir removed" - !insertmacro CockLogListing "old-leftovers-after-removal" "*.*" - Pop $INSTDIR -FunctionEnd - -; Copies the old uninstaller's log ($TEMP\Cockatrice-uninstaller.log) into this -; installer's log, so the report AbortInstall sends shows what the uninstaller did. -; Uninstallers older than this log simply have no file; that is logged too. -Function AppendUninstallerLog - ClearErrors - FileOpen $UninstLogHandle "$TEMP\Cockatrice-uninstaller.log" r - ${If} ${Errors} - ${OrIf} $UninstLogHandle == "" - !insertmacro CockLog "uninstaller log: not found at $TEMP\Cockatrice-uninstaller.log" - Return - ${EndIf} - !insertmacro CockLog "uninstaller log begin ----------------------------------------" - aul_loop: - ClearErrors - FileRead $UninstLogHandle $UninstLogLine - IfErrors aul_done - ; Strip the line break FileRead keeps, so CockLog does not double it. - aul_trim: - StrCpy $R9 $UninstLogLine 1 -1 - ${If} $R9 == "$\n" - ${OrIf} $R9 == "$\r" - StrCpy $UninstLogLine $UninstLogLine -1 - Goto aul_trim - ${EndIf} - !insertmacro CockLog "uninstaller: $UninstLogLine" - Goto aul_loop - aul_done: - FileClose $UninstLogHandle - !insertmacro CockLog "uninstaller log end ------------------------------------------" -FunctionEnd - ; BackupUserDataBeforeUninstall / RestoreUserDataAfterUninstall ; ; The uninstallers shipped in 3.1.0/3.1.1 run a blanket "RMDir /r $INSTDIR". @@ -996,337 +668,222 @@ FunctionEnd ; "The procedure entry point X could not be located in the dynamic link ; library ...Qt6Network.dll" on the next start. ; -; Both questions this installer has about running processes - "may I replace -; the payload?" and "what is holding this file I could not delete?" - are -; answered by tools/installer-holder-probe, a small executable built from this -; repository and embedded in the installer and the uninstaller alike. It names -; every process and service that has one of the payload files open or mapped -; (the Restart Manager, RmRegisterResources/RmGetList) and, separately, every -; payload program whose image lies in the directory being replaced rather than -; in a folder below it. +; On a silent update (/R /S) matching processes are asked to close gracefully +; (WM_CLOSE) and waited for up to 60 s. Cockatrice is never force-stopped: it +; may be holding an unsaved deck or showing a "really leave this game?" +; confirmation, so if it refuses to close the install is aborted - nothing has +; been replaced yet, so the update is simply deferred. Oracle and Servatrice +; are console applications without a main window, so CloseMainWindow() does +; nothing for them and waiting cannot help; they hold no unsaved user data and +; are force-stopped by PID once the grace period is over. On interactive +; installs and uninstalls the user is prompted to close them instead. ; -; The second lookup is not redundant. Restart Manager answers "who has this -; file open", and a running instance does not have to have one open: its own -; image and the modules it loaded are mapped into it rather than opened by it, -; and a mapped image is not reliably reported. That is the state the failed -; updates this probe exists for were in - every DLL still on disk, an instance -; running from the directory, and "nothing is holding them any more" as the -; answer. A guard built on file handles alone therefore misses exactly the -; instance it exists to find; it would pass, the purge would fail, and the -; install would end in a warning with no offer to close and try again. -; -; Why not enumerate processes from this script: the NSIS stub is a 32-bit -; process, so a bare "powershell" started from it is the 32-bit Windows -; PowerShell, whose Get-Process cannot read the path of a 64-bit process. A -; guard built on that misses exactly the running instance it exists to find. -; The probe opens each process with PROCESS_QUERY_LIMITED_INFORMATION and calls -; QueryFullProcessImageNameW, which has no bitness mismatch, and Restart -; Manager is a system API rather than a view of this process's architecture. -; -; The files that count are the payload the purge removes and the installer -; replaces: the three executables, the top-level DLLs, qt.conf and everything -; below Plugins. A document, a log or a screenshot the user happens to have -; open in the install directory is not part of it and cannot block an upgrade; -; the image lookup matches on the directory a process runs from, so it does not -; widen that set. -; -; Nothing is force-stopped without the user asking for it: Cockatrice may be holding -; an unsaved deck or showing a "really leave this game?" confirmation, Oracle writes -; the card database in place and can be killed mid-write, and Servatrice may be serving -; a game. A close is a request, made automatically because that is what the user started -; the installer to do: the probe first asks every instance over its local socket, which -; reaches a hidden window and an instance sitting on its own confirmation, and falls back -; to WM_CLOSE for an older client. The automatic close is retried while the set of holders -; changes, so one instance exiting does not leave the next one unasked. When a close is -; still not answered the user is told which program is in the way and is offered to force -; close all three, check again after closing them by hand, or abort - the last resort, with -; what it costs spelled out in the prompt. +; Processes are matched by image name AND by their executable path living +; under the guarded directory ($GuardDir, normally $INSTDIR), so unrelated +; processes that merely share an image name (e.g. the Oracle DB instance +; "oracle.exe") are never touched. -; One sentence for every prompt whose Yes ends the whole set, so that answer reads the same -; wherever the user meets it: this guard before anything is replaced, the uninstaller's, and -; the recovery prompt after a failed purge. -!define END_COST "An unsaved deck, a running game or an unwritten setting is lost, Servatrice drops its players, and an Oracle that is mid-write can damage the card database." - -; What the user does instead when they say no: the prompt's other answer stops the run, so they -; are told how to get past the same wall next time. Also used by the abort dialogs, which is -; where the answer to "why did it stop" ends up. -!define AFTER_CLOSING_GUIDANCE "Answer the confirmation Cockatrice may be showing - open it from the notification area if it is hidden there - and use Task Manager for a program that would not end." - -; The escape hatch for a user who would rather not force-close anything: the same release, -; downloaded and run by hand once Cockatrice is closed. Every abort dialog shows this and -; offers to open the page, so an update that cannot free the files still leaves the user a -; way forward that does not end in Task Manager. -!define LATEST_RELEASE_URL "https://github.com/Cockatrice/Cockatrice/releases/latest" -!define MANUAL_UPDATE_GUIDANCE "To update safely instead, close Cockatrice and download the latest installer from:$\r$\n${LATEST_RELEASE_URL}$\r$\nThen run it manually." - -; usage: set the variable named by the second argument to the probe's listing, then call -; this. Sets $GuardPrompt to the question the guard and the recovery path ask. -; -; One macro rather than three copies, because the wording is a promise: Yes ends the -; programs the payload installs - which is exactly the set EndHolders acts on - and anything -; else the probe named has to be closed by hand, so a listing that includes a scanner or a -; service hosted elsewhere does not come with an answer that cannot do what it says. -; -; Two answers, one of each: the automatic close already ran when this is asked, so Yes is the -; only thing left that can free the files and No stops the run. The guard itself uses -; CockEndRecheckPrompt instead, whose No checks again; this one stays for the recovery path, -; where a retry cannot change anything because the purge was already retried. -; -; The assembled string has to fit the characters one NSIS string holds, which is what -; bounds HOLDER_LINE_CHAR_MAX in tools\installer-holder-probe\holder_line.h: with the -; longest listing that can arrive it stays inside that budget instead of being cut off at -; the tail, where the cancel line is. -; -; @param header the sentence that introduces the listing, with its trailing colon -; @param listing the variable holding the probe's line, $GuardOutput or $HolderOutput -; @param cancelWord what No gives up, "installation" or "uninstallation" -!macro CockEndPrompt header listing cancelWord - StrCpy $GuardPrompt "${header}$\r$\n$\r$\n${listing}" - StrCpy $GuardPrompt "$GuardPrompt$\r$\n$\r$\nChoose Yes to force close the Cockatrice, Oracle and Servatrice programs listed above. ${END_COST}$\r$\n$\r$\nChoose No to cancel the ${cancelWord}. ${AFTER_CLOSING_GUIDANCE}" -!macroend - -; usage: like CockEndPrompt, for a MB_YESNOCANCEL question whose No checks again. -; -; The automatic close has already run, so the three answers are what is left: force close -; the programs the payload installs (Yes), close them by hand and check again (No), or stop -; the run (Cancel). No used to be the only alternative to ending them, which meant a user who -; would rather close the instance themselves had to abandon the update and start the -; installer again. The listing the question is asked with is refreshed on every pass, so the -; question changes as programs go away instead of repeating itself word for word. -; -; The manual-update guidance lives in the abort dialog, which is where a Cancel ends up, not -; in this prompt. With the longest listing HOLDER_LINE_CHAR_MAX allows, the assembled string -; stays under 900 characters, inside the 1024 one NSIS string holds, so the Cancel line is -; never the part that gets cut off. -; -; @param header the sentence that introduces the listing, with its trailing colon -; @param listing the variable holding the probe's line, $GuardOutput -; @param cancelWord what Cancel gives up, "installation" or "uninstallation" -!macro CockEndRecheckPrompt header listing cancelWord - StrCpy $GuardPrompt "${header}$\r$\n$\r$\n${listing}" - StrCpy $GuardPrompt "$GuardPrompt$\r$\n$\r$\nChoose Yes to force close the Cockatrice, Oracle and Servatrice programs listed above. ${END_COST}$\r$\n$\r$\nChoose No to close them yourself, then check again - answer the confirmation Cockatrice may be showing, and open it from the notification area if it is hidden there.$\r$\n$\r$\nChoose Cancel to abort the ${cancelWord}." -!macroend - -; Emitted once for the installer and once for the uninstaller: the uninstaller -; cannot call installer functions, and its own copy of the probe keeps a running -; 64-bit instance from being missed there too. -!macro CockHolderProbeFunctions prefix tag -Function ${prefix}ExtractHolderProbe - InitPluginsDir - IfFileExists "$PLUGINSDIR\installer-holder-probe.exe" ck_probe_ready_${tag} - File "/oname=$PLUGINSDIR\installer-holder-probe.exe" "..\..\..\installer-holder-probe.exe" - ck_probe_ready_${tag}: -FunctionEnd - -; usage: set $GuardDir to the directory to check, then call this. Sets -; $GuardExit to the probe's exit code and $GuardOutput to its one-line answer. -; 0 = nothing holds the payload and no payload program runs from the directory, 1 = the -; line names what does, anything else = the probe could not answer (2 = no -; directory was given, 3 = no payload program was named but one refuses to be replaced, or -; the Restart Manager call failed and no payload program runs from the directory either, -; "timeout" = it did not return in time). -Function ${prefix}RunHolderProbe - Call ${prefix}ExtractHolderProbe - ; The directory travels in an environment variable, never on the command - ; line: a path with spaces, quotes or a trailing backslash then does not - ; have to survive NSIS quoting. - System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")' - ; /TIMEOUT because this can run before the first page: a Restart Manager - ; that does not answer must not leave the installer with no window and no - ; way out. - nsExec::ExecToStack /TIMEOUT=20000 '"$PLUGINSDIR\installer-holder-probe.exe"' - Pop $GuardExit - Pop $GuardOutput -FunctionEnd - -; usage: set $GuardDir to the directory being replaced, then call this. Asks the running -; Cockatrice to close, and waits for it to exit. -; -; The set is what the probe names: the processes holding a file of the version being -; replaced, plus the payload programs running from the directory itself. The probe -; closes that set by pid, which is why nothing here has to match an executable path. Only -; cockatrice.exe is asked: Oracle writes the card database in place and Servatrice has no -; window to ask, so both are left alone here and only ended by the explicit answer. -; -; Each instance is asked to close over the local socket it listens on, which is the same -; request as clicking its close button and reaches one whose window is hidden in the -; notification area or is sitting on its own confirmation. A client from before that socket -; is asked with WM_CLOSE on its own window instead, and a visible window is brought forward -; first. Either way the close is answered, not obeyed - Cockatrice asks before it quits while -; a card database update is running, a game is open or a deck is unsaved - and that question -; is only useful if it is in front of the installer instead of behind it. -; -; The probe waits up to 30 s for the processes it asked to exit, so the check that follows -; measures a settled directory rather than sleeping for a fixed time. Its line goes into the -; log as it stands: it names every holder, including the ones left to the user, so a close -; that did nothing can be told apart from one that was never sent. -; -; Never a force-close here: the application decides whether to quit. Ending is a separate -; answer the guard prompt offers when a close did not free the files, spelled out with what -; it costs - see EndHolders - and it ends every program the payload installs. -Function ${prefix}CloseHolders - Call ${prefix}ExtractHolderProbe - System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")' - ; The probe waits 30 s itself. The timeout here only bounds a probe that never returns; - ; the Restart Manager call inside it is the same one the holder listing makes. - nsExec::ExecToStack /TIMEOUT=40000 '"$PLUGINSDIR\installer-holder-probe.exe" --close-holders' - Pop $R3 - Pop $R4 - ; Not the CockLog macro: this function exists twice and the uninstaller copy may only - ; Call un. functions, so the macro's two lines are spelled with the prefix here. - Push "close-holders: probe exit=$R3 [$R4]" - Call ${prefix}CockLogWrite - DetailPrint "Closing Cockatrice: $R4" -FunctionEnd - -; usage: set $GuardDir to the directory being replaced, then call this. Ends the running -; Cockatrice, Oracle and Servatrice instances the probe names. -; -; This is the only place anything is stopped without being asked to stop, so it is only -; reached after the user picked it from a prompt that says what it costs: a client that is -; still running may have an unsaved deck, a game or a settings write in flight, Oracle may -; be part-way through writing the card database, and Servatrice may be serving a game. The -; probe ends the three programs the payload installs, services included, and nothing else: -; a scanner or a program the user keeps in the directory is named and left to the user. -Function ${prefix}EndHolders - Call ${prefix}ExtractHolderProbe - System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")' - ; The probe waits up to 5 s for what it ended to be gone, so the check that follows - ; measures a settled directory instead of racing the processes it just terminated. - nsExec::ExecToStack /TIMEOUT=20000 '"$PLUGINSDIR\installer-holder-probe.exe" --end-holders' - Pop $R3 - Pop $R4 - Push "end-holders: probe exit=$R3 [$R4]" - Call ${prefix}CockLogWrite - DetailPrint "Ending Cockatrice, Oracle and Servatrice: $R4" -FunctionEnd -!macroend - -!insertmacro CockHolderProbeFunctions "" install -!insertmacro CockHolderProbeFunctions "un." uninstall - -; usage: set $GuardDir to the directory to guard, then call this. -; -; Asks Cockatrice to close once, then asks one question with one answer that can free the -; payload. The automatic close is the same request clicking the window's close button makes, -; so the common case - the client was still shutting down - never turns into a question. The -; prompt is what remains when the close cannot reach everything: Oracle writes the card -; database in place, Servatrice has no window, and a client that is sitting on its own -; confirmation needs an answer nobody can give it from here. It offers to end the programs -; the payload installs - and only those, which the text says - with what that costs (Yes), -; and its other answer stops the install (No) instead of repeating the same dialog. What -; survives an end is reported in the abort, never asked about again - ending is the last -; resort, see EndHolders. -; -; An error is always shown, in every mode. A /S install without /R has nobody at -; the keyboard, but this installer does not promise unattended runs: when an -; error needs a decision, asking for one is better than silently leaving a -; half-updated install behind. The happy path has no dialog, so a /S reinstall -; that has nothing to report stays silent. -Function GuardAppsNotRunning - !insertmacro CockLog "guard: checking $GuardDir (launcherExited=$LauncherExited)" - StrCpy $GuardCloseRounds 0 - StrCpy $GuardRecheckCount 0 - StrCpy $GuardLastHolders "" - guard_check: - Call RunHolderProbe - ${If} $GuardExit == "0" - !insertmacro CockLog "guard: nothing is holding the payload" - DetailPrint "No running Cockatrice, Oracle or Servatrice instance was found." +; usage: set $R2 to the base image name (without extension, e.g. "cockatrice", +; as accepted by Get-Process -Name), call this, result in $R0 +; (1 = running from $GuardDir, 0 = not running, 2 = could not determine) +Function IsAppRunning + ; Every caller has to set $GuardDir; an empty one would make the path check + ; below match every process that merely shares the image name, which is the + ; opposite of what this guard is for. + ${If} $GuardDir == "" + StrCpy $R0 2 Return ${EndIf} - ${If} $GuardExit == "1" - !insertmacro CockLog "guard: still in use by $GuardOutput" - ; Close it for the user first. The probe asks every instance over its local socket - ; and falls back to WM_CLOSE for an older client, so this is the same request as - ; clicking the window's close button: the application still answers its own "really - ; quit?" prompt, so nothing is lost. A second round is spent only when the listing - ; changed, which means something exited and the rest is worth asking again; repeating - ; the same request against the same processes cannot change anything. - ${If} $GuardCloseRounds < 2 - ${AndIf} $GuardOutput != $GuardLastHolders - StrCpy $GuardLastHolders $GuardOutput - IntOp $GuardCloseRounds $GuardCloseRounds + 1 - !insertmacro CockLog "guard: asking the payload programs to close (round $GuardCloseRounds)" - Call CloseHolders - Goto guard_check - ${EndIf} - DetailPrint "The install directory is in use by: $GuardOutput" - ; The close ran and did not free the files. Force-closing them is what the installer - ; can do (Yes); closing them by hand is what the user can do (No checks again, with - ; the fresh listing); Cancel stops the run and the abort dialog offers the same - ; update from the official download page. Without the middle answer a user who did - ; not want anything killed had no way forward but to abandon the update. - guard_ask: - !insertmacro CockEndRecheckPrompt "Cockatrice, Oracle or Servatrice is still using files of the installation:" $GuardOutput "installation" - MessageBox MB_YESNOCANCEL|MB_ICONEXCLAMATION|MB_DEFBUTTON2 "$GuardPrompt" IDYES guard_end IDNO guard_recheck - Goto guard_cancel - guard_recheck: - IntOp $GuardRecheckCount $GuardRecheckCount + 1 - !insertmacro CockLog "guard: user closed programs by hand, checking again (recheck $GuardRecheckCount)" - ; A fresh set gets a fresh automatic close, exactly as if the guard had just started: - ; what the user closed by hand may have been one instance of several. - StrCpy $GuardCloseRounds 0 - StrCpy $GuardLastHolders "" - Goto guard_check - guard_cancel: - !insertmacro CockLog "guard: cancelled by user after $GuardRecheckCount recheck(s)" - StrCpy $AbortReason \ - "The installation was cancelled because Cockatrice, Oracle or Servatrice is still running and holding files of the previous version.$\r$\n$\r$\n${AFTER_CLOSING_GUIDANCE}$\r$\n$\r$\nClose them and run the installer again." - ; RemoveOldPayloadLeftovers runs with the pre-uninstall backup already taken; - ; restoring here keeps this abort from stranding the only copy in %TEMP%. - Call RestoreUserDataAfterUninstall - Call AbortInstall - ; Only reached through Yes: AbortInstall does not return. - guard_end: - !insertmacro CockLog "guard: ending the payload programs on request, then checking again" - Call EndHolders - ; Measure once and report, rather than asking the same question again. Everything the - ; payload installs was ended, so what survived is a program this installer may not - ; open or does not own - another dialog offering to end it again would only repeat - ; itself. - Call RunHolderProbe - ${If} $GuardExit == "0" - !insertmacro CockLog "guard: nothing is holding the payload" + ; Pass the directory to guard to PowerShell via an environment variable so + ; it never has to be embedded into the -Command string: apostrophes in a + ; path would terminate the command early, and the path in a -like pattern + ; would treat [ and ] as wildcard characters. + System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")' + ; Exit the process explicitly so nsExec's pop reflects whether a matching + ; process was found: powershell.exe exits 0 whether or not the pipeline + ; produced objects. 0 = not running, 1 = running, anything else = could not + ; be determined (see IsProcessLookupAvailable). + ; A process is only a match when its path is readable and lives under + ; $GuardDir. A process we cannot inspect is not one of ours: the updater is + ; always started by the application it updates, so it runs in the same user + ; session, and an interactive install is elevated and can read the paths of + ; the user's own processes. Assuming a match instead would be a hard stop + ; rather than a fail-closed guard - an Oracle database instance running as + ; SYSTEM has a process named "oracle" whose path raises Access denied, and + ; that would make every silent update abort and every interactive install + ; loop on the Retry prompt with no way out but Cancel. + nsExec::ExecToLog 'powershell -NoProfile -Command "if (Get-Process -Name $\'$R2$\' -ErrorAction SilentlyContinue | Where-Object { try { $$p = $$_.Path; if ($$p) { $$p.StartsWith($$env:COCKATRICE_GUARD_DIR, [StringComparison]::OrdinalIgnoreCase) } else { $$false } } catch { $$false } } | Select-Object -First 1) { exit 1 } else { exit 0 }"' + Pop $R0 + ${If} $R0 == "0" + StrCpy $R0 0 + ${ElseIf} $R0 == "1" + StrCpy $R0 1 + ${Else} + ; powershell.exe could not be run at all (nsExec pushes the string + ; "error"), or died with an unexpected code. That is "could not + ; determine", which has to stay distinguishable from "definitely + ; running": a state that can never clear would block the installer + ; forever. IsProcessLookupAvailable warns about that case once. + StrCpy $R0 2 + ${EndIf} +FunctionEnd + +; Sets $R0 to 1 if PowerShell can be started at all, 0 if it cannot. The +; running instance check is built on top of it, so a machine where +; powershell.exe is missing, blocked by policy or refused by antivirus is +; reported once instead of being reported as "Cockatrice is running" on every +; single check. +Function IsProcessLookupAvailable + nsExec::ExecToLog 'powershell -NoProfile -Command "exit 0"' + Pop $R0 + ${If} $R0 == "0" + StrCpy $R0 1 + ${Else} + StrCpy $R0 0 + ${EndIf} +FunctionEnd + +Function CloseMatchingApps + ; gracefully ask every matching instance to close (sends WM_CLOSE) + ; + ; The filter here is deliberately the strict one: a process is only asked to + ; close when its path is readable and lives under $GuardDir. Treating a + ; process whose path cannot be read (an unrelated oracle.exe / servatrice.exe, + ; or one owned by another account running as SYSTEM) as a match would make + ; the installer close a third party application, and it runs elevated, so + ; CloseMainWindow() would succeed on it. + System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")' + nsExec::ExecToLog 'powershell -NoProfile -Command "Get-Process -Name $\'$R2$\' -ErrorAction SilentlyContinue | Where-Object { try { $$p = $$_.Path; if ($$p) { $$p.StartsWith($$env:COCKATRICE_GUARD_DIR, [StringComparison]::OrdinalIgnoreCase) } else { $$false } } catch { $$false } } | ForEach-Object { $$null = $$_.CloseMainWindow() }"' + Pop $R3 +FunctionEnd + +Function ForceCloseApps + ; stop the matching instances by PID once the grace period is over. Only + ; used for the console applications ($AllowForceClose), which have no window + ; to close and hold no unsaved user data. Same strict filter as above. + System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")' + nsExec::ExecToLog 'powershell -NoProfile -Command "Get-Process -Name $\'$R2$\' -ErrorAction SilentlyContinue | Where-Object { try { $$p = $$_.Path; if ($$p) { $$p.StartsWith($$env:COCKATRICE_GUARD_DIR, [StringComparison]::OrdinalIgnoreCase) } else { $$false } } catch { $$false } } | ForEach-Object { Stop-Process -Id $$_.Id -Force -ErrorAction SilentlyContinue }"' + Pop $R3 +FunctionEnd + +Function WaitForAppToClose + ; usage: set $R1 to the display name (e.g. "cockatrice.exe") and $R2 to the + ; base image name (e.g. "cockatrice") + Call IsAppRunning + !insertmacro CockLog "guard $R1: state=$R0 (dir=$GuardDir)" + ; Only a definite "running" is worth waiting for. "Not running" is done, and + ; "could not determine" must not keep the installer waiting on an answer it + ; will never get - but it cannot simply be waved through either, see + ; HandleUnknownAppState. + ${If} $R0 = 0 + Return + ${EndIf} + ${If} $R0 = 2 + Call HandleUnknownAppState + Return + ${EndIf} + + ${If} ${Silent} + ; ask the application to close gracefully (WM_CLOSE), then wait for it to exit + DetailPrint "Closing $R1 ..." + Call CloseMatchingApps + ; wall-clock deadline: each poll has to spawn a fresh powershell.exe, so + ; a bare loop counter would not bound the real wait time to 60 s. + System::Call 'kernel32::GetTickCount()i.s' + Pop $R8 + ck_wait_loop: + Sleep 500 + Call IsAppRunning + ${If} $R0 = 0 + !insertmacro CockLog "guard $R1: closed gracefully" + DetailPrint "$R1 closed." Return ${EndIf} - !insertmacro CockLog "guard: still in use after ending them ($GuardExit): $GuardOutput" - ${If} $GuardExit == "1" - StrCpy $AbortReason \ - "The installation was cancelled because these programs still hold files of the previous version and could not be ended:$\r$\n$\r$\n$GuardOutput$\r$\n$\r$\n${AFTER_CLOSING_GUIDANCE}$\r$\n$\r$\nClose them and run the installer again." - ${Else} - StrCpy $AbortReason \ - "The installation was cancelled because, after ending Cockatrice, Oracle and Servatrice, the installer could not check whether they still hold files of the previous version.$\r$\n$\r$\nThe check reported: $GuardOutput$\r$\n$\r$\nClose them and run the installer again." + ${If} $R0 = 2 + Call HandleUnknownAppState + Return ${EndIf} - Call RestoreUserDataAfterUninstall + System::Call 'kernel32::GetTickCount()i.s' + Pop $R9 + IntOp $R9 $R9 - $R8 + ${If} $R9 < 60000 + Goto ck_wait_loop + ${EndIf} + ; The grace period is over. Cockatrice is never force-stopped: it may be + ; showing a "really leave this game?" confirmation or holding an unsaved + ; deck, and nothing has been replaced yet, so aborting just defers the + ; update and preserves the user's data. + ${If} $AllowForceClose = 0 + !insertmacro CockLog "guard $R1: still running after 60s, aborting (no force close)" + DetailPrint "$R1 is still running after 60 s; aborting the update so that no data is lost." + StrCpy $AbortReason \ + "$R1 did not close within 60 seconds, so the installation was aborted to avoid losing unsaved data.$\r$\n$\r$\nPlease close $R1 and run the installer again." + Call AbortInstall + ${EndIf} + ; Oracle and Servatrice are console applications: CloseMainWindow() + ; returned false for them and waiting longer cannot help. They hold no + ; unsaved user data, so stop them by PID. + !insertmacro CockLog "guard $R1: still running after 60s, force-stopping" + DetailPrint "Force-stopping $R1 ..." + Call ForceCloseApps + Sleep 2000 + Call IsAppRunning + ${If} $R0 = 0 + !insertmacro CockLog "guard $R1: force-stopped" + DetailPrint "$R1 closed." + Return + ${EndIf} + ${If} $R0 = 2 + Call HandleUnknownAppState + Return + ${EndIf} + !insertmacro CockLog "guard $R1: could not be stopped, aborting" + DetailPrint "$R1 is still running after 60 s; aborting the update." + StrCpy $AbortReason \ + "$R1 could not be stopped, so the installation was aborted.$\r$\n$\r$\nPlease close $R1 and run the installer again." + Call AbortInstall + ${Else} + ck_wait_prompt: + MessageBox MB_RETRYCANCEL|MB_ICONEXCLAMATION|MB_DEFBUTTON1 \ + "$R1 is still running.$\r$\n$\r$\nPlease close it, then click Retry.$\r$\nClick Cancel to abort." \ + IDCANCEL ck_abort_install + Call IsAppRunning + ${If} $R0 = 0 + Return + ${EndIf} + ${If} $R0 = 2 + Call HandleUnknownAppState + Return + ${EndIf} + Goto ck_wait_prompt + ck_abort_install: + Abort + ${EndIf} +FunctionEnd + +; A process lookup came back "could not determine": powershell.exe ran but the +; answer was not usable, e.g. the process could not be inspected at all. That +; state is fine to carry on from when the user is at the keyboard - they can +; close the applications by hand and the stale-runtime check still stands +; between a failed guard and a mismatched install. +; +; It is not fine on a silent upgrade. There the whole point of this guard is to +; know that nothing holds locks on the files about to be replaced, and this is +; exactly the state where that is unknown, with no one to click a dialog: the +; old code returned and let the install carry on into the mixed Qt DLL set that +; made the next start fail. Cancel the update instead; the next attempt runs +; with the applications closed and succeeds. +Function HandleUnknownAppState + ${If} $ReinstallMode = 1 + StrCpy $AbortReason \ + "The installation was cancelled because it could not determine whether Cockatrice is still running.$\r$\n$\r$\nPlease close Cockatrice, Oracle and Servatrice and run the installer again." Call AbortInstall ${EndIf} - ; Not an answer at all: the probe was not configured, the Restart Manager - ; call failed and no payload program runs from the directory, a payload - ; program refuses to be replaced but no lookup could name it (it may run - ; under another account), or the probe timed out. Carrying on from that is a - ; decision, so it is asked for rather than assumed - the stale-runtime check - ; after the purge still stands between a failed guard and a mismatched - ; install, but it only fires after files have been replaced, and on an - ; upgrade the launcher asked for nobody is watching the installer's exit - ; code. Retry asks the probe again (a Restart Manager that was busy answers - ; the second time, and a program the user has closed frees its file), Ignore - ; installs anyway, Abort cancels instead of writing a payload next to files - ; the guard could not rule out. - !insertmacro CockLog "guard: the holder probe could not answer ($GuardExit): $GuardOutput" - DetailPrint "Could not confirm whether Cockatrice is still running: $GuardOutput" - MessageBox MB_ABORTRETRYIGNORE|MB_ICONEXCLAMATION|MB_DEFBUTTON1 \ - "This installer could not confirm whether Cockatrice, Oracle or Servatrice is still running.$\r$\n$\r$\nThe check reported: $GuardOutput$\r$\n$\r$\nClose them, then choose Retry to check again. Choose Ignore to install anyway, or Abort to cancel the installation." \ - IDRETRY guard_check IDABORT guard_unknown_abort - !insertmacro CockLog "guard: continuing without an answer from the probe" - Return - guard_unknown_abort: - StrCpy $AbortReason \ - "The installation was cancelled because it could not confirm whether Cockatrice, Oracle or Servatrice is still running.$\r$\n$\r$\nThe check reported: $GuardOutput$\r$\n$\r$\nPlease close them and run the installer again." - ; Same reason as the cancel path above: this abort can be reached from the - ; leftover-removal guard, after the backup was taken. - Call RestoreUserDataAfterUninstall - Call AbortInstall + DetailPrint "Could not determine whether $R1 is running; close Cockatrice, Oracle and Servatrice manually if the installation reports a problem." + ${IfNot} ${Silent} + MessageBox MB_OK|MB_ICONEXCLAMATION \ + "This installer could not determine whether $R1 is still running.$\r$\n$\r$\nPlease close Cockatrice, Oracle and Servatrice manually before continuing." + ${EndIf} FunctionEnd Function EnsureAppsNotRunning @@ -1337,155 +894,103 @@ FunctionEnd ; Aborts the installation, showing $AbortReason to the user. ; -; Every abort goes through here so no failure ends silently: the in-app updater -; runs this installer silent and detached and never looks at its exit code, and -; a /S install without /R has nobody watching its exit code either, so a plain -; Abort would leave the user with no update and no explanation. An error always -; shows its dialog, in every mode; a silent run stays silent only for as long -; as nothing goes wrong. The log path is part of the dialog, so a report can be -; acted on without asking the user to find the file. +; Every abort goes through here so the unattended case is handled the same way +; everywhere: the in-app updater runs this installer silent and detached and +; never looks at its exit code, so a plain Abort would leave the user with no +; update and no explanation, while a /S install without /R is unattended - by a +; package manager, for instance - and a message box there would block forever. +; Only the first of those two gets a dialog; the second reports through the log +; and the exit code. Function AbortInstall !insertmacro CockLog "ABORT: $AbortReason" !insertmacro CockLog "full installer log: $InstallLogPath" - !insertmacro CockLog "manual update: ${LATEST_RELEASE_URL}" - ; The reason names what stopped the run; the manual-update paragraph is the way forward - ; that does not need this installer to free anything. Shown in every mode, exactly like - ; the reason, because a /R upgrade has nobody watching its exit code. - MessageBox MB_OK|MB_ICONEXCLAMATION "$AbortReason$\r$\n$\r$\n${MANUAL_UPDATE_GUIDANCE}$\r$\n$\r$\nA detailed installer log was written to:$\r$\n$InstallLogPath" - ; Offer the page as well: copying a URL out of a message box is a step users skip. - ; explorer.exe hands the URL to the already-running shell, so the browser opens at the - ; user's integrity level rather than as the elevated installer. - MessageBox MB_YESNO|MB_ICONQUESTION|MB_DEFBUTTON2 "Open the download page now?$\r$\n$\r$\n${LATEST_RELEASE_URL}" IDNO abort_no_link - !insertmacro CockLog "manual update: opening the download page" - Exec '"$WINDIR\explorer.exe" "${LATEST_RELEASE_URL}"' - abort_no_link: - ; Offer to send the installer log to Sentry so aborted installs can be diagnosed. - ; Only a package built with a baked DSN offers to send, so the prompt can only appear - ; when a send can actually go out. A preprocessor guard rather than a runtime test: a - ; package built without a DSN compiles the block out entirely, so it cannot even carry - ; the helper or its baked endpoint. No /SD: the abort is exactly the case the offer - ; exists for, including on the silent /R upgrade. - !if "@CPACK_SENTRY_ENABLED@" == "1" - MessageBox MB_YESNO|MB_ICONQUESTION "Send the installer log to the developers to help fix this? The log contains file paths from this machine, which may include your Windows user name." IDYES sentry_send - !insertmacro CockLog "sentry send: declined by user" - Goto sentry_done - sentry_send: - ; The abort reason is already in the log file (CockLog writes "ABORT: ..." as the - ; first thing AbortInstall does), so the helper reads it from there. Nothing - ; reason-shaped crosses the command line, which keeps quoting trivial and needs - ; no NSIS string functions. - InitPluginsDir - ; Embedded by the same relative path that resolves NSIS.definitions.nsh above (both are - ; configure_file'd into the build root, which is three levels up from project.nsi). An - ; absolute CMake path here carries forward slashes, which makensis does not treat as - ; absolute and so fails to find the file at package time. - File "/oname=$PLUGINSDIR\send-installer-log.ps1" "..\..\..\SendInstallerLog.ps1" - ; The send can take up to the /TIMEOUT below, so put something on the details pane - ; the moment the user says yes rather than leaving the window blank until it returns. - DetailPrint "Sending installer log..." - nsExec::ExecToStack /TIMEOUT=60000 'powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "$PLUGINSDIR\send-installer-log.ps1" -LogPath "$InstallLogPath"' - Pop $SentryResult - Pop $SentryOutput - ; $SentryOutput is a single line by construction: the helper flattens its message - ; and this log file is uploaded verbatim, so a stray newline must not be able to - ; forge extra lines into the artifact we send. - !insertmacro CockLog "sentry send: result=$SentryResult output=$SentryOutput" - ${If} $SentryResult == "0" - MessageBox MB_OK|MB_ICONINFORMATION "The installer log was sent. Thank you for helping improve Cockatrice!" - ${Else} - MessageBox MB_OK|MB_ICONEXCLAMATION "The installer log could not be sent. You can still share it manually; it was written to:$\r$\n$InstallLogPath" - ${EndIf} - sentry_done: - !endif + ; Show a dialog whenever someone is at the keyboard to see it: an interactive + ; install, or the in-app updater's /R upgrade. The /R upgrade is silent, but + ; the client that launched the installer has already quit, so the user is + ; still there. Only a /S install without /R is attended by a package manager + ; and must go straight to the exit code. + ${If} $ReinstallMode = 1 + ${OrIfNot} ${Silent} + MessageBox MB_OK|MB_ICONEXCLAMATION "$AbortReason$\r$\n$\r$\nA detailed installer log was written to:$\r$\n$InstallLogPath" + ; Offer to send the installer log to Sentry so aborted installs can be diagnosed. This block already runs + ; only when someone is at the keyboard (attended install, or the updater's /R upgrade whose launcher has + ; quit), so a silent or package-manager install never reaches the prompt and its behavior is unchanged. + ; Only a package built with a baked DSN offers to send, so the prompt can only appear when a send + ; can actually go out. A preprocessor guard rather than a runtime test: a package built without a + ; DSN compiles the block out entirely, so it cannot even carry the helper or its baked endpoint. + ; No /SD here: a /SD would suppress this prompt on the silent /R upgrade, which is exactly the + ; attended-update case this offer exists for. The runtime gate above (ReinstallMode/Silent) is the + ; hang protection — a package-manager /S install without /R never reaches this box, matching the + ; two sibling MessageBox sites in this file. + !if "@CPACK_SENTRY_ENABLED@" == "1" + MessageBox MB_YESNO|MB_ICONQUESTION "Send the installer log to the developers to help fix this? The log contains file paths from this machine, which may include your Windows user name." IDYES sentry_send + !insertmacro CockLog "sentry send: declined by user" + Goto sentry_done + sentry_send: + ; The abort reason is already in the log file (CockLog writes "ABORT: ..." as the + ; first thing AbortInstall does), so the helper reads it from there. Nothing + ; reason-shaped crosses the command line, which keeps quoting trivial and needs + ; no NSIS string functions. + InitPluginsDir + ; Embedded by the same relative path that resolves NSIS.definitions.nsh above (both are + ; configure_file'd into the build root, which is three levels up from project.nsi). An + ; absolute CMake path here carries forward slashes, which makensis does not treat as + ; absolute and so fails to find the file at package time. + File "/oname=$PLUGINSDIR\send-installer-log.ps1" "..\..\..\SendInstallerLog.ps1" + ; The send can take up to the /TIMEOUT below, so put something on the details pane + ; the moment the user says yes rather than leaving the window blank until it returns. + DetailPrint "Sending installer log..." + nsExec::ExecToStack /TIMEOUT=60000 'powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "$PLUGINSDIR\send-installer-log.ps1" -LogPath "$InstallLogPath"' + Pop $SentryResult + Pop $SentryOutput + ; $SentryOutput is a single line by construction: the helper flattens its message + ; and this log file is uploaded verbatim, so a stray newline must not be able to + ; forge extra lines into the artifact we send. + !insertmacro CockLog "sentry send: result=$SentryResult output=$SentryOutput" + ${If} $SentryResult == "0" + MessageBox MB_OK|MB_ICONINFORMATION "The installer log was sent. Thank you for helping improve Cockatrice!" + ${Else} + MessageBox MB_OK|MB_ICONEXCLAMATION "The installer log could not be sent. You can still share it manually; it was written to:$\r$\n$InstallLogPath" + ${EndIf} + sentry_done: + !endif + ${EndIf} SetErrorLevel 1 Abort FunctionEnd -; usage: set $GuardDir and $WaitPid, then call this. Asks the one process the installer -; waits on to close and sets $R3 to what it could do: 1 = a window exists and was asked to -; close (or is waiting on a question of its own), 4 = it has no window a user could answer, -; anything else = the probe could not answer. Only 4 lets the wait act on its own; every -; other code keeps the user in the decision, because the installer holds a live handle and -; "the probe did not find it" is not the same answer as "it is gone". -; -; The guard's CloseHolders answers the same question for every holder and leaves a -; windowless instance to the user, which is right there: the prompt names it. The launcher -; is the one process nobody can be asked about - the in-app updater runs the installer -; silent and detached, and a client that has already hidden its window has answered its -; own prompts - so this answer has to say whether there was a window to ask at all. -Function CloseLauncher - Call ExtractHolderProbe - System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")' - System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_LAUNCHER_PID", "$WaitPid")' - ; The probe waits 30 s itself when it posts a close. The timeout here only bounds a - ; probe that never returns; the Restart Manager call the other modes make is not - ; reached in this one. - nsExec::ExecToStack /TIMEOUT=40000 '"$PLUGINSDIR\installer-holder-probe.exe" --close-launcher' - Pop $R3 - Pop $R4 - Push "close-launcher: probe exit=$R3 [$R4]" - Call CockLogWrite - DetailPrint "Closing Cockatrice: $R4" -FunctionEnd - ; Waits for the application that launched this installer to exit, using the /PID= ; it passed. Sets $LauncherExited to 1 when that process is provably gone. ; ; This is the authority for the one question the rest of the installer can only ; approximate: is the process that started us still holding its Qt DLLs mapped? -; A mapped module cannot be deleted, so an answer of "nothing is holding them" -; obtained from file handles is what allows the update to write a new payload on +; A mapped module cannot be deleted, so an answer of "not running" obtained by +; enumerating process names is what allows the update to write a new payload on ; top of files it could not remove. Waiting on a real handle has no such gap, -; needs no process enumeration at all, and is unaffected by a process the holder -; probe cannot inspect. +; needs no powershell.exe, and is unaffected by a process whose path cannot be +; read - the case the name based check has to treat as "not ours" so it never +; closes a third party application. ; ; Sets $LauncherExited to 0 when no usable /PID= was given, which is the older ; client that does not pass one; the caller then has no authority available and -; falls back to the holder probe. -; -; A launcher that survives the first grace is asked to close by the probe, and the -; probe also answers whether there was a window a close could reach. A client hides -; its window the moment its own close is accepted, so a launcher without one has -; already answered its own prompts and is stuck in shutdown: no dialog can reach it -; there, and the wait ends it after one more grace instead of asking the user a -; question whose "keep waiting" answer can never change anything. +; falls back to the name based guard. Function WaitForLauncherToExit StrCpy $LauncherExited 0 - StrCpy $WaitWindowless 0 !insertmacro CockLog "wait-launcher: pid='$WaitPid'" ${If} $WaitPid == "" !insertmacro CockLog "wait-launcher: no pid given, falling back to name check" Return ${EndIf} + ; PROCESS_SYNCHRONIZE is the only access this needs - enough to wait on the + ; handle, and the least likely to be refused for a reason that has nothing to + ; do with the upgrade. + ; ; System.dll maps its output registers so that .rN writes $N and .RN writes ; $RN. The caller reads $R0/$R1, so the uppercase .RN form is required; the ; original .r0/.r1 wrote $0/$1 and left $R1 empty, which made the "did not ; exit" test below compare "" against "0" and abort on every /PID= upgrade. - ; Reopened on every round below, so a launcher that exited in the meantime answers - ; "already exited" instead of being waited on again. - ; - ; PROCESS_TERMINATE rides along with PROCESS_SYNCHRONIZE so that the "end it" - ; answer below acts on this very handle: the pid looked up again after a dialog - ; that can stay open as long as the user takes would name whatever process holds - ; that pid by then, and a handle cannot be handed to another process. - ; - ; When those rights are refused - a launcher started by another account, a - ; protected process - the wait still has to happen, so the ask is repeated with - ; synchronize access alone and $WaitCanEnd records that ending is not on offer. - ; Reporting "already exited" instead would be the worse answer: the payload would - ; be written over a live process, which is what this wait exists to prevent. - ; - ; Ten seconds, not the full grace: a client that started an update exits in well - ; under a second, so at ten seconds it is either flushing a card database or - far - ; more often - sitting on its own "quit anyway?" confirmation with nobody looking at - ; it. Silence for a minute is what makes that read as a hung installer. The full - ; grace is spent after the first question, so a user who answers Cockatrice's - ; confirmation is not asked again while they do it. - StrCpy $WaitSlice 10000 - wait_launcher_open: ClearErrors - StrCpy $WaitCanEnd 1 - System::Call 'kernel32::OpenProcess(i 0x00100001, i 0, i $WaitPid) p.R0' + System::Call 'kernel32::OpenProcess(i 0x00100000, i 0, i $WaitPid) p.R0' ; OpenProcess returns a null handle for a process that is already gone. The ; System plugin does not raise the NSIS error flag from a called function's ; return value, so ${Errors} alone never saw that case and the null handle was @@ -1494,118 +999,27 @@ Function WaitForLauncherToExit ${If} ${Errors} StrCpy $R0 0 ${EndIf} - ${If} $R0 == 0 - StrCpy $WaitCanEnd 0 - ClearErrors - System::Call 'kernel32::OpenProcess(i 0x00100000, i 0, i $WaitPid) p.R0' - ${If} ${Errors} - StrCpy $R0 0 - ${EndIf} - ${EndIf} ${If} $R0 == 0 ; The launcher is gone - OpenProcess cannot open a dead process - which is - ; exactly the state the wait was for. A handle refused for want of the terminate - ; right is not this case; the synchronize-only ask above covers it. + ; exactly the state the wait was for. !insertmacro CockLog "wait-launcher: already exited" DetailPrint "The application that started this installer has already exited." StrCpy $LauncherExited 1 Return ${EndIf} DetailPrint "Waiting for the application that started this installer to exit ..." + ; Same 60 s grace period the name based check uses, so a client that is slow + ; to flush its card database does not get less room than it used to. ; WAIT_OBJECT_0 is 0; WAIT_TIMEOUT is 258 and a failed wait reports 0xFFFFFFFF. - System::Call 'kernel32::WaitForSingleObject(p R0, i $WaitSlice) i.R1' + System::Call 'kernel32::WaitForSingleObject(p R0, i 60000) i.R1' + System::Call 'kernel32::CloseHandle(p R0)' ${If} $R1 != 0 - ; $R0 is deliberately still open: it is the process that timed out, and the answer - ; below has to act on that process and not on whatever $WaitPid names by then. - !insertmacro CockLog "wait-launcher: did not exit within $WaitSlice ms (wait=$R1, canEnd=$WaitCanEnd)" - DetailPrint "The application that started this installer did not exit in time." - ; Ask the launcher itself before asking the user. A client that has already hidden - ; its window has answered its own prompts and is stuck in shutdown: there is nothing - ; left a dialog could reach, and the "ask it to close again" answer the dialog used - ; to offer could only repeat itself until the user cancelled. - StrCpy $GuardDir $INSTDIR - Call CloseLauncher - ; $R3 = what the probe could do (see CloseLauncher), $R4 = its line, already logged. - ; A process that answered the close during the probe's own wait is gone by now. - System::Call 'kernel32::WaitForSingleObject(p R0, i 0) i.R1' - ${If} $R1 == 0 - System::Call 'kernel32::CloseHandle(p R0)' - StrCpy $R0 0 - Goto wait_launcher_open - ${EndIf} - ${If} $R3 == "4" - ; Nothing a user could answer: the window is gone, so the launcher is past its - ; own prompts and stuck in shutdown. It cannot be asked, and it is not going to - ; ask the user anything either, so the dialog below would be unanswerable. - ${If} $WaitCanEnd == 1 - ${If} $WaitWindowless == 0 - ; Hiding the window is what the client does when its close is accepted, - ; so what remains is the shutdown it already started: one full grace for - ; that, without a question nobody could act on. - StrCpy $WaitWindowless 1 - StrCpy $WaitSlice 60000 - !insertmacro CockLog "wait-launcher: no window to ask, giving it one more minute" - System::Call 'kernel32::CloseHandle(p R0)' - StrCpy $R0 0 - Goto wait_launcher_open - ${EndIf} - ${If} $WaitWindowless == 1 - ; It did not use that minute either. Ending it is what the dialog's Yes - ; answer would have done, and there is no question left to put to the - ; user: the instance lost its window before this round started. - StrCpy $WaitWindowless 2 - !insertmacro CockLog "wait-launcher: still no window to ask; ending the launcher" - DetailPrint "The application that started this installer could not be asked to close; ending it." - System::Call 'kernel32::TerminateProcess(p R0, i 1) i.R2' - !insertmacro CockLog "wait-launcher: ending the launcher returned $R2" - System::Call 'kernel32::CloseHandle(p R0)' - StrCpy $R0 0 - Goto wait_launcher_open - ${EndIf} - ${EndIf} - ${EndIf} - ; A window exists, or the launcher cannot be ended from here: the three answers - ; below. "Carry on anyway" is not one of them - a live process has its image and its - ; Qt DLLs mapped, so writing the payload over them is the broken install this wait - ; exists to prevent. Yes ends the launcher, Cancel cancels, and No keeps waiting: - ; the close and the bring-forward just happened above. - MessageBox MB_YESNOCANCEL|MB_ICONEXCLAMATION|MB_DEFBUTTON2 \ - "Cockatrice has not finished closing, so the installation is paused rather than replace files it is still using.$\r$\n$\r$\nIt may be waiting for you to answer its own question about an unsaved deck, an open game or a card database update.$\r$\n$\r$\nChoose Yes to end it now - an unsaved deck, a running game or an unwritten setting is lost.$\r$\n$\r$\nChoose No to keep waiting, or Cancel to cancel the installation." \ - IDYES wait_launcher_end IDNO wait_launcher_retry - !insertmacro CockLog "wait-launcher: cancelled by user" + !insertmacro CockLog "wait-launcher: did not exit within 60s (wait=$R1)" + DetailPrint "The application that started this installer did not exit within 60 s." StrCpy $AbortReason \ "The application that started this update has not exited yet, so the installation was cancelled rather than replace files it is still using.$\r$\n$\r$\nPlease close Cockatrice and run the installer again." Call AbortInstall - ; Only reached through Yes or No: AbortInstall does not return. - wait_launcher_end: - ; End it through the handle waited on above, and through nothing else: no pid is - ; looked up again, so a process that inherited the number in the meantime - or a - ; /PID= that was not the launcher to begin with - cannot be hit by this. - !insertmacro CockLog "wait-launcher: ending the launcher on request" - ${If} $WaitCanEnd == 0 - !insertmacro CockLog "wait-launcher: the launcher could not be opened to end it, asking it to close instead" - Goto wait_launcher_retry - ${EndIf} - System::Call 'kernel32::TerminateProcess(p R0, i 1) i.R3' - System::Call 'kernel32::CloseHandle(p R0)' - StrCpy $R0 0 - !insertmacro CockLog "wait-launcher: ending the launcher returned $R3" - Goto wait_launcher_open - wait_launcher_retry: - System::Call 'kernel32::CloseHandle(p R0)' - StrCpy $R0 0 - ; The question has been asked and answered once; from here the wait is the full - ; grace again, so a slow flush or a confirmation being answered does not turn - ; into a stream of prompts. The close itself and the bring-forward already - ; happened just before the dialog, so this only waits. The windowless grace starts - ; over as well: "keep waiting" must not be answered by ending the process a minute - ; later without asking again. - StrCpy $WaitWindowless 0 - StrCpy $WaitSlice 60000 - Goto wait_launcher_open ${EndIf} - System::Call 'kernel32::CloseHandle(p R0)' - StrCpy $R0 0 !insertmacro CockLog "wait-launcher: launcher exited" DetailPrint "The application that started this installer has exited." StrCpy $LauncherExited 1 @@ -1629,6 +1043,57 @@ Function EnsureOldInstallNotRunning Call GuardAppsNotRunning FunctionEnd +Function GuardAppsNotRunning + ; usage: set $GuardDir to the directory to guard, then call this + Call IsProcessLookupAvailable + !insertmacro CockLog "guard: powershell available=$R0 dir=$GuardDir launcherExited=$LauncherExited" + ${If} $R0 = 0 + ; Without PowerShell we cannot tell whether a running instance is + ; holding locks on the files we are about to replace. + !insertmacro CockLog "guard: powershell unavailable, skipping instance check" + DetailPrint "Could not start powershell.exe; skipping the running instance check. Close Cockatrice, Oracle and Servatrice manually if the installation reports a problem." + ; On a silent upgrade that used to be a warning and nothing more, which + ; left the very process that launched the installer free to go on + ; replacing files it still had mapped - the one outcome this guard exists + ; to prevent - and then writing the new payload on top of them. Cancel + ; instead. An interactive install can still continue: the user is at the + ; keyboard, and refusing outright would leave them with an installer that + ; can never complete. + ; + ; Unless the launcher has already been waited on directly: WaitForLauncherToExit + ; needs no powershell.exe, so when it has run, Cockatrice is known to be + ; gone and the lookup failing to run costs nothing but the Oracle and + ; Servatrice checks. An older client that passes no /PID= leaves no such + ; authority, and there the upgrade is cancelled as before. + ${If} $LauncherExited != 1 + ${AndIf} $ReinstallMode = 1 + StrCpy $AbortReason \ + "The installation was cancelled because it could not check whether Cockatrice is still running, because powershell.exe could not be started.$\r$\n$\r$\nPlease close Cockatrice, Oracle and Servatrice and run the installer again." + Call AbortInstall + ${EndIf} + ; The user has to be told to close the applications by hand, but a /S + ; install without /R is unattended and would block on this forever. + ${If} $ReinstallMode = 1 + ${OrIfNot} ${Silent} + MessageBox MB_OK|MB_ICONEXCLAMATION "This installer could not check whether Cockatrice is still running, because powershell.exe could not be started.$\r$\n$\r$\nPlease close Cockatrice, Oracle and Servatrice manually before continuing." + ${EndIf} + Return + ${EndIf} + StrCpy $R1 "cockatrice.exe" + StrCpy $R2 "cockatrice" + ; a GUI application with unsaved user data: graceful close only + StrCpy $AllowForceClose 0 + Call WaitForAppToClose + StrCpy $R1 "oracle.exe" + StrCpy $R2 "oracle" + StrCpy $AllowForceClose 1 + Call WaitForAppToClose + StrCpy $R1 "servatrice.exe" + StrCpy $R2 "servatrice" + StrCpy $AllowForceClose 1 + Call WaitForAppToClose +FunctionEnd + ; Sets $R0 to 1 if $INSTDIR looks like an existing Cockatrice install. Used to ; guard the stale-runtime-DLL purge so a first-time install into a pre-existing ; directory with an unrelated "Plugins" folder is never touched, while an @@ -1706,351 +1171,142 @@ Function CheckStaleRuntimeMask ${EndIf} FunctionEnd -; Clears the read-only attribute of everything the purge is about to delete. -; -; Delete and RMDir only set the error flag: a read-only file is left on disk -; untouched, and the retry loop then cannot make progress however long it waits, -; because the file is not held - it is protected. The abort diagnostics have a -; branch for exactly that ("read-only, which stops Delete"), so a survivor that -; is both free and read-only was a dead end until now. This runs on every -; attempt, so an attribute set again between attempts is cleared as well. -; SetFileAttributes has no wildcard support, hence the walks. -Function ClearReadOnlyPurgeTargets - FindFirst $StaleFindHandle $StaleFindName "$INSTDIR\*.dll" - ${If} $StaleFindHandle != "" - crp_dll_loop: - SetFileAttributes "$INSTDIR\$StaleFindName" NORMAL - ClearErrors - FindNext $StaleFindHandle $StaleFindName - IfErrors 0 crp_dll_loop - FindClose $StaleFindHandle - ${EndIf} - StrCpy $R6 "$INSTDIR\Plugins" - Call ClearReadOnlyTree -FunctionEnd - -; usage: set $R6 to a directory. Clears the read-only attribute of every entry -; below it, so RMDir /r is not stopped by a single file. Saves and restores the -; registers it works in, because it recurses into itself. -Function ClearReadOnlyTree - Push $R6 - Push $R7 - Push $R8 - FindFirst $R7 $R8 "$R6\*.*" - ${If} $R7 != "" - crt_loop: - ${If} $R8 != "." - ${AndIf} $R8 != ".." - SetFileAttributes "$R6\$R8" NORMAL - ; Only an entry with contents has anything below it that the purge - ; would fail on; an empty directory's own attribute is already - ; cleared above and there is nothing left to descend into. - ${If} ${FileExists} "$R6\$R8\*.*" - Push $R6 - StrCpy $R6 "$R6\$R8" - Call ClearReadOnlyTree - Pop $R6 - ${EndIf} - ${EndIf} - ClearErrors - FindNext $R7 $R8 - IfErrors 0 crt_loop - FindClose $R7 - ${EndIf} - Pop $R8 - Pop $R7 - Pop $R6 -FunctionEnd - -; Removes the Plugins directory and all top-level DLLs from $INSTDIR, then -; retries up to two more times (2 s apart) when files remain locked, so that -; transient holders such as antivirus scanners have a chance to release their -; handles before the installer gives up. Three attempts total. The read-only -; attribute is cleared before each attempt: a protected file is not a held one -; and would never clear on its own. -; Sets $R0 to 0 when the purge is clean, or to 1 when stale DLLs remain -; after all attempts. -Function RetryDllPurge - StrCpy $R5 0 ; counts retries after the initial attempt - rdp_loop: - Call ClearReadOnlyPurgeTargets - RMDir /r "$INSTDIR\Plugins" - Delete "$INSTDIR\*.dll" - Call HasStaleRuntimeFiles - ; $R0 = 0: all clear, nothing left to do. - ${If} $R0 = 0 +; Uninstaller copies of the same routines (the uninstaller gets its own +; function set compiled in, it cannot call the installer functions). +Function un.IsAppRunning + ; An unset $GuardDir would match every process that merely shares the image + ; name, see IsAppRunning in the installer. + ${If} $GuardDir == "" + StrCpy $R0 2 Return ${EndIf} - IntOp $R5 $R5 + 1 - ; Allow two retries after the initial attempt ($R5 is the retry counter, - ; so the loop runs at most 3 times: attempt + retry 1 + retry 2). - ${If} $R5 > 2 - !insertmacro CockLog "RetryDllPurge: stale files remain after 3 attempts, giving up" - Return ; $R0 is already 1 - ${EndIf} - !insertmacro CockLog "RetryDllPurge: retry $R5 of 2 after stale purge, sleeping 2 s" - Sleep 2000 - Goto rdp_loop -FunctionEnd ; RetryDllPurge -; Turns a Win32 error code into the wording that goes into the log. -; usage: set $0 to the code, result in $R0. -; -; These are Windows' own names for the codes and deliberately nothing more. A -; code on its own cannot say whether an upgrade was blocked by a running instance -; or by a permission: deleting a file whose image is mapped by another process -; surfaces as ACCESS_DENIED, exactly like a file whose ACL denies delete does. -; LogProcessesUsingInstallDir is what separates the two, which is why it runs -; right after this and why the two are read together. -Function GetWin32ErrorText - ${If} $0 = 2 - StrCpy $R0 "already gone" - ${ElseIf} $0 = 3 - StrCpy $R0 "path not found" - ${ElseIf} $0 = 5 - StrCpy $R0 "access denied" - ${ElseIf} $0 = 19 - StrCpy $R0 "write-protected media" - ${ElseIf} $0 = 32 - StrCpy $R0 "sharing violation" - ${ElseIf} $0 = 33 - StrCpy $R0 "lock violation" - ${ElseIf} $0 = 1224 - StrCpy $R0 "file is mapped in memory" + ; Pass the directory to guard to PowerShell via an environment variable so + ; it never has to be embedded into the -Command string: apostrophes in a + ; path would terminate the command early, and the path in a -like pattern + ; would treat [ and ] as wildcard characters. + System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")' + ; Exit the process explicitly so nsExec's pop reflects whether a matching + ; process was found: powershell.exe exits 0 whether or not the pipeline + ; produced objects. 0 = not running, 1 = running, anything else = could not + ; be determined (see un.IsProcessLookupAvailable). + ; Strict match: a readable path under $GuardDir, same as in the installer. + nsExec::ExecToLog 'powershell -NoProfile -Command "if (Get-Process -Name $\'$R2$\' -ErrorAction SilentlyContinue | Where-Object { try { $$p = $$_.Path; if ($$p) { $$p.StartsWith($$env:COCKATRICE_GUARD_DIR, [StringComparison]::OrdinalIgnoreCase) } else { $$false } } catch { $$false } } | Select-Object -First 1) { exit 1 } else { exit 0 }"' + Pop $R0 + ${If} $R0 == "0" + StrCpy $R0 0 + ${ElseIf} $R0 == "1" + StrCpy $R0 1 ${Else} - StrCpy $R0 "windows error" + ; powershell.exe could not be run at all (nsExec pushes the string + ; "error"), or died with an unexpected code. That is "could not + ; determine", which has to stay distinguishable from "definitely + ; running": a state that can never clear would block the uninstaller + ; forever. un.IsProcessLookupAvailable warns about that case once. + StrCpy $R0 2 ${EndIf} FunctionEnd -; Records which runtime files the purge left behind, what Windows said about each -; one, and which processes are using the install directory. -; -; Diagnostic only, and only on the abort path: it deletes nothing, so it can never -; turn a failed upgrade into a half-removed old install, and it decides nothing - -; the abort that called it is already decided. It exists because NSIS Delete/RMDir -; only set the error flag: without this, a report of "files of a previous version -; survived the purge" said which files were present but never which one was held, -; and never whether the cause was a lock or a permission. -; -; Each survivor is opened for DELETE access and then asked the question DeleteFile -; asks: SetFileInformationByHandle(FileDispositionInfo) marks it for deletion on -; close, and the mark is cleared again right away, so nothing is deleted (and if -; that cancel ever failed, the file would go when the handle closes - a file the -; purge already tried to delete, and the line says so). That separates the three -; causes a failed Delete cannot tell apart: another handle that does not share -; delete refuses the mark, a read-only attribute refuses it, and a file nobody -; holds accepts it and would have been deleted. The mark is read through ?e, so a -; refusal reports Windows' own code; clearing LastError first keeps a zero code -; meaning "accepted". The wording below reports what Windows said and never claims -; a cause the code cannot prove. Which process is holding a survivor, if any, is -; what the holders line after the loop answers. -Function LogStaleRuntimeDiagnostics - FindFirst $StaleFindHandle $StaleFindName "$INSTDIR\*.dll" - ${If} $StaleFindHandle != "" - stale_diag_loop: - ; ClearErrors tells a plugin failure apart from an API failure: the System - ; plugin sets the error flag only when it could not make the call at all, - ; in which case it pushes its own message instead of the call's value and - ; the pop below must not run. - ClearErrors - System::Call 'kernel32::SetLastError(i 0)' - System::Call 'kernel32::CreateFileW(w "$INSTDIR\$StaleFindName", i 0x00010000, i 0, p 0, i 3, i 0, p 0) p.R1 ?e' - ${If} ${Errors} - StrCpy $R0 "the delete probe could not be run" - ${Else} - Pop $R2 - ${If} $R2 = 0 - ; The open worked, which says nothing about Delete: a DLL a running - ; process has mapped opens for DELETE and still refuses DeleteFile, and - ; a read-only file does the same until its attribute is cleared. Ask - ; Windows the question the purge actually asks - mark the file for - ; deletion on close, then cancel that again. This is the same request - ; DeleteFile makes on a temporary handle, so its answer says whether a - ; delete would succeed now. - ClearErrors - System::Call '*(&i1 1) p.R4' - System::Call 'kernel32::SetFileInformationByHandle(p R1, i 4, p R4, i 1) i.R5 ?e' - ${If} ${Errors} - StrCpy $R0 "a delete-access open now succeeds, but Windows could not be asked whether it would delete it" - ${Else} - Pop $R6 - ${If} $R5 != 0 - ; Accepted: cancel the mark again, so this stays a question and - ; does not turn into the deletion it is asking about. A cancel - ; that fails leaves the file to go when the handle closes, which - ; is what the purge wanted anyway; the line says so. - ClearErrors - System::Call '*(&i1 0) p.R4' - System::Call 'kernel32::SetFileInformationByHandle(p R1, i 4, p R4, i 1) i.R5 ?e' - ${If} ${Errors} - StrCpy $R0 "a delete would succeed now, but the check could not undo the mark it set" - ${Else} - Pop $R6 - ${If} $R5 != 0 - StrCpy $R0 "a delete would succeed now - the earlier failure was transient" - ${Else} - StrCpy $R0 "a delete would succeed now, and it is marked for deletion from here on" - ${EndIf} - ${EndIf} - ${Else} - StrCpy $0 $R6 - Call GetWin32ErrorText - StrCpy $R0 "a delete-access open now succeeds, but Windows refused to mark it for deletion: win32 error $R6: $R0" - ${EndIf} - ${EndIf} - System::Call 'kernel32::CloseHandle(p R1)' - ; The read-only attribute is a cause on its own, and the purge clears it - ; before every attempt, so a survivor that is read-only had it set again. - System::Call 'kernel32::GetFileAttributesW(w "$INSTDIR\$StaleFindName") i.R3' - ${If} $R3 != -1 - IntOp $R3 $R3 & 1 - ${If} $R3 = 1 - StrCpy $R0 "$R0; it is read-only again" - ${EndIf} - ${EndIf} - ${Else} - StrCpy $0 $R2 - Call GetWin32ErrorText - StrCpy $R0 "win32 error $R2: $R0" - ${EndIf} - ${EndIf} - !insertmacro CockLog "stale-runtime: '$StaleFindName' is still present: $R0" - ClearErrors - FindNext $StaleFindHandle $StaleFindName - IfErrors 0 stale_diag_loop - FindClose $StaleFindHandle - ${EndIf} - Call LogProcessesUsingInstallDir -FunctionEnd - -; Names the applications and services that still have the payload open or mapped. -; Writes one line to the log and sets $HolderFound (1 when anything was named), so the -; abort message can repeat that line to the user. -; -; The lookup is tools/installer-holder-probe, the same executable the guard runs before -; anything is replaced; here it answers the other question, about files a purge could not -; remove. It asks the Restart Manager (RmRegisterResources/RmGetList), which names a -; process holding a plain handle on a file - an antivirus scanner - and names services as -; well as user programs, and it adds every payload program running from the directory -; itself - not one in a folder below it, which is another application the user installed -; there - because a running instance holds its payload as mapped images rather than as open -; handles and the Restart Manager does not reliably report those. Its remaining blind spot -; is a handle taken in kernel mode, where there is no process to name; a payload executable -; among the files then still refuses to be replaced, and the probe answers with that file -; instead of naming nothing. -; -; $INSTDIR travels through the same COCKATRICE_GUARD_DIR environment variable the guard -; uses, so a path with spaces or apostrophes never has to be embedded in a command line. -Function LogProcessesUsingInstallDir - StrCpy $HolderExit 0 - StrCpy $HolderOutput "" - StrCpy $HolderFound 0 - ; The directory that just failed the purge, not whatever a check before the directory - ; page looked at: a portable install into a custom folder reaches here with the two - ; being different. - StrCpy $GuardDir $INSTDIR - Call RunHolderProbe - StrCpy $HolderExit $GuardExit - StrCpy $HolderOutput $GuardOutput - ; The exit code, not the text, decides what happened: it cannot be mangled by what - ; nsExec leaves on the end of the captured output. 1 = the line below names what is - ; holding the files, 0 = nothing holds them any more, anything else = the probe could - ; not answer, and says why in its own output. - ${If} $HolderExit == "1" - StrCpy $HolderFound 1 - ${ElseIf} $HolderExit == "0" - StrCpy $HolderOutput "nothing is holding them and no payload program is running from the directory" +Function un.IsProcessLookupAvailable + nsExec::ExecToLog 'powershell -NoProfile -Command "exit 0"' + Pop $R0 + ${If} $R0 == "0" + StrCpy $R0 1 ${Else} - StrCpy $HolderOutput "the holder probe could not name what holds them: $HolderOutput" + StrCpy $R0 0 ${EndIf} - !insertmacro CockLog "holders: $HolderOutput" FunctionEnd -; Called after a purge left files behind and LogStaleRuntimeDiagnostics named who is -; holding them. Instead of aborting straight away - which on /R strands the user, since -; the old version is already uninstalled - it closes once, purges again, and only then -; asks: end what is still holding the files, or try the removal again by hand. Sets -; $StaleRuntimeAfterPurge to the last purge's verdict, so the caller still aborts when the -; files survive. -; -; The shape is the guard's, deliberately: one automatic attempt at a graceful close, then a -; prompt whose Yes ends the payload programs and whose No retries. A client that was still -; shutting down releases the files in the automatic round and never sees a question, and an -; Oracle or Servatrice - which a close cannot reach - is what the prompt exists for, so the -; user is never left with a dialog whose answers cannot free anything. -; -; The automatic close is a request (WM_CLOSE) and the client keeps the last word: it may be -; waiting for an answer to its own "really quit?" confirmation (a running card database -; update, an open game, an unsaved deck), and that stays its decision. The instance is -; brought forward first, so that confirmation is on screen rather than behind this -; installer's prompt. -; -; Ending is the same last resort the guard offers, with the same cost spelled out, and only -; ever the programs the payload installs - see EndHolders. -; -; Restart Manager's RmShutdown was the alternative and was rejected: it shuts down -; services as well as applications, and the only way to bring them back is RmRestart, -; which would restart this very application into files that are about to be replaced. -; -; Always offered, in every mode: a /S install without /R is the only case without someone -; at the keyboard, and this installer does not promise unattended runs - if an error needs -; a decision, it asks for one. No offer when the probe named nobody: there is nothing to -; end, and the abort's per-file diagnostics already record what Windows said. -; -; One question after the automatic close, and never a loop: Yes ends the programs the -; payload installs - and says so, because anything else the probe named is left to the user - -; and purges again; the other answer gives up, and the caller's abort names what survived. -; The listing is re-read before the question, so it says what is left now rather than -; repeating the first one. -Function RecoverFromHolders - ${If} $HolderFound != 1 - !insertmacro CockLog "recover-holders: no holder was named, nothing to offer" - Return - ${EndIf} - ; The automatic attempt, the same one the guard makes before it asks anything. - !insertmacro CockLog "recover-holders: closing cockatrice, then trying the removal again" - StrCpy $GuardDir $INSTDIR - Call CloseHolders - Call RetryDllPurge - StrCpy $StaleRuntimeAfterPurge $R0 - !insertmacro CockLogListing "after-retry-purge" "*.dll" - !insertmacro CockLogListing "after-retry-plugins" "Plugins\*.*" - !insertmacro CockLog "recover-holders: stale-runtime after retry=$StaleRuntimeAfterPurge" - ${If} $StaleRuntimeAfterPurge = 0 - DetailPrint "Files of the previous version were removed; continuing the installation." - Return - ${EndIf} - ; Still stuck: refresh the names, so the prompt and the log describe what survived the - ; last attempt and not what was there before it. - Call LogStaleRuntimeDiagnostics - ${If} $HolderFound != 1 - Return - ${EndIf} - ; One question, the same one the guard asks and with the same two answers. No retry loop: - ; the purge has already been tried after the close, so the only thing that can still free - ; the files is ending them, and the caller's abort names whatever survives that. - !insertmacro CockEndPrompt "Files of the previous version of Cockatrice are still in use by:" $HolderOutput "installation" - MessageBox MB_YESNO|MB_ICONEXCLAMATION|MB_DEFBUTTON2 "$GuardPrompt" IDYES rfh_end - !insertmacro CockLog "recover-holders: cancelled by user" - Return - ; Only reached through Yes: Return does not come back here. - rfh_end: - !insertmacro CockLog "recover-holders: ending the payload programs on request, then trying the removal again" - Call EndHolders - Call RetryDllPurge - StrCpy $StaleRuntimeAfterPurge $R0 - !insertmacro CockLog "recover-holders: stale-runtime after end=$StaleRuntimeAfterPurge" - ${If} $StaleRuntimeAfterPurge = 0 - DetailPrint "Files of the previous version were removed; continuing the installation." - Return - ${EndIf} - ; Refresh the names one last time so the abort that follows describes what survived the - ; end, and then let it stop the run: the same dialog again would only repeat itself. - Call LogStaleRuntimeDiagnostics +Function un.CloseMatchingApps + ; gracefully ask every matching instance to close (sends WM_CLOSE) + ; + ; Strict filter, same as in the installer: never close a process whose path + ; cannot be read, it is not necessarily one of ours. + System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")' + nsExec::ExecToLog 'powershell -NoProfile -Command "Get-Process -Name $\'$R2$\' -ErrorAction SilentlyContinue | Where-Object { try { $$p = $$_.Path; if ($$p) { $$p.StartsWith($$env:COCKATRICE_GUARD_DIR, [StringComparison]::OrdinalIgnoreCase) } else { $$false } } catch { $$false } } | ForEach-Object { $$null = $$_.CloseMainWindow() }"' + Pop $R3 +FunctionEnd + +Function un.ForceCloseApps + ; stop the matching instances by PID once the grace period is over, see + ; ForceCloseApps in the installer. + System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")' + nsExec::ExecToLog 'powershell -NoProfile -Command "Get-Process -Name $\'$R2$\' -ErrorAction SilentlyContinue | Where-Object { try { $$p = $$_.Path; if ($$p) { $$p.StartsWith($$env:COCKATRICE_GUARD_DIR, [StringComparison]::OrdinalIgnoreCase) } else { $$false } } catch { $$false } } | ForEach-Object { Stop-Process -Id $$_.Id -Force -ErrorAction SilentlyContinue }"' + Pop $R3 +FunctionEnd + +Function un.WaitForAppToClose + Call un.IsAppRunning + ; Only a definite "running" is worth waiting for, see WaitForAppToClose. + ${If} $R0 != 1 + Return + ${EndIf} + + ${If} ${Silent} + DetailPrint "Closing $R1 ..." + Call un.CloseMatchingApps + ; wall-clock deadline: each poll has to spawn a fresh powershell.exe, so + ; a bare loop counter would not bound the real wait time to 60 s. + System::Call 'kernel32::GetTickCount()i.s' + Pop $R8 + un_ck_wait_loop: + Sleep 500 + Call un.IsAppRunning + ${If} $R0 = 0 + DetailPrint "$R1 closed." + Return + ${EndIf} + ${If} $R0 != 1 + Return + ${EndIf} + System::Call 'kernel32::GetTickCount()i.s' + Pop $R9 + IntOp $R9 $R9 - $R8 + ${If} $R9 < 60000 + Goto un_ck_wait_loop + ${EndIf} + ; The grace period is over. Cockatrice is never force-stopped, see + ; WaitForAppToClose in the installer. A silent uninstall only reports + ; through the log and its exit code: it is also run unattended (the /R + ; updater uses it via QuietUninstallString, package managers use it + ; directly), and a message box would block there. The installer that ran + ; us reports the incomplete result to the user itself. + ${If} $AllowForceClose = 0 + DetailPrint "$R1 is still running after 60 s; aborting so that no data is lost." + SetErrorLevel 1 + Abort + ${EndIf} + DetailPrint "Force-stopping $R1 ..." + Call un.ForceCloseApps + Sleep 2000 + Call un.IsAppRunning + ${If} $R0 = 0 + DetailPrint "$R1 closed." + Return + ${EndIf} + ${If} $R0 != 1 + Return + ${EndIf} + DetailPrint "$R1 is still running after 60 s; aborting." + SetErrorLevel 1 + Abort + ${Else} + un_ck_wait_prompt: + MessageBox MB_RETRYCANCEL|MB_ICONEXCLAMATION|MB_DEFBUTTON1 \ + "$R1 is still running.$\r$\n$\r$\nPlease close it, then click Retry.$\r$\nClick Cancel to abort." \ + IDCANCEL un_ck_abort_install + Call un.IsAppRunning + ${If} $R0 = 0 + Return + ${EndIf} + ${If} $R0 != 1 + Return + ${EndIf} + Goto un_ck_wait_prompt + un_ck_abort_install: + Abort + ${EndIf} FunctionEnd -; Uninstaller copy of the guard (the uninstaller gets its own function set compiled in, it -; cannot call the installer functions). ExtractHolderProbe and RunHolderProbe come from -; CockHolderProbeFunctions above; only the policy differs. Function un.EnsureAppsNotRunning ; guards the install directory; the uninstaller only ever has to check this one StrCpy $GuardDir $INSTDIR @@ -2058,80 +1314,29 @@ Function un.EnsureAppsNotRunning FunctionEnd Function un.GuardAppsNotRunning - StrCpy $GuardCloseRounds 0 - StrCpy $GuardRecheckCount 0 - StrCpy $GuardLastHolders "" - un_guard_check: - !insertmacro CockLogUn "uninst-guard: checking $GuardDir" - Call un.RunHolderProbe - ${If} $GuardExit == "0" - !insertmacro CockLogUn "uninst-guard: nothing is holding the payload" + Call un.IsProcessLookupAvailable + ${If} $R0 = 0 + ; Same reasoning as in the installer: without PowerShell we cannot tell + ; whether a running instance still holds file locks, and refusing to + ; uninstall would leave the user with an uninstaller that never + ; completes. Warn once and continue. + DetailPrint "Could not start powershell.exe; skipping the running instance check. Close Cockatrice, Oracle and Servatrice manually if the uninstall reports a problem." + MessageBox MB_OK|MB_ICONEXCLAMATION "This uninstaller could not check whether Cockatrice is still running, because powershell.exe could not be started.$\r$\n$\r$\nPlease close Cockatrice, Oracle and Servatrice manually before continuing." Return ${EndIf} - ${If} $GuardExit == "1" - !insertmacro CockLogUn "uninst-guard: still in use by $GuardOutput" - ; Same convenience as in the installer: ask every instance to close, repeat only - ; while the set changed, then report what the close could not reach. - ${If} $GuardCloseRounds < 2 - ${AndIf} $GuardOutput != $GuardLastHolders - StrCpy $GuardLastHolders $GuardOutput - IntOp $GuardCloseRounds $GuardCloseRounds + 1 - !insertmacro CockLogUn "uninst-guard: asking the payload programs to close (round $GuardCloseRounds)" - Call un.CloseHolders - Goto un_guard_check - ${EndIf} - DetailPrint "The install directory is in use by: $GuardOutput" - ; Same question and the same three answers as the installer's guard: force close, - ; close by hand and check again, or abort. CockEndRecheckPrompt words it, so the two - ; copies promise the same thing. - !insertmacro CockEndRecheckPrompt "Cockatrice, Oracle or Servatrice is still using files of the installation:" $GuardOutput "uninstallation" - MessageBox MB_YESNOCANCEL|MB_ICONEXCLAMATION|MB_DEFBUTTON2 "$GuardPrompt" IDYES un_guard_end IDNO un_guard_recheck - !insertmacro CockLogUn "uninst-guard: cancelled by user after $GuardRecheckCount recheck(s)" - ; The installer's Cancel lands in AbortInstall, which explains what to do next. The - ; uninstaller has no such dialog, so say the same thing here: a bare Abort would - ; close the window and leave the user with no reason and no next step. - MessageBox MB_OK|MB_ICONEXCLAMATION \ - "The uninstallation was cancelled because Cockatrice, Oracle or Servatrice is still running and holding files of the installation.$\r$\n$\r$\n${AFTER_CLOSING_GUIDANCE}$\r$\n$\r$\nClose them and run the uninstaller again." - SetErrorLevel 1 - Abort - un_guard_recheck: - IntOp $GuardRecheckCount $GuardRecheckCount + 1 - !insertmacro CockLogUn "uninst-guard: user closed programs by hand, checking again (recheck $GuardRecheckCount)" - StrCpy $GuardCloseRounds 0 - StrCpy $GuardLastHolders "" - Goto un_guard_check - ; Only reached through Yes: Abort does not return. - un_guard_end: - !insertmacro CockLogUn "uninst-guard: ending the payload programs on request, then checking again" - Call un.EndHolders - ; One measurement and a stop, not another question: what survived a terminate is a - ; program this uninstaller may not open or does not own, and ending it again cannot - ; change that. - Call un.RunHolderProbe - ${If} $GuardExit == "0" - !insertmacro CockLogUn "uninst-guard: nothing is holding the payload" - Return - ${EndIf} - !insertmacro CockLogUn "uninst-guard: still in use after ending them ($GuardExit): $GuardOutput" - SetErrorLevel 1 - Abort - ${EndIf} - ; Same reasoning as in the installer: without an answer from the probe we cannot tell - ; whether a running instance still holds file locks, and refusing outright would leave - ; the user with an uninstaller that never completes. Same three answers too, so an - ; uninstall that hits a Restart Manager that was busy, or a payload program the user has - ; not closed yet, can simply be retried. - !insertmacro CockLogUn "uninst-guard: the holder probe could not answer ($GuardExit): $GuardOutput" - DetailPrint "Could not confirm whether Cockatrice is still running: $GuardOutput" - MessageBox MB_ABORTRETRYIGNORE|MB_ICONEXCLAMATION|MB_DEFBUTTON1 \ - "This uninstaller could not confirm whether Cockatrice, Oracle or Servatrice is still running.$\r$\n$\r$\nThe check reported: $GuardOutput$\r$\n$\r$\nClose them, then choose Retry to check again. Choose Ignore to uninstall anyway, or Abort to cancel the uninstallation." \ - IDRETRY un_guard_check IDABORT un_guard_unknown_abort - !insertmacro CockLogUn "uninst-guard: continuing without an answer from the probe" - Return - un_guard_unknown_abort: - !insertmacro CockLogUn "uninst-guard: cancelled after the probe could not answer" - SetErrorLevel 1 - Abort + StrCpy $R1 "cockatrice.exe" + StrCpy $R2 "cockatrice" + ; a GUI application with unsaved user data: graceful close only + StrCpy $AllowForceClose 0 + Call un.WaitForAppToClose + StrCpy $R1 "oracle.exe" + StrCpy $R2 "oracle" + StrCpy $AllowForceClose 1 + Call un.WaitForAppToClose + StrCpy $R1 "servatrice.exe" + StrCpy $R2 "servatrice" + StrCpy $AllowForceClose 1 + Call un.WaitForAppToClose FunctionEnd Function PortableModePageCreate @@ -2187,10 +1392,7 @@ ${If} $PortableMode = 0 ${If} $ReinstallMode = 0 MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "A previous version of Cockatrice must be uninstalled before installing the new one." IDOK uninst32 - ; Abort would only skip this page and install over the old version anyway; - ; the message said the old one must go first, so Cancel means cancel. - !insertmacro CockLog "componentsPagePre: cancelled at the previous-version prompt" - Quit + Abort ${Else} Goto uninst32 ${EndIf} @@ -2207,24 +1409,10 @@ ${If} $PortableMode = 0 StrCpy $OldUninstallString $R0 Call EnsureOldInstallNotRunning StrCpy $OldInstallDir $GuardDir - ; Run it silently. The question above is the one the user answered; the - ; uninstaller's own confirmation page would be a second one, and that one opens - ; behind this window, where nobody can answer it. The installer then looks hung, - ; the user clicks Next on it, the new version is installed, and the uninstaller - ; removes everything once this window closes. /S leaves no window to be behind, - ; so ExecWait is waiting for something that cannot outlive it. - ReadRegStr $R1 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "QuietUninstallString" - ${If} $R1 == "" - StrCpy $OldUninstallString "$OldUninstallString /S" - ${Else} - StrCpy $OldUninstallString $R1 - ${EndIf} !insertmacro CockLog "componentsPagePre: old 32-bit uninstall '$OldUninstallString _?=$OldInstallDir' (oldDir=$OldInstallDir InstDir=$INSTDIR)" ClearErrors Call BackupUserDataBeforeUninstall - Call RunOldUninstaller - !insertmacro CockLog "componentsPagePre: old 32-bit uninstall exit=$OldUninstallExit" - Call WaitForOldPayloadToDisappear + ExecWait "$OldUninstallString _?=$OldInstallDir" Call RestoreUserDataAfterUninstall done32: @@ -2238,8 +1426,7 @@ ${If} $PortableMode = 0 ${If} $ReinstallMode = 0 MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "A previous version of Cockatrice must be uninstalled before installing the new one." IDOK uninst64 - !insertmacro CockLog "componentsPagePre: cancelled at the previous-version prompt" - Quit + Abort ${Else} Goto uninst64 ${EndIf} @@ -2248,19 +1435,10 @@ ${If} $PortableMode = 0 StrCpy $OldUninstallString $R0 Call EnsureOldInstallNotRunning StrCpy $OldInstallDir $GuardDir - ; Silent, for the reason given in the 32-bit branch above. - ReadRegStr $R1 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "QuietUninstallString" - ${If} $R1 == "" - StrCpy $OldUninstallString "$OldUninstallString /S" - ${Else} - StrCpy $OldUninstallString $R1 - ${EndIf} !insertmacro CockLog "componentsPagePre: old 64-bit uninstall '$OldUninstallString _?=$OldInstallDir' (oldDir=$OldInstallDir InstDir=$INSTDIR)" ClearErrors Call BackupUserDataBeforeUninstall - Call RunOldUninstaller - !insertmacro CockLog "componentsPagePre: old 64-bit uninstall exit=$OldUninstallExit" - Call WaitForOldPayloadToDisappear + ExecWait "$OldUninstallString _?=$OldInstallDir" Call RestoreUserDataAfterUninstall done64: @@ -2300,23 +1478,6 @@ ${If} $ReinstallMode = 0 Call EnsureAppsNotRunning ${EndIf} -; Measured before either purge block, because it has to describe the directory as -; it arrived here and not what a purge left of it: IsExistingCockatriceInstall -; looks for cockatrice.exe, oracle.exe, servatrice.exe, uninstall.exe, Qt6*.dll -; and qt.conf, and the portable purge below deletes exactly those. Taken after -; that purge it answered "not an install" as soon as the only survivor was a file -; it does not look for - a locked abseil_dll.dll or libssl-3-x64.dll, say - and -; the abort further down was then skipped for the very directory the purge had -; just failed in. -; Remembered because the stale-runtime check below must only ever fire for a -; directory this installer recognised as a Cockatrice install. Scanning for -; leftovers in an unrelated directory (a first-time install into a folder that -; happens to hold some Qt5 DLLs, say) would otherwise turn those files into an -; abort reason even though the payload is about to overwrite nothing of theirs. -Call IsExistingCockatriceInstall -!insertmacro CockLog "section: IsExistingCockatriceInstall=$R0" -StrCpy $IsUpgrade $R0 - ${If} $PortableMode = 1 ${AndIf} ${FileExists} "$INSTDIR\portable.dat" ; upgrade portable mode @@ -2340,12 +1501,7 @@ ${AndIf} ${FileExists} "$INSTDIR\portable.dat" ; what this has to guarantee is that nothing from the previous build is left ; to be loaded next to the new executables. Naming the families by hand is ; what let abseil_dll.dll through; see HasStaleRuntimeFiles. - Call RetryDllPurge - ; The purge's own verdict: HasStaleRuntimeFiles measures *.dll + Plugins\*.*, - ; the set that catches any leftover runtime DLL, Qt or otherwise. Held in a - ; variable because $R0 is reused by the next function call in this section. - StrCpy $StaleRuntimeAfterPurge $R0 - StrCpy $DllPurgeRan 1 + Delete "$INSTDIR\*.dll" Delete "$INSTDIR\qt.conf" Delete "$INSTDIR\qdebug.txt" Delete "$INSTDIR\servatrice.sql" @@ -2356,33 +1512,20 @@ ${EndIf} ; Remove any runtime DLLs left over from older versions, so a mismatched ; Qt/OpenSSL/protobuf set cannot survive next to the new executables. -; -; A purge that fails is reported with evidence, not only as a survivor list: the -; abort below logs which files survived, what Windows said about each of them and -; which processes still have the install directory mapped (see -; LogStaleRuntimeDiagnostics). -; -; Re-running the running-instance check here was considered and dropped: it would only -; narrow the window the uninstaller ExecWait and the backup/restore copy open between -; .onInit and this point, and on /R it would now abort after the old version has already -; been removed - leaving no install at all. The surviving files are the authority on -; whether anything still holds the directory, and they are measured anyway. -; -; The holder list comes from Restart Manager (RmRegisterResources/RmGetList), the API -; that names every process and service with one of these files open or mapped. It is -; called from tools/installer-holder-probe rather than from this script, because the -; RM_PROCESS_INFO array is a table of hand-computed offsets when it is marshalled -; through the System plugin, and that table is the one thing here that cannot be -; checked without a Windows toolchain. Compiled by the same build that compiles this -; installer, the layout belongs to the Windows SDK, and .ci/compile.sh runs the probe -; on the Windows runners instead of trusting that it compiles. +Call IsExistingCockatriceInstall +!insertmacro CockLog "section: IsExistingCockatriceInstall=$R0" +; Remembered because the stale-runtime check below must only ever fire for a +; directory this installer recognised as a Cockatrice install. Scanning for +; leftovers in an unrelated directory (a first-time install into a folder that +; happens to hold some Qt5 DLLs, say) would otherwise turn those files into an +; abort reason even though the payload is about to overwrite nothing of theirs. +StrCpy $IsUpgrade $R0 ${If} $PortableMode = 0 -${AndIf} $IsUpgrade = 1 +${AndIf} $R0 = 1 !insertmacro CockLog "section: non-portable upgrade purge" !insertmacro CockLogListing "before-purge" "*.dll" - Call RetryDllPurge - StrCpy $StaleRuntimeAfterPurge $R0 - StrCpy $DllPurgeRan 1 + RMDir /r "$INSTDIR\Plugins" + Delete "$INSTDIR\*.dll" !insertmacro CockLogListing "after-purge" "*.dll" !insertmacro CockLogListing "after-plugins" "Plugins\*.*" ${EndIf} @@ -2397,78 +1540,22 @@ ${EndIf} ; run on Windows or it contains an error", so stop here instead of compounding ; it. Nothing of the new payload has been written at this point, so the next run, ; with the applications closed, gets a clean install. -; $StaleRuntimeAfterPurge is RetryDllPurge's verdict, measured with -; HasStaleRuntimeFiles (*.dll + Plugins\*.*) - the set that catches any leftover -; runtime DLL, Qt or otherwise. It exists only when a purge ran, and the two -; purge blocks above cover every upgrade except one: a portable install into a -; directory without portable.dat is neither of them, so that directory is -; measured here instead. A test of $IsUpgrade alone could not stand in for it - -; that only says the directory was ours before the purge, not what the purge -; left behind. -; A directory this installer did not recognise as an install never aborts: the -; unrelated DLLs already in it (a first-time install into a folder that happens -; to hold some Qt5 DLLs, say) are not leftovers, and the payload overwrites -; nothing of theirs. ${If} $IsUpgrade = 1 - ${If} $DllPurgeRan = 0 - !insertmacro CockLog "section: no purge applies, measuring the directory instead" - Call HasStaleRuntimeFiles - StrCpy $StaleRuntimeAfterPurge $R0 - ${EndIf} - !insertmacro CockLog "section: stale-runtime after purge=$StaleRuntimeAfterPurge" + Call HasStaleRuntimeFiles + !insertmacro CockLog "section: HasStaleRuntimeFiles=$R0" ${Else} - StrCpy $StaleRuntimeAfterPurge 0 + StrCpy $R0 0 ${EndIf} -${If} $StaleRuntimeAfterPurge = 1 - !insertmacro CockLog "section: files of a previous version survived the purge" - ; Who is holding the payload is recorded first, so the log that reaches us - ; answers the question the message can only guess at, and so the prompt can - ; name them and offer to close the ones this installer owns. - Call LogStaleRuntimeDiagnostics - ${If} $HolderFound = 1 - ; On /R the previous version is already uninstalled at this point, so an abort - ; leaves the user with no working Cockatrice. It closes once, then offers to end - ; what is left; this updates $StaleRuntimeAfterPurge with the verdict after that - ; close, or after the end when the user asked for one. - Call RecoverFromHolders - ${Else} - ; Nothing is holding them. The three attempts in RetryDllPurge were two seconds - ; apart and all failed with no process to blame, which is the transient case a - ; virus scanner or a background indexer creates - or a permission problem that - ; will not clear. Give the delete one last chance before giving up: it costs a - ; second on a path that is about to abort anyway, and it keeps a lock that was - ; released in the meantime from turning into a failed upgrade. - !insertmacro CockLog "section: no holder named, one final purge attempt" - Sleep 1000 - Call RetryDllPurge - StrCpy $StaleRuntimeAfterPurge $R0 - !insertmacro CockLog "section: final-purge stale-runtime=$StaleRuntimeAfterPurge" - ${EndIf} -${EndIf} -${If} $StaleRuntimeAfterPurge = 1 +${If} $R0 = 1 !insertmacro CockLog "section: aborting, files of a previous version survived the purge" DetailPrint "Files of a previous version could not be removed; aborting the installation." - ; Say what is known and nothing more: the guard ran before anything was replaced, - ; so "most likely still running" is no longer a fair guess and would send the user - ; to close programs that are already closed. + ; The in-app updater runs this installer silent and detached and never looks + ; at its exit code, so the user has to be told what happened. A /S install + ; without /R is unattended (a package manager, for instance) and a message + ; box there would block forever, so that case only reports through the log + ; and the exit code. AbortInstall owns that decision. StrCpy $AbortReason \ - "The installation could not remove all files of a previous version of Cockatrice, and was cancelled rather than leave a mismatched installation behind." - ${If} $HolderFound = 1 - StrCpy $AbortReason "$AbortReason$\r$\n$\r$\nStill using the install directory:$\r$\n$HolderOutput$\r$\n$\r$\nClose them and run the installer again." - ${ElseIf} $HolderExit == "0" - StrCpy $AbortReason "$AbortReason$\r$\n$\r$\nNo running program was found using those files. A file permission or a virus scanner may be preventing their removal; the installer log records what Windows reported for each file." - ${Else} - ; The check itself failed, so "no running program was found" would be a claim this - ; installer cannot make. Name the failure instead, and leave the per-file codes to - ; the log, where they already are. - StrCpy $AbortReason "$AbortReason$\r$\n$\r$\nThe check for a running program could not be completed: $HolderOutput$\r$\n$\r$\nClose Cockatrice, Oracle and Servatrice and run the installer again; the installer log records what Windows reported for each file." - ${EndIf} - ; "Run the installer again" is not enough on an in-app update: the old version - ; was uninstalled before the purge, so point at the installer that was downloaded. - ${If} $ReinstallMode = 1 - ${AndIf} $PortableMode = 0 - StrCpy $AbortReason "$AbortReason$\r$\n$\r$\nThe previous version has already been removed. To finish the update, run this installer again:$\r$\n$EXEPATH" - ${EndIf} + "The installation could not remove all files of a previous version of Cockatrice, most likely because it is still running or was started as administrator, and was cancelled rather than leave a mismatched install behind.$\r$\n$\r$\nPlease close Cockatrice, Oracle and Servatrice and run the installer again." Call AbortInstall ${EndIf} diff --git a/cmake/SendInstallerLog.ps1.in b/cmake/SendInstallerLog.ps1.in index 585c726dd..dfb8c3d63 100644 --- a/cmake/SendInstallerLog.ps1.in +++ b/cmake/SendInstallerLog.ps1.in @@ -57,29 +57,6 @@ try { } } - # --- What was holding the files, read from the log as well ------------------- - # The guard and the close attempt each log one line naming every holder and what - # happened to it. Carrying the last of each as event data keeps an abort caused by - # a running instance actionable without opening the attachment. Kept out of the - # message so the issue still groups on the reason alone. - $Holders = '' - $CloseResult = '' - $EndResult = '' - if (Test-Path -LiteralPath $LogPath) { - $holderLine = Select-String -LiteralPath $LogPath -Pattern 'guard: still in use by (.*)$' | Select-Object -Last 1 - if ($holderLine) { - $Holders = $holderLine.Matches[0].Groups[1].Value - } - $closeLine = Select-String -LiteralPath $LogPath -Pattern '(close-holders: .*)$' | Select-Object -Last 1 - if ($closeLine) { - $CloseResult = $closeLine.Matches[0].Groups[1].Value - } - $endLine = Select-String -LiteralPath $LogPath -Pattern '(end-holders: .*)$' | Select-Object -Last 1 - if ($endLine) { - $EndResult = $endLine.Matches[0].Groups[1].Value - } - } - # --- Event --------------------------------------------------------------- $eventId = [guid]::NewGuid().ToString('N') $now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') @@ -98,9 +75,6 @@ try { extra = @{ reason = $Reason os = [System.Environment]::OSVersion.VersionString - holders = $Holders - close_result = $CloseResult - end_result = $EndResult } } $eventJson = $sentryEvent | ConvertTo-Json -Compress -Depth 6 diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index ade7c3976..ebd2a2f78 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -25,7 +25,9 @@ set(cockatrice_SOURCES src/client/settings/shortcut_treeview.cpp src/client/settings/shortcuts_settings.cpp src/interface/deck_loader/card_node_function.cpp + src/interface/deck_loader/deck_file_format.cpp src/interface/deck_loader/deck_loader.cpp + src/interface/deck_loader/loaded_deck.cpp src/interface/widgets/dialogs/dlg_connect.cpp src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.cpp src/interface/widgets/dialogs/dlg_create_game.cpp @@ -69,27 +71,36 @@ set(cockatrice_SOURCES src/filters/filter_builder.cpp src/filters/filter_tree_model.cpp src/filters/syntax_help.cpp + src/game/abstract_game.cpp + src/game/arrow_registry.cpp src/game_graphics/board/abstract_card_drag_item.cpp src/game_graphics/board/abstract_card_item.cpp src/game_graphics/board/abstract_counter.cpp + src/game/board/arrow_data.cpp src/game_graphics/board/arrow_item.cpp - src/game_graphics/board/arrow_registry.cpp src/game_graphics/board/arrow_target.cpp src/game_graphics/board/card_drag_item.cpp src/game_graphics/board/card_item.cpp + src/game/board/card_list.cpp + src/game/board/card_state.cpp src/game_graphics/board/counter_general.cpp + src/game/board/counter_state.cpp src/game_graphics/board/translate_counter_name.cpp src/game_graphics/deckview/deck_view.cpp src/game_graphics/deckview/deck_view_container.cpp src/game_graphics/deckview/tabbed_deck_view_container.cpp - src/game_graphics/dialogs/dlg_annotation.cpp src/game_graphics/dialogs/dlg_create_token.cpp src/game_graphics/dialogs/dlg_move_top_cards_until.cpp src/game_graphics/dialogs/dlg_roll_dice.cpp + src/game/game.cpp + src/game/game_event_handler.cpp + src/game/game_meta_info.cpp src/game_graphics/game_scene.cpp + src/game/game_state.cpp src/game_graphics/game_view.cpp src/game_graphics/hand_counter.cpp src/game_graphics/log/message_log_widget.cpp + src/game/phase.cpp src/game_graphics/phases_toolbar.cpp src/game_graphics/player/menu/card_menu.cpp src/game_graphics/player/menu/custom_zone_menu.cpp @@ -107,11 +118,23 @@ set(cockatrice_SOURCES src/game_graphics/tally/stats_tally.cpp src/game_graphics/tally/subtype_tally.cpp src/game_graphics/tally/tally.cpp + src/game/player/player_actions.cpp src/game_graphics/player/player_area.cpp src/game_graphics/player/player_dialogs.cpp + src/game/player/player_event_handler.cpp src/game_graphics/player/player_graphics_item.cpp + src/game/player/player_info.cpp src/game_graphics/player/player_list_widget.cpp + src/game/player/player_logic.cpp + src/game/player/player_manager.cpp src/game_graphics/player/player_target.cpp + src/game/replay.cpp + src/game/zones/card_zone_logic.cpp + src/game/zones/hand_zone_logic.cpp + src/game/zones/pile_zone_logic.cpp + src/game/zones/stack_zone_logic.cpp + src/game/zones/table_zone_logic.cpp + src/game/zones/view_zone_logic.cpp src/game_graphics/zones/card_zone.cpp src/game_graphics/zones/hand_zone.cpp src/game_graphics/zones/pile_zone.cpp @@ -172,7 +195,6 @@ set(cockatrice_SOURCES src/interface/widgets/cards/card_info_picture_widget.cpp src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.cpp src/interface/widgets/cards/card_info_text_widget.cpp - src/interface/widgets/cards/card_picture_delegate.cpp src/interface/widgets/cards/card_size_widget.cpp src/interface/widgets/cards/deck_card_zone_display_widget.cpp src/interface/widgets/cards/deck_preview_card_picture_widget.cpp @@ -292,7 +314,6 @@ set(cockatrice_SOURCES src/interface/widgets/utility/sequence_edit.cpp src/interface/widgets/utility/visibility_change_listener.cpp src/interface/widgets/utility/visibility_change_listener.h - src/interface/widgets/visual_database_display/card_database_visual_view.cpp src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp src/interface/widgets/visual_database_display/visual_database_display_filter_save_load_widget.cpp src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp @@ -325,7 +346,6 @@ set(cockatrice_SOURCES src/interface/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.cpp src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp src/interface/window_main.cpp - src/installer_close_server.cpp src/main.cpp src/single_instance_manager.cpp src/interface/widgets/tabs/abstract_tab_deck_editor.cpp @@ -431,7 +451,6 @@ set(cockatrice_SOURCES src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.h src/interface/widgets/utility/compact_push_button.cpp src/interface/widgets/utility/compact_push_button.h - src/installer_close_server.h src/single_instance_manager.h src/client/url_scheme_event_filter.h src/interface/intents/intent_connect_to_server.cpp @@ -493,7 +512,7 @@ if(APPLE) set(cockatrice_SOURCES ${cockatrice_SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/resources/appicon.icns) endif(APPLE) -qt_add_resources(cockatrice_RESOURCES_RCC ${cockatrice_RESOURCES}) +qt6_add_resources(cockatrice_RESOURCES_RCC ${cockatrice_RESOURCES}) # Declare path variables set(ICONDIR @@ -514,7 +533,7 @@ set(COCKATRICE_MAC_QM_INSTALL_DIR "cockatrice.app/Contents/Resources/translation set(COCKATRICE_UNIX_QM_INSTALL_DIR "share/cockatrice/translations") set(COCKATRICE_WIN32_QM_INSTALL_DIR "translations") -qt_add_executable( +qt6_add_executable( cockatrice WIN32 MACOSX_BUNDLE @@ -526,7 +545,7 @@ qt_add_executable( target_precompile_headers(cockatrice PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtwidgets_pch.h") -qt_add_shaders( +qt6_add_shaders( cockatrice "onboarding_shaders" PREFIX @@ -538,7 +557,7 @@ qt_add_shaders( src/interface/widgets/onboarding/shaders/brand_plate.frag ) -qt_add_resources( +qt6_add_resources( cockatrice "onboarding_qml" PREFIX @@ -554,7 +573,6 @@ target_link_libraries( PUBLIC libcockatrice_card libcockatrice_deck_list libcockatrice_filters - libcockatrice_game libcockatrice_utility libcockatrice_network libcockatrice_models @@ -771,7 +789,7 @@ endif() if(Qt6LinguistTools_FOUND) #Qt6 Translations happen after the executable is built up if(UPDATE_TRANSLATIONS) - qt_add_translations( + qt6_add_translations( cockatrice TS_FILES ${cockatrice_TS} @@ -781,7 +799,7 @@ if(Qt6LinguistTools_FOUND) cockatrice_QM ) else() - qt_add_translations(cockatrice TS_FILES ${cockatrice_TS} QM_FILES_OUTPUT_VARIABLE cockatrice_QM) + qt6_add_translations(cockatrice TS_FILES ${cockatrice_TS} QM_FILES_OUTPUT_VARIABLE cockatrice_QM) endif() if(UNIX) @@ -807,4 +825,4 @@ if(Qt6LinguistTools_FOUND) endforeach() endif() -qt_finalize_target(cockatrice) +qt6_finalize_target(cockatrice) diff --git a/cockatrice/src/client/latency_status_widget.cpp b/cockatrice/src/client/latency_status_widget.cpp index 907a50668..940074581 100644 --- a/cockatrice/src/client/latency_status_widget.cpp +++ b/cockatrice/src/client/latency_status_widget.cpp @@ -14,7 +14,6 @@ #include #include #include -#include #include LatencyStatusWidget::LatencyStatusWidget(QWidget *parent) : QWidget(parent) @@ -25,7 +24,9 @@ LatencyStatusWidget::LatencyStatusWidget(QWidget *parent) : QWidget(parent) latencyGraph = new LatencyGraphWidget(this); latencyGraph->setFixedSize(90, 14); - auto *layout = QtUtils::makeLayout(this, 4); + auto *layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(4); layout->addWidget(latencyGraph); layout->addWidget(pingLabel); diff --git a/cockatrice/src/client/network/connection_controller/remote_connection_controller.cpp b/cockatrice/src/client/network/connection_controller/remote_connection_controller.cpp index c702f91f1..037565c6a 100644 --- a/cockatrice/src/client/network/connection_controller/remote_connection_controller.cpp +++ b/cockatrice/src/client/network/connection_controller/remote_connection_controller.cpp @@ -3,7 +3,6 @@ #include "../../../interface/pixel_map_generator.h" #include "../../settings/cache_settings.h" #include "../interface/widgets/dialogs/dlg_connect.h" -#include "../interface/widgets/dialogs/dlg_edit_password.h" #include "../interface/widgets/dialogs/dlg_forgot_password_challenge.h" #include "../interface/widgets/dialogs/dlg_forgot_password_request.h" #include "../interface/widgets/dialogs/dlg_forgot_password_reset.h" @@ -25,7 +24,6 @@ #include #include #include -#include #include class QWidget; @@ -100,10 +98,6 @@ void ConnectionController::wireClientSignals() connect(remoteClient, &RemoteClient::sigPasswordVerifierReady, this, &ConnectionController::onPasswordVerifierReady); - - connect(remoteClient, &RemoteClient::sigPasswordChangeSuccess, this, - &ConnectionController::onPasswordChangeSuccess); - connect(remoteClient, &RemoteClient::sigPasswordChangeError, this, &ConnectionController::onPasswordChangeError); } void ConnectionController::connectToServer() @@ -249,27 +243,17 @@ void ConnectionController::onLoginError(int r, { switch (static_cast(r)) { case Response::RespClientUpdateRequired: { + QString formatted = "Missing Features: "; + for (int i = 0; i < missingFeatures.size(); ++i) { + formatted.append(QString("\n %1").arg(QChar(0x2022)) + " " + missingFeatures.value(i)); + } + QMessageBox msgBox(dialogParent); msgBox.setIcon(QMessageBox::Critical); msgBox.setWindowTitle(tr("Failed Login")); - - if (missingFeatures.isEmpty()) { - // Without a feature list this is either a login the client stopped itself or a - // strict server asking for a password; the reason string already says which and - // what to do. No client update can fix any of those causes, so show it instead of - // the update advice. - msgBox.setText(reasonStr.isEmpty() ? tr("The login did not complete.") : reasonStr); - } else { - QString formatted = tr("Missing Features:"); - for (const QString &feature : missingFeatures) { - formatted.append(QString("\n %1 ").arg(QChar(0x2022)) + feature); - } - - msgBox.setText(tr("Your client seems to be missing features this server requires for connection.") + - "\n\n" + tr("To update your client, go to 'Help -> Check for Client Updates'.")); - msgBox.setDetailedText(formatted); - } - + msgBox.setText(tr("Your client seems to be missing features this server requires for connection.") + + "\n\n" + tr("To update your client, go to 'Help -> Check for Client Updates'.")); + msgBox.setDetailedText(formatted); msgBox.exec(); break; } @@ -316,8 +300,9 @@ void ConnectionController::onLoginError(int r, } case Response::RespRegistrationRequired: { - if (QtUtils::confirm(dialogParent, tr("Error"), - tr("This server requires user registration. Do you want to register now?"))) { + if (QMessageBox::question(dialogParent, tr("Error"), + tr("This server requires user registration. Do you want to register now?"), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { registerToServer(); } return; // don't re-prompt connect @@ -356,16 +341,10 @@ void ConnectionController::onLoginError(int r, } case Response::RespPasswordChangeRequired: { - // The credentials were right, but the account may not be used until a new password - // is chosen. Let the user complete the change here: sending them off to "ask the - // administrator for your temporary password" sends them in circles, since they just - // logged in with it and the session ends without a way to comply. - DlgEditPassword dlg(dialogParent); - dlg.setWindowTitle(tr("Password Change Required")); - if (dlg.exec()) { - remoteClient->changePasswordToServer(dlg.getOldPassword(), dlg.getNewPassword()); - return; // onPasswordChangeSuccess/onPasswordChangeError decides the next step - } + QMessageBox::information( + dialogParent, tr("Password Change Required"), + tr("An administrator has reset your password. Please contact your server administrator to obtain " + "your temporary password, then log in and change it via Account -> Change Password.")); remoteClient->disconnectFromServer(); return; } @@ -528,43 +507,6 @@ void ConnectionController::onNotifyUserAboutUpdate() "To update your client, go to Help -> Check for Updates.")); } -void ConnectionController::onPasswordChangeSuccess() -{ - // The credential saved for this profile is stale now: drop it so the next login asks for - // the new password instead of replaying the old verifier. - if (!pendingSaveName.isEmpty()) { - SettingsCache::instance().servers().setServerPassword(pendingSaveName, ""); - } - - QMessageBox::information(dialogParent, tr("Password Changed"), - tr("Your password has been changed. Please log in again with your new password.")); - remoteClient->disconnectFromServer(); - connectToServer(); -} - -void ConnectionController::onPasswordChangeError(int responseCode) -{ - QString reason; - switch (static_cast(responseCode)) { - case Response::RespWrongPassword: - reason = tr("The current password is incorrect."); - break; - case Response::RespPasswordTooShort: - reason = tr("The new password is too short."); - break; - case Response::RespClientUpdateRequired: - reason = tr("The server does not accept the new password format."); - break; - default: - reason = tr("The password could not be changed (error %1).").arg(responseCode); - break; - } - - QMessageBox::critical(dialogParent, tr("Password Change Failed"), reason); - remoteClient->disconnectFromServer(); - connectToServer(); -} - void ConnectionController::onForgotPasswordSuccess() { QMessageBox::information( @@ -575,38 +517,14 @@ void ConnectionController::onForgotPasswordSuccess() SettingsCache::instance().servers().setFPPlayerName(""); } -void ConnectionController::onForgotPasswordError(int responseCode) +void ConnectionController::onForgotPasswordError() { - // Rate limiting is retryable, so it must not clear the prefill. - bool keepPrefill = false; - QString message; - switch (static_cast(responseCode)) { - case Response::RespTooManyRequests: - // Retryable: keep the host/port/user prefill so the user can try again. The window - // is server-configured (an hour by default), so do not promise a shorter wait. - keepPrefill = true; - message = tr("Too many password reset attempts from your address. Please wait before " - "requesting a new reset."); - break; - case Response::RespFunctionNotAllowed: - // Also returned for an unknown user, a banned user, and an expired token or - // challenge, so the message must not claim a single cause. - message = tr("The server did not accept the reset request. Check the user name, or contact " - "the server operator to reset your password."); - break; - default: - message = tr("Failed to reset user account password, please contact the server operator to reset your " - "password."); - break; - } - - QMessageBox::warning(dialogParent, tr("Reset Password"), message); - - if (!keepPrefill) { - SettingsCache::instance().servers().setFPHostName(""); - SettingsCache::instance().servers().setFPPort(""); - SettingsCache::instance().servers().setFPPlayerName(""); - } + QMessageBox::warning( + dialogParent, tr("Reset Password"), + tr("Failed to reset user account password, please contact the server operator to reset your password.")); + SettingsCache::instance().servers().setFPHostName(""); + SettingsCache::instance().servers().setFPPort(""); + SettingsCache::instance().servers().setFPPlayerName(""); } void ConnectionController::onPromptForgotPasswordReset() diff --git a/cockatrice/src/client/network/connection_controller/remote_connection_controller.h b/cockatrice/src/client/network/connection_controller/remote_connection_controller.h index 2987a8d99..150ec9e5b 100644 --- a/cockatrice/src/client/network/connection_controller/remote_connection_controller.h +++ b/cockatrice/src/client/network/connection_controller/remote_connection_controller.h @@ -87,17 +87,13 @@ private slots: // Forgot-password flow void onForgotPasswordSuccess(); - void onForgotPasswordError(int responseCode); + void onForgotPasswordError(); void onPromptForgotPasswordReset(); void onPromptForgotPasswordChallenge(); // Persists the derived scrypt verifier after a successful challenge-response login void onPasswordVerifierReady(const QString &verifier); - // Completes the forced password change the server demanded at login - void onPasswordChangeSuccess(); - void onPasswordChangeError(int responseCode); - private: void wireClientSignals(); void updateWindowTitle(); diff --git a/cockatrice/src/client/network/interfaces/deck_stats_interface.cpp b/cockatrice/src/client/network/interfaces/deck_stats_interface.cpp index 508f0bdc9..d5cfdbbc9 100644 --- a/cockatrice/src/client/network/interfaces/deck_stats_interface.cpp +++ b/cockatrice/src/client/network/interfaces/deck_stats_interface.cpp @@ -20,12 +20,11 @@ #include #include #include -#include #include DeckStatsInterface::DeckStatsInterface(QObject *parent) : QObject(parent) { - manager = NetworkManagerFactory::create(this); + manager = new QNetworkAccessManager(this); connect(manager, &QNetworkAccessManager::finished, this, &DeckStatsInterface::queryFinished); } diff --git a/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp b/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp index 34dd03765..30044c2b6 100644 --- a/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp +++ b/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp @@ -26,12 +26,11 @@ #include #include #include -#include #include TappedOutInterface::TappedOutInterface(QObject *parent) : QObject(parent) { - manager = NetworkManagerFactory::create(this); + manager = new QNetworkAccessManager(this); connect(manager, &QNetworkAccessManager::finished, this, &TappedOutInterface::queryFinished); } diff --git a/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.cpp b/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.cpp index 62668c4d9..192029b23 100644 --- a/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.cpp +++ b/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.cpp @@ -27,7 +27,6 @@ #include #include #include -#include #include #include #include @@ -57,7 +56,7 @@ void SpoilerBackgroundUpdater::startSpoilerDownloadProcess(QString url, bool sav void SpoilerBackgroundUpdater::downloadFromURL(QUrl url, bool saveResults) { - auto *nam = NetworkManagerFactory::create(this); + auto *nam = new QNetworkAccessManager(this); auto request = QNetworkRequest(url); request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING)); QNetworkReply *reply = nam->get(request); diff --git a/cockatrice/src/client/network/update/client/release_channel.cpp b/cockatrice/src/client/network/update/client/release_channel.cpp index ce80a2a76..45c503623 100644 --- a/cockatrice/src/client/network/update/client/release_channel.cpp +++ b/cockatrice/src/client/network/update/client/release_channel.cpp @@ -20,7 +20,6 @@ #include #include #include -#include #include #include #include @@ -63,7 +62,7 @@ std::optional getProductVersionMajor() } // namespace -ReleaseChannel::ReleaseChannel() : netMan(NetworkManagerFactory::create(this)), response(nullptr), lastRelease(nullptr) +ReleaseChannel::ReleaseChannel() : netMan(new QNetworkAccessManager(this)), response(nullptr), lastRelease(nullptr) { } diff --git a/cockatrice/src/client/network/update/client/update_downloader.cpp b/cockatrice/src/client/network/update/client/update_downloader.cpp index 8868876fa..c86972d60 100644 --- a/cockatrice/src/client/network/update/client/update_downloader.cpp +++ b/cockatrice/src/client/network/update/client/update_downloader.cpp @@ -9,11 +9,10 @@ #include #include #include -#include UpdateDownloader::UpdateDownloader(QObject *parent) : QObject(parent), response(nullptr) { - netMan = NetworkManagerFactory::create(this); + netMan = new QNetworkAccessManager(this); } void UpdateDownloader::beginDownload(QUrl downloadUrl) diff --git a/cockatrice/src/client/settings/cache_settings.h b/cockatrice/src/client/settings/cache_settings.h index 8ccf6f76b..a6610ee0d 100644 --- a/cockatrice/src/client/settings/cache_settings.h +++ b/cockatrice/src/client/settings/cache_settings.h @@ -19,6 +19,7 @@ inline Q_LOGGING_CATEGORY(SettingsCacheLog, "settings_cache"); class ReleaseChannel; +#define DEFAULT_LANG_NAME "English" #define CLIENT_INFO_NOT_SET "notset" #define DEFAULT_FONT_SIZE 12 diff --git a/libcockatrice_game/libcockatrice/game/translation.h b/cockatrice/src/client/translation.h similarity index 100% rename from libcockatrice_game/libcockatrice/game/translation.h rename to cockatrice/src/client/translation.h diff --git a/cockatrice/src/database/interface/settings_card_preference_provider.h b/cockatrice/src/database/interface/settings_card_preference_provider.h index f4bf23141..1feb2006b 100644 --- a/cockatrice/src/database/interface/settings_card_preference_provider.h +++ b/cockatrice/src/database/interface/settings_card_preference_provider.h @@ -2,18 +2,13 @@ #define COCKATRICE_SETTINGS_CARD_PREFERENCE_PROVIDER_H #include "../../client/settings/cache_settings.h" -#include #include #include #include -class SettingsCardPreferenceProvider : public QObject, public ICardPreferenceProvider +class SettingsCardPreferenceProvider : public ICardPreferenceProvider { public: - explicit SettingsCardPreferenceProvider(QObject *parent = nullptr) : QObject(parent) - { - } - [[nodiscard]] QString getCardPreferenceOverride(const QString &cardName) const override { return SettingsCache::instance().cardOverrides().getCardPreferenceOverride(cardName); diff --git a/cockatrice/src/filters/deck_filter_string.cpp b/cockatrice/src/filters/deck_filter_string.cpp index f0df897fc..616311750 100644 --- a/cockatrice/src/filters/deck_filter_string.cpp +++ b/cockatrice/src/filters/deck_filter_string.cpp @@ -1,5 +1,6 @@ #include "deck_filter_string.h" +#include "../interface/deck_loader/loaded_deck.h" #include "libcockatrice/card/card_localization.h" #include "libcockatrice/card/database/card_database_querier.h" #include "libcockatrice/deck_list/deck_list.h" @@ -14,7 +15,6 @@ #include #include #include -#include #include #include #include diff --git a/cockatrice/src/filters/deck_filter_string.h b/cockatrice/src/filters/deck_filter_string.h index b27cf87ca..76b891b59 100644 --- a/cockatrice/src/filters/deck_filter_string.h +++ b/cockatrice/src/filters/deck_filter_string.h @@ -11,7 +11,6 @@ #include #include #include -#include struct CardSearchLanguage; struct LoadedDeck; diff --git a/cockatrice/src/filters/filter_builder.cpp b/cockatrice/src/filters/filter_builder.cpp index 9c2a91a5b..e785d7f1a 100644 --- a/cockatrice/src/filters/filter_builder.cpp +++ b/cockatrice/src/filters/filter_builder.cpp @@ -13,7 +13,6 @@ #include #include #include -#include #include FilterBuilder::FilterBuilder(QWidget *parent) : QWidget(parent) @@ -40,8 +39,9 @@ FilterBuilder::FilterBuilder(QWidget *parent) : QWidget(parent) edit->setClearButtonEnabled(true); edit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); - QGridLayout *layout = QtUtils::makeLayout(); + QGridLayout *layout = new QGridLayout; layout->setObjectName("layout"); + layout->setContentsMargins(0, 0, 0, 0); layout->addWidget(typeCombo, 0, 0, 1, 2); layout->addWidget(filterCombo, 0, 2, 1, 2); diff --git a/cockatrice/src/filters/filter_tree_model.cpp b/cockatrice/src/filters/filter_tree_model.cpp index 9b55ce2fd..b3bf307f1 100644 --- a/cockatrice/src/filters/filter_tree_model.cpp +++ b/cockatrice/src/filters/filter_tree_model.cpp @@ -11,27 +11,6 @@ class QObject; -namespace -{ -/** - * @brief Visits every filter item below @p node, depth first. - */ -template void forEachFilter(const FilterTreeNode *node, const Callback &callback) -{ - if (node == nullptr) { - return; - } - - if (const auto *filterItem = dynamic_cast(node)) { - callback(filterItem); - } - - for (int i = 0; i < node->childCount(); ++i) { - forEachFilter(node->nodeAt(i), callback); - } -} -} // namespace - FilterTreeModel::FilterTreeModel(QObject *parent) : QAbstractItemModel(parent) { fTree = new FilterTree; @@ -131,31 +110,46 @@ void FilterTreeModel::clearFiltersOfType(CardFilter::Attr filterType) QList FilterTreeModel::getFiltersOfType(CardFilter::Attr filterType) const { QList filters; - forEachFilter(fTree, [&](const FilterItem *filterItem) { - if (filterItem->attr() == filterType) { - filters.append(new CardFilter(filterItem->text(), filterItem->type(), filterItem->attr())); - } - }); - return filters; -} + if (!fTree) { + return filters; + } -QStringList FilterTreeModel::getFilterTermsOfType(CardFilter::Attr filterType) const -{ - QStringList terms; - forEachFilter(fTree, [&](const FilterItem *filterItem) { - if (filterItem->attr() == filterType) { - terms.append(filterItem->text()); + std::function traverse; + traverse = [&](const FilterTreeNode *node) { + if (const auto *filterItem = dynamic_cast(node)) { + if (filterItem->attr() == filterType) { + QString text = filterItem->text(); + filters.append(new CardFilter(text, filterItem->type(), filterItem->attr())); + } } - }); - return terms; + for (int i = 0; i < node->childCount(); ++i) { + traverse(node->nodeAt(i)); + } + }; + + traverse(fTree); + return filters; } QList FilterTreeModel::allFilters() const { QList filters; - forEachFilter(fTree, [&](const FilterItem *filterItem) { - filters.append(new CardFilter(filterItem->text(), filterItem->type(), filterItem->attr())); - }); + if (!fTree) { + return filters; + } + + std::function traverse; + traverse = [&](const FilterTreeNode *node) { + if (const auto *filterItem = dynamic_cast(node)) { + QString text = filterItem->text(); + filters.append(new CardFilter(text, filterItem->type(), filterItem->attr())); + } + for (int i = 0; i < node->childCount(); ++i) { + traverse(node->nodeAt(i)); + } + }; + + traverse(fTree); return filters; } diff --git a/cockatrice/src/filters/filter_tree_model.h b/cockatrice/src/filters/filter_tree_model.h index 05f34f508..826fcc941 100644 --- a/cockatrice/src/filters/filter_tree_model.h +++ b/cockatrice/src/filters/filter_tree_model.h @@ -28,14 +28,7 @@ public slots: void addFilter(const CardFilter *f); void removeFilter(const CardFilter *f); void clearFiltersOfType(CardFilter::Attr filterType); - /** - * @brief Copies every active filter of one attribute into new CardFilter objects. - * - * The caller owns the returned filters. Prefer getFilterTermsOfType() when only - * the terms are needed. - */ [[nodiscard]] QList getFiltersOfType(CardFilter::Attr filterType) const; - /// @brief Copies every active filter into new CardFilter objects. The caller owns them. [[nodiscard]] QList allFilters() const; private slots: @@ -55,13 +48,6 @@ public: { return fTree; } - - /** - * @brief The texts of every active filter of one attribute, without allocating filters. - * - * Prefer this over getFiltersOfType() when only the filter terms are needed. - */ - [[nodiscard]] QStringList getFilterTermsOfType(CardFilter::Attr filterType) const; [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; [[nodiscard]] int columnCount(const QModelIndex & /*parent*/ = QModelIndex()) const override; [[nodiscard]] QVariant data(const QModelIndex &index, int role) const override; diff --git a/libcockatrice_game/libcockatrice/game/abstract_game.cpp b/cockatrice/src/game/abstract_game.cpp similarity index 88% rename from libcockatrice_game/libcockatrice/game/abstract_game.cpp rename to cockatrice/src/game/abstract_game.cpp index a93f44018..efc575eff 100644 --- a/libcockatrice_game/libcockatrice/game/abstract_game.cpp +++ b/cockatrice/src/game/abstract_game.cpp @@ -18,6 +18,8 @@ AbstractGame::AbstractGame(QObject *_parent) : QObject(_parent) { gameMetaInfo = new GameMetaInfo(this); gameEventHandler = new GameEventHandler(this); + + activeCard = nullptr; } bool AbstractGame::isHost() const @@ -46,7 +48,12 @@ void AbstractGame::loadReplay(const GameReplay *replay) gameMetaInfo->setSpectatorsOmniscient(true); } -CardState *AbstractGame::getCard(int playerId, const QString &zoneName, int cardId) const +void AbstractGame::setActiveCard(CardItem *card) +{ + activeCard = card; +} + +CardItem *AbstractGame::getCard(int playerId, const QString &zoneName, int cardId) const { PlayerLogic *player = playerManager->getPlayer(playerId); if (!player) { diff --git a/libcockatrice_game/libcockatrice/game/abstract_game.h b/cockatrice/src/game/abstract_game.h similarity index 82% rename from libcockatrice_game/libcockatrice/game/abstract_game.h rename to cockatrice/src/game/abstract_game.h index 00a95a3b8..326af8ac9 100644 --- a/libcockatrice_game/libcockatrice/game/abstract_game.h +++ b/cockatrice/src/game/abstract_game.h @@ -11,14 +11,14 @@ #include #include +class CardItem; class AbstractClient; -class CardState; class GameEventHandler; class GameMetaInfo; class GameReplay; class GameState; class PlayerManager; -class CardState; + class AbstractGame : public QObject { Q_OBJECT @@ -30,6 +30,7 @@ public: GameState *gameState; GameEventHandler *gameEventHandler; PlayerManager *playerManager; + CardItem *activeCard; GameMetaInfo *getGameMetaInfo() { @@ -57,7 +58,13 @@ public: void loadReplay(const GameReplay *replay); - CardState *getCard(int playerId, const QString &zoneName, int cardId) const; + CardItem *getCard(int playerId, const QString &zoneName, int cardId) const; + + void setActiveCard(CardItem *card); + CardItem *getActiveCard() const + { + return activeCard; + } }; #endif // COCKATRICE_ABSTRACT_GAME_H diff --git a/cockatrice/src/game_graphics/board/arrow_registry.cpp b/cockatrice/src/game/arrow_registry.cpp similarity index 93% rename from cockatrice/src/game_graphics/board/arrow_registry.cpp rename to cockatrice/src/game/arrow_registry.cpp index 10b3cde2f..c05134a75 100644 --- a/cockatrice/src/game_graphics/board/arrow_registry.cpp +++ b/cockatrice/src/game/arrow_registry.cpp @@ -1,9 +1,9 @@ #include "arrow_registry.h" -#include "arrow_item.h" +#include "../game_graphics/board/arrow_item.h" +#include "board/arrow_data.h" #include -#include void ArrowRegistry::insert(QSharedPointer data, ArrowItem *arrow) { diff --git a/cockatrice/src/game_graphics/board/arrow_registry.h b/cockatrice/src/game/arrow_registry.h similarity index 94% rename from cockatrice/src/game_graphics/board/arrow_registry.h rename to cockatrice/src/game/arrow_registry.h index 7103747e8..ca3b04090 100644 --- a/cockatrice/src/game_graphics/board/arrow_registry.h +++ b/cockatrice/src/game/arrow_registry.h @@ -4,7 +4,6 @@ #include #include #include -#include class ArrowItem; struct ArrowData; diff --git a/libcockatrice_game/libcockatrice/game/board/arrow_data.cpp b/cockatrice/src/game/board/arrow_data.cpp similarity index 100% rename from libcockatrice_game/libcockatrice/game/board/arrow_data.cpp rename to cockatrice/src/game/board/arrow_data.cpp diff --git a/libcockatrice_game/libcockatrice/game/board/arrow_data.h b/cockatrice/src/game/board/arrow_data.h similarity index 100% rename from libcockatrice_game/libcockatrice/game/board/arrow_data.h rename to cockatrice/src/game/board/arrow_data.h diff --git a/libcockatrice_game/libcockatrice/game/board/card_list.cpp b/cockatrice/src/game/board/card_list.cpp similarity index 70% rename from libcockatrice_game/libcockatrice/game/board/card_list.cpp rename to cockatrice/src/game/board/card_list.cpp index 361c51502..94bae67cd 100644 --- a/libcockatrice_game/libcockatrice/game/board/card_list.cpp +++ b/cockatrice/src/game/board/card_list.cpp @@ -1,6 +1,7 @@ #include "card_list.h" -#include "card_state.h" +#include "../../game_graphics/board/card_item.h" +#include "libcockatrice/card/printing/exact_card.h" #include #include @@ -10,26 +11,26 @@ #include #include -CardList::CardList(bool _contentsKnown) : QList(), contentsKnown(_contentsKnown) +CardList::CardList(bool _contentsKnown) : QList(), contentsKnown(_contentsKnown) { } /** - * @brief Finds the CardState with the given id in the list. + * @brief Finds the CardItem with the given id in the list. * If contentsKnown is false, then this just returns the first element of the list. * * @param cardId The id of the card to find. * - * @returns A pointer to the CardState, or a nullptr if not found. + * @returns A pointer to the CardItem, or a nullptr if not found. */ -CardState *CardList::findCard(const int cardId) const +CardItem *CardList::findCard(const int cardId) const { if (!contentsKnown && !empty()) { return at(0); } else { - for (auto *cardState : *this) { - if (cardState->getId() == cardId) { - return cardState; + for (auto *cardItem : *this) { + if (cardItem->getId() == cardId) { + return cardItem; } } } @@ -37,7 +38,7 @@ CardState *CardList::findCard(const int cardId) const } /** - * @brief sorts the list by using string comparison on properties extracted from the CardState + * @brief sorts the list by using string comparison on properties extracted from the CardItem * The cards are compared using each property in order. * If two cards have the same value for a property, then the next property in the list is used. * @@ -50,7 +51,7 @@ void CardList::sortBy(const QList &option) return; } - auto comparator = [&option](CardState *a, CardState *b) { + auto comparator = [&option](CardItem *a, CardItem *b) { for (auto prop : option) { auto extractor = getExtractorFor(prop); QString t1 = extractor(a); @@ -115,26 +116,26 @@ static QString getColorSortString(const CardInfo &c, bool appendAtEnd) } /** - * @brief returns the function that extracts the given property from the CardState. + * @brief returns the function that extracts the given property from the CardItem. */ -std::function CardList::getExtractorFor(SortOption option) +std::function CardList::getExtractorFor(SortOption option) { switch (option) { case NoSort: - return [](CardState *) { return ""; }; + return [](CardItem *) { return ""; }; case SortByMainType: - return [](CardState *c) { return c->getCardInfo().getMainCardType(); }; + return [](CardItem *c) { return c->getCardInfo().getMainCardType(); }; case SortByManaValue: // getCmc returns the int as a string. We pad with 0's so that string comp also works on it - return [](CardState *c) { return c->getCard() ? c->getCardInfo().getCmc().rightJustified(4, '0') : ""; }; + return [](CardItem *c) { return c->getCard() ? c->getCardInfo().getCmc().rightJustified(4, '0') : ""; }; case SortByColorGrouping: - return [](CardState *c) { return c->getCard() ? getColorSortString(c->getCardInfo(), false) : ""; }; + return [](CardItem *c) { return c->getCard() ? getColorSortString(c->getCardInfo(), false) : ""; }; case SortByName: - return [](CardState *c) { return c->getName(); }; + return [](CardItem *c) { return c->getName(); }; case SortByType: - return [](CardState *c) { return c->getCardInfo().getCardType(); }; + return [](CardItem *c) { return c->getCardInfo().getCardType(); }; case SortByManaCost: - return [](CardState *c) { + return [](CardItem *c) { if (!c->getCard()) { return QString(); } @@ -146,18 +147,18 @@ std::function CardList::getExtractorFor(SortOption option) return QString("%1%2").arg(info.getCmc(), 4, QChar('0')).arg(info.getManaCost()); }; case SortByColors: - return [](CardState *c) { return c->getCard() ? getColorSortString(c->getCardInfo(), true) : ""; }; + return [](CardItem *c) { return c->getCard() ? getColorSortString(c->getCardInfo(), true) : ""; }; case SortByPt: // do the same padding trick as above return - [](CardState *c) { return c->getCard() ? c->getCardInfo().getPowTough().rightJustified(10, '0') : ""; }; + [](CardItem *c) { return c->getCard() ? c->getCardInfo().getPowTough().rightJustified(10, '0') : ""; }; case SortBySet: - return [](CardState *c) { return c->getCardInfo().getSetsNames(); }; + return [](CardItem *c) { return c->getCardInfo().getSetsNames(); }; case SortByPrinting: - return [](CardState *c) { return c->getProviderId(); }; + return [](CardItem *c) { return c->getProviderId(); }; } // this line should never be reached qCWarning(CardListLog) << "cardlist.cpp: Could not find extractor for SortOption" << option; - return [](CardState *) { return ""; }; + return [](CardItem *) { return ""; }; } \ No newline at end of file diff --git a/libcockatrice_game/libcockatrice/game/board/card_list.h b/cockatrice/src/game/board/card_list.h similarity index 84% rename from libcockatrice_game/libcockatrice/game/board/card_list.h rename to cockatrice/src/game/board/card_list.h index b3037d0f6..b5427df0d 100644 --- a/libcockatrice_game/libcockatrice/game/board/card_list.h +++ b/cockatrice/src/game/board/card_list.h @@ -14,9 +14,9 @@ inline Q_LOGGING_CATEGORY(CardListLog, "card_list"); -class CardState; +class CardItem; -class CardList : public QList +class CardList : public QList { protected: bool contentsKnown; @@ -43,7 +43,7 @@ public: SortByPrinting }; explicit CardList(bool _contentsKnown); - CardState *findCard(const int cardId) const; + CardItem *findCard(const int cardId) const; bool getContentsKnown() const { return contentsKnown; @@ -51,7 +51,7 @@ public: void sortBy(const QList &options); - static std::function getExtractorFor(SortOption option); + static std::function getExtractorFor(SortOption option); }; #endif diff --git a/cockatrice/src/game/board/card_state.cpp b/cockatrice/src/game/board/card_state.cpp new file mode 100644 index 000000000..3f879a849 --- /dev/null +++ b/cockatrice/src/game/board/card_state.cpp @@ -0,0 +1,113 @@ +#include "card_state.h" + +#include + +void CardState::resetState(bool keepAnnotations) +{ + attacking = false; + counters.clear(); + pt.clear(); + if (!keepAnnotations) { + annotation.clear(); + } + attachedTo = nullptr; +} + +void CardState::setZone(CardZoneLogic *_zone) +{ + if (zone == _zone) { + return; + } + + zone = _zone; + emit zoneChanged(this, zone); + emit stateChanged(); +} + +void CardState::setAttacking(bool _attacking) +{ + if (attacking == _attacking) { + return; + } + attacking = _attacking; + emit attackingChanged(_attacking); + emit stateChanged(); +} + +void CardState::insertCounter(int id, int value) +{ + counters.insert(id, value); + + emit countersChanged(counters); + emit stateChanged(); +} + +void CardState::setCounter(int id, int value) +{ + if (value) { + counters[id] = value; + } else { + counters.remove(id); + } + + emit countersChanged(counters); + emit stateChanged(); +} + +void CardState::clearCounters() +{ + counters.clear(); + emit countersChanged(counters); + emit stateChanged(); +} + +void CardState::setAnnotation(const QString &_annotation) +{ + if (annotation == _annotation) { + return; + } + annotation = _annotation; + emit annotationChanged(annotation); + emit stateChanged(); +} + +void CardState::setPT(const QString &_pt) +{ + if (pt == _pt) { + return; + } + pt = _pt; + emit ptChanged(pt); + emit stateChanged(); +} + +void CardState::setDoesntUntap(bool _doesntUntap) +{ + if (doesntUntap == _doesntUntap) { + return; + } + doesntUntap = _doesntUntap; + emit doesntUntapChanged(_doesntUntap); + emit stateChanged(); +} + +void CardState::setDestroyOnZoneChange(bool _destroyOnZoneChange) +{ + if (destroyOnZoneChange == _destroyOnZoneChange) { + return; + } + + destroyOnZoneChange = _destroyOnZoneChange; + emit destroyOnZoneChangeChanged(_destroyOnZoneChange); + emit stateChanged(); +} + +void CardState::setAttachedTo(CardItem *_attachedTo) +{ + if (attachedTo == _attachedTo) { + return; + } + attachedTo = _attachedTo; + emit attachedToChanged(_attachedTo); + emit stateChanged(); +} \ No newline at end of file diff --git a/cockatrice/src/game/board/card_state.h b/cockatrice/src/game/board/card_state.h new file mode 100644 index 000000000..ca7aaf927 --- /dev/null +++ b/cockatrice/src/game/board/card_state.h @@ -0,0 +1,106 @@ +#ifndef COCKATRICE_CARD_STATE_H +#define COCKATRICE_CARD_STATE_H + +#include +#include +#include +#include + +class CardZoneLogic; +class CardItem; + +class CardState : public QObject +{ + Q_OBJECT + +private: + bool attacking = false; + QMap counters; + QString annotation; + QString pt; + bool doesntUntap = false; + bool destroyOnZoneChange = false; + + CardItem *attachedTo = nullptr; + CardZoneLogic *zone = nullptr; + +signals: + void stateChanged(); + + void attackingChanged(bool newValue); + void countersChanged(const QMap &newCounters); + void annotationChanged(const QString &newAnnotation); + void ptChanged(const QString &newPt); + void doesntUntapChanged(bool newValue); + void destroyOnZoneChangeChanged(bool newValue); + void attachedToChanged(CardItem *newAttachedTo); + void zoneChanged(CardState *changedCard, CardZoneLogic *newZone); + +public: + explicit CardState(QObject *parent, CardZoneLogic *_zone) : QObject(parent), zone(_zone) + { + } + + void resetState(bool keepAnnotations); + + CardZoneLogic *getZone() const + { + return zone; + } + + void setZone(CardZoneLogic *_zone); + + bool getAttacking() const + { + return attacking; + } + void setAttacking(bool _attacking); + + const QMap &getCounters() const + { + return counters; + } + + void insertCounter(int id, int value); + + void setCounter(int id, int value); + + void clearCounters(); + + QString getAnnotation() const + { + return annotation; + } + + void setAnnotation(const QString &_annotation); + + QString getPT() const + { + return pt; + } + + void setPT(const QString &_pt); + + bool getDoesntUntap() const + { + return doesntUntap; + } + + void setDoesntUntap(bool _doesntUntap); + + bool getDestroyOnZoneChange() const + { + return destroyOnZoneChange; + } + + void setDestroyOnZoneChange(bool _destroyOnZoneChange); + + CardItem *getAttachedTo() const + { + return attachedTo; + } + + void setAttachedTo(CardItem *_attachedTo); +}; + +#endif // COCKATRICE_CARD_STATE_H diff --git a/libcockatrice_game/libcockatrice/game/board/counter_state.cpp b/cockatrice/src/game/board/counter_state.cpp similarity index 100% rename from libcockatrice_game/libcockatrice/game/board/counter_state.cpp rename to cockatrice/src/game/board/counter_state.cpp diff --git a/libcockatrice_game/libcockatrice/game/board/counter_state.h b/cockatrice/src/game/board/counter_state.h similarity index 100% rename from libcockatrice_game/libcockatrice/game/board/counter_state.h rename to cockatrice/src/game/board/counter_state.h diff --git a/libcockatrice_game/libcockatrice/game/game.cpp b/cockatrice/src/game/game.cpp similarity index 75% rename from libcockatrice_game/libcockatrice/game/game.cpp rename to cockatrice/src/game/game.cpp index a1a4a31a8..7343d052a 100644 --- a/libcockatrice_game/libcockatrice/game/game.cpp +++ b/cockatrice/src/game/game.cpp @@ -16,16 +16,13 @@ Game::Game(QObject *_parent, bool isLocalGame, QList &_clients, const Event_GameJoined &event, - const QMap &_roomGameTypes, - IInterfaceSettingsProvider *interfaceSettings, - ICardPreferenceProvider *cardPreferences) + const QMap &_roomGameTypes) : AbstractGame(_parent) { gameMetaInfo->setFromProto(event.game_info()); gameMetaInfo->setRoomGameTypes(_roomGameTypes); gameState = new GameState(this, 0, event.host_id(), isLocalGame, _clients, false, event.resuming(), -1, false); connect(gameMetaInfo, &GameMetaInfo::startedChanged, gameState, &GameState::onStartedChanged); - playerManager = new PlayerManager(this, event.player_id(), event.judge(), event.spectator(), interfaceSettings, - cardPreferences); + playerManager = new PlayerManager(this, event.player_id(), event.judge(), event.spectator()); gameMetaInfo->setStarted(false); } diff --git a/libcockatrice_game/libcockatrice/game/game.h b/cockatrice/src/game/game.h similarity index 69% rename from libcockatrice_game/libcockatrice/game/game.h rename to cockatrice/src/game/game.h index f80b75ea5..6a309cfb2 100644 --- a/libcockatrice_game/libcockatrice/game/game.h +++ b/cockatrice/src/game/game.h @@ -18,9 +18,6 @@ class QString; template class QMap; template class QList; -class ICardPreferenceProvider; -class IInterfaceSettingsProvider; - class Game : public AbstractGame { Q_OBJECT @@ -30,9 +27,7 @@ public: bool isLocalGame, QList &_clients, const Event_GameJoined &event, - const QMap &_roomGameTypes, - IInterfaceSettingsProvider *interfaceSettings = nullptr, - ICardPreferenceProvider *cardPreferences = nullptr); + const QMap &_roomGameTypes); }; #endif // COCKATRICE_GAME_H diff --git a/libcockatrice_game/libcockatrice/game/game_event_handler.cpp b/cockatrice/src/game/game_event_handler.cpp similarity index 89% rename from libcockatrice_game/libcockatrice/game/game_event_handler.cpp rename to cockatrice/src/game/game_event_handler.cpp index 856475dfa..7b08af47c 100644 --- a/libcockatrice_game/libcockatrice/game/game_event_handler.cpp +++ b/cockatrice/src/game/game_event_handler.cpp @@ -28,20 +28,12 @@ #include #include #include -#include #include -#include #include #include #include -#include #include #include -#include -#include -#include -#include -#include #include #include #include @@ -56,7 +48,6 @@ #include #include #include -#include #include #include @@ -232,89 +223,6 @@ void GameEventHandler::handleActivePhaseChanged(int phase) sendGameCommand(cmd); } -void GameEventHandler::handleUntapAll() -{ - Command_SetCardAttr cmd; - cmd.set_zone(ZoneNames::TABLE); - cmd.set_attribute(AttrTapped); - cmd.set_attr_value("0"); - - sendGameCommand(cmd); -} - -void GameEventHandler::handleDrawCard() -{ - Command_DrawCards cmd; - cmd.set_number(1); - - sendGameCommand(cmd); -} - -PendingCommand *GameEventHandler::prepareDeckSelect(const QString &deckList) -{ - Command_DeckSelect cmd; - cmd.set_deck(deckList.toStdString()); - - return prepareGameCommand(cmd); -} - -PendingCommand *GameEventHandler::prepareDeckSelectId(int deckId) -{ - Command_DeckSelect cmd; - cmd.set_deck_id(deckId); - - return prepareGameCommand(cmd); -} - -PendingCommand *GameEventHandler::prepareSetPlaymat(const PlaymatInfo &playmat) -{ - Command_SetPlaymat cmd; - auto *pp = cmd.mutable_playmat_params(); - pp->set_card_name(playmat.card.name.toStdString()); - pp->set_card_provider_id(playmat.card.providerId.toStdString()); - pp->set_margin_pct_l(playmat.params.marginPctL); - pp->set_margin_pct_r(playmat.params.marginPctR); - pp->set_vertical_offset(playmat.params.verticalOffset); - pp->set_zoom(playmat.params.zoom); - - return prepareGameCommand(cmd); -} - -void GameEventHandler::sendReadyStart(bool ready, int playerId) -{ - Command_ReadyStart cmd; - cmd.set_ready(ready); - - sendGameCommand(cmd, playerId); -} - -void GameEventHandler::sendForceStart(int playerId) -{ - Command_ReadyStart cmd; - cmd.set_force_start(true); - cmd.set_ready(true); - - sendGameCommand(cmd, playerId); -} - -void GameEventHandler::sendSetSideboardLock(bool locked, int playerId) -{ - Command_SetSideboardLock cmd; - cmd.set_locked(locked); - - sendGameCommand(cmd, playerId); -} - -void GameEventHandler::sendSetSideboardPlan(const QList &plan, int playerId) -{ - Command_SetSideboardPlan cmd; - for (const auto &zone : plan) { - cmd.add_move_list()->CopyFrom(zone); - } - - sendGameCommand(cmd, playerId); -} - void GameEventHandler::handleGameLeft() { sendGameCommand(Command_LeaveGame()); diff --git a/libcockatrice_game/libcockatrice/game/game_event_handler.h b/cockatrice/src/game/game_event_handler.h similarity index 83% rename from libcockatrice_game/libcockatrice/game/game_event_handler.h rename to cockatrice/src/game/game_event_handler.h index f7b11780b..30021d295 100644 --- a/libcockatrice_game/libcockatrice/game/game_event_handler.h +++ b/cockatrice/src/game/game_event_handler.h @@ -29,14 +29,13 @@ // GameEventContext, ServerInfo_Player and ServerInfo_User are passed by value // to signals and slots, so the moc-generated code needs the complete types. #include -#include #include #include -#include class AbstractClient; class AbstractGame; class GameEventContainer; +class GameEventContext; class PendingCommand; class PlayerLogic; class Response; @@ -128,12 +127,6 @@ public: */ void handleActivePhaseChanged(int phase); - /** @brief Request untapping every permanent on the table. */ - void handleUntapAll(); - - /** @brief Request drawing a single card. */ - void handleDrawCard(); - /** @brief Leave the current game session. */ void handleGameLeft(); @@ -152,70 +145,6 @@ public: void handleArrowDeletion(int creatorId, int arrowId); void handleArrowDeletionFinished(const Response &response, int creatorId, int arrowId); - /** - * @brief Prepare a deck selection from its native deck list text. - * - * The returned command is not sent yet, so the caller can connect to - * PendingCommand::finished before handing it to sendGameCommand(). - * - * @param deckList Serialized deck list. - * @return The pending command awaiting the deck download response. - */ - PendingCommand *prepareDeckSelect(const QString &deckList); - - /** - * @brief Prepare a deck selection for a deck stored on the server. - * - * The returned command is not sent yet, so the caller can connect to - * PendingCommand::finished before handing it to sendGameCommand(). - * - * @param deckId Server-side deck identifier. - * @return The pending command awaiting the deck download response. - */ - PendingCommand *prepareDeckSelectId(int deckId); - - /** - * @brief Build the playmat command for a player without sending it. - * - * The returned command is not sent yet, so the caller can connect to - * PendingCommand::finished before handing it to sendGameCommand(). - * - * @param playmat Resolved playmat card and positioning parameters. - * @return The pending command awaiting the playmat response. - */ - PendingCommand *prepareSetPlaymat(const PlaymatInfo &playmat); - - /** - * @brief Set a player's ready state. - * - * @param ready Whether the player is ready. - * @param playerId Player the state applies to. - */ - void sendReadyStart(bool ready, int playerId); - - /** - * @brief Force-start the game, kicking every player who is not ready. - * - * @param playerId Player issuing the force start. - */ - void sendForceStart(int playerId); - - /** - * @brief Lock or unlock a player's sideboard. - * - * @param locked Whether the sideboard is locked. - * @param playerId Player the lock applies to. - */ - void sendSetSideboardLock(bool locked, int playerId); - - /** - * @brief Send a player's sideboard plan. - * - * @param plan Cards to move between zones when leaving sideboarding. - * @param playerId Player the plan belongs to. - */ - void sendSetSideboardPlan(const QList &plan, int playerId); - /** @} */ /** @name Incoming event processing diff --git a/libcockatrice_game/libcockatrice/game/game_meta_info.cpp b/cockatrice/src/game/game_meta_info.cpp similarity index 100% rename from libcockatrice_game/libcockatrice/game/game_meta_info.cpp rename to cockatrice/src/game/game_meta_info.cpp diff --git a/libcockatrice_game/libcockatrice/game/game_meta_info.h b/cockatrice/src/game/game_meta_info.h similarity index 100% rename from libcockatrice_game/libcockatrice/game/game_meta_info.h rename to cockatrice/src/game/game_meta_info.h diff --git a/libcockatrice_game/libcockatrice/game/game_state.cpp b/cockatrice/src/game/game_state.cpp similarity index 100% rename from libcockatrice_game/libcockatrice/game/game_state.cpp rename to cockatrice/src/game/game_state.cpp diff --git a/libcockatrice_game/libcockatrice/game/game_state.h b/cockatrice/src/game/game_state.h similarity index 100% rename from libcockatrice_game/libcockatrice/game/game_state.h rename to cockatrice/src/game/game_state.h diff --git a/libcockatrice_game/libcockatrice/game/phase.cpp b/cockatrice/src/game/phase.cpp similarity index 100% rename from libcockatrice_game/libcockatrice/game/phase.cpp rename to cockatrice/src/game/phase.cpp diff --git a/libcockatrice_game/libcockatrice/game/phase.h b/cockatrice/src/game/phase.h similarity index 100% rename from libcockatrice_game/libcockatrice/game/phase.h rename to cockatrice/src/game/phase.h diff --git a/libcockatrice_game/libcockatrice/game/player/event_processing_options.h b/cockatrice/src/game/player/event_processing_options.h similarity index 100% rename from libcockatrice_game/libcockatrice/game/player/event_processing_options.h rename to cockatrice/src/game/player/event_processing_options.h diff --git a/libcockatrice_game/libcockatrice/game/player/player_actions.cpp b/cockatrice/src/game/player/player_actions.cpp similarity index 84% rename from libcockatrice_game/libcockatrice/game/player/player_actions.cpp rename to cockatrice/src/game/player/player_actions.cpp index 90823fec6..35682ede8 100644 --- a/libcockatrice_game/libcockatrice/game/player/player_actions.cpp +++ b/cockatrice/src/game/player/player_actions.cpp @@ -1,8 +1,13 @@ #include "player_actions.h" +#include "../../client/settings/cache_settings.h" +#include "../../game_graphics/board/card_item.h" +#include "../../game_graphics/dialogs/dlg_create_token.h" +#include "../../game_graphics/dialogs/dlg_move_top_cards_until.h" +#include "../../game_graphics/player/card_menu_action_type.h" +#include "../../game_graphics/zones/table_zone.h" +#include "../../interface/deck_loader/loaded_deck.h" #include "../abstract_game.h" -#include "../board/arrow_data.h" -#include "../board/card_state.h" #include "../board/counter_state.h" #include "../game_event_handler.h" #include "../zones/card_zone_logic.h" @@ -26,6 +31,8 @@ #include #include #include +#include +#include #include #include #include @@ -44,15 +51,10 @@ #include #include #include -#include -#include -#include #include #include -#include #include #include -#include #include #include #include @@ -62,12 +64,11 @@ #include #include #include -#include #include #include -#include +#include +#include #include -#include #include #include #include @@ -79,11 +80,8 @@ // milliseconds in between triggers of the move top cards until action static constexpr int MOVE_TOP_CARD_UNTIL_INTERVAL = 100; -PlayerActions::PlayerActions(PlayerLogic *_player, - IInterfaceSettingsProvider *_interfaceSettings, - ICardPreferenceProvider *_cardPreferences) - : QObject(_player), player(_player), interfaceSettings(_interfaceSettings), cardPreferences(_cardPreferences), - lastTokenTableRow(0), movingCardsUntil(false) +PlayerActions::PlayerActions(PlayerLogic *_player) + : QObject(_player), player(_player), lastTokenTableRow(0), movingCardsUntil(false) { connect(this, &PlayerActions::requestZoneViewToggle, player, &PlayerLogic::onRequestZoneViewToggle); @@ -93,27 +91,7 @@ PlayerActions::PlayerActions(PlayerLogic *_player, connect(moveTopCardTimer, &QTimer::timeout, [this]() { actMoveTopCardToPlay(); }); } -bool PlayerActions::getPlayToStack() const -{ - return interfaceSettings ? interfaceSettings->getPlayToStack() : true; -} - -int PlayerActions::getStartingHandSize() const -{ - return interfaceSettings ? interfaceSettings->getStartingHandSize() : 7; -} - -bool PlayerActions::getAnnotateTokens() const -{ - return interfaceSettings ? interfaceSettings->getAnnotateTokens() : false; -} - -QString PlayerActions::getCardPreferenceOverride(const QString &cardName) const -{ - return cardPreferences ? cardPreferences->getCardPreferenceOverride(cardName) : QString(); -} - -void PlayerActions::playCard(CardState *card, bool faceDown) +void PlayerActions::playCard(CardItem *card, bool faceDown) { if (card == nullptr) { return; @@ -134,7 +112,7 @@ void PlayerActions::playCard(CardState *card, bool faceDown) const CardInfo &info = exactCard.getInfo(); int tableRow = info.getUiAttributes().tableRow; - bool playToStack = getPlayToStack(); + bool playToStack = SettingsCache::instance().userInterface().getPlayToStack(); QString currentZone = card->getZone()->getName(); if (!faceDown && currentZone == ZoneNames::STACK && tableRow == 3) { cmd.set_target_zone(ZoneNames::GRAVE); @@ -147,7 +125,7 @@ void PlayerActions::playCard(CardState *card, bool faceDown) cmd.set_y(0); } else { tableRow = faceDown ? 2 : info.getUiAttributes().tableRow; - QPoint gridPoint = QPoint(-1, TableZoneLogic::tableRowToGridY(tableRow)); + QPoint gridPoint = QPoint(-1, TableZone::tableRowToGridY(tableRow)); cardToMove->set_face_down(faceDown); if (!faceDown) { cardToMove->set_pt(info.getPowTough().toStdString()); @@ -166,7 +144,7 @@ void PlayerActions::playCard(CardState *card, bool faceDown) * Like {@link PlayerActions::playCard}, but forces the card to be played to the table zone. * Cards with tablerow 3 (the stack) will be played to tablerow 1 (the noncreatures row). */ -void PlayerActions::playCardToTable(const CardState *card, bool faceDown) +void PlayerActions::playCardToTable(const CardItem *card, bool faceDown) { if (card == nullptr) { return; @@ -187,7 +165,7 @@ void PlayerActions::playCardToTable(const CardState *card, bool faceDown) const CardInfo &info = exactCard.getInfo(); int tableRow = faceDown ? 2 : info.getUiAttributes().tableRow; - QPoint gridPoint = QPoint(-1, TableZoneLogic::tableRowToGridY(tableRow)); + QPoint gridPoint = QPoint(-1, TableZone::tableRowToGridY(tableRow)); cardToMove->set_face_down(faceDown); if (!faceDown) { cardToMove->set_pt(info.getPowTough().toStdString()); @@ -199,60 +177,6 @@ void PlayerActions::playCardToTable(const CardState *card, bool faceDown) sendGameCommand(cmd); } -void PlayerActions::attachCards(CardState *startCard, const CardState *targetCard) -{ - if (targetCard->getAttachedTo() || targetCard->getZone()->getName() != ZoneNames::TABLE) { - return; - } - - // move card onto table first if attaching from some other zone - if (startCard->getZone()->getName() != ZoneNames::TABLE) { - playCardToTable(startCard, false); - } - - Command_AttachCard cmd; - cmd.set_start_zone(ZoneNames::TABLE); - cmd.set_card_id(startCard->getId()); - cmd.set_target_player_id(targetCard->getZone()->getPlayer()->getPlayerInfo()->getId()); - cmd.set_target_zone(targetCard->getZone()->getName().toStdString()); - cmd.set_target_card_id(targetCard->getId()); - sendGameCommand(cmd); -} - -void PlayerActions::createArrow(CardState *startCard, const ArrowData &arrow, int deleteInPhase) -{ - Command_CreateArrow cmd; - cmd.mutable_arrow_color()->CopyFrom(convertQColorToColor(arrow.color)); - cmd.set_start_player_id(arrow.startPlayerId); - cmd.set_start_zone(arrow.startZone.toStdString()); - cmd.set_start_card_id(arrow.startCardId); - cmd.set_target_player_id(arrow.targetPlayerId); - - if (!arrow.isPlayerTargeted()) { - cmd.set_target_zone(arrow.targetZone.toStdString()); - cmd.set_target_card_id(arrow.targetCardId); - } - - // if the card is in hand then it has just been moved to the stack or table as part of drawing the arrow - if (arrow.startZone == ZoneNames::HAND) { - CardInfoPtr ci = startCard->getCard().getCardPtr(); - bool playToStack = getPlayToStack(); - if (ci && ((!playToStack && ci->getUiAttributes().tableRow == 3) || - (playToStack && ci->getUiAttributes().tableRow != 0 && - startCard->getZone()->getName() != ZoneNames::STACK))) { - cmd.set_start_zone(ZoneNames::STACK); - } else { - cmd.set_start_zone(playToStack ? ZoneNames::STACK : ZoneNames::TABLE); - } - } - - if (deleteInPhase != 0) { - cmd.set_delete_in_phase(deleteInPhase); - } - - sendGameCommand(cmd); -} - void PlayerActions::actViewLibrary() { emit requestZoneViewToggle(ZoneNames::DECK, -1); @@ -431,7 +355,7 @@ void PlayerActions::actDrawCard() void PlayerActions::actRequestMulliganDialog() { - int startSize = getStartingHandSize(); + int startSize = SettingsCache::instance().userInterface().getStartingHandSize(); int handSize = player->getHandZone()->getCards().size(); int deckSize = player->getDeckZone()->getCards().size() + handSize; @@ -447,7 +371,7 @@ void PlayerActions::actMulligan(int number) } doMulligan(number); - emit startingHandSizeChosen(number); + SettingsCache::instance().userInterface().setStartingHandSize(number); } void PlayerActions::actMulliganSameSize() @@ -625,7 +549,7 @@ void PlayerActions::moveTopCardsUntil(const QString &expr, MoveTopCardsUntilOpti } } -void PlayerActions::moveOneCardUntil(CardState *card) +void PlayerActions::moveOneCardUntil(CardItem *card) { moveTopCardTimer->stop(); @@ -817,6 +741,61 @@ void PlayerActions::actMoveBottomCardToTop() sendGameCommand(cmd); } +/** + * Selects all cards in the given zone. + * + * @param zone The zone to select from + * @param filter A predicate to filter which cards are selected. Defaults to always returning true. + */ +static void selectCardsInZone( + const CardZoneLogic *zone, + std::function filter = [](const CardItem *) { return true; }) +{ + if (!zone) { + return; + } + + for (auto &cardItem : zone->getCards()) { + if (cardItem && filter(cardItem)) { + cardItem->setSelected(true); + } + } +} + +void PlayerActions::actSelectAll() +{ + const CardItem *card = player->getGame()->getActiveCard(); + if (!card) { + return; + } + + selectCardsInZone(card->getZone()); +} + +void PlayerActions::actSelectRow() +{ + const CardItem *card = player->getGame()->getActiveCard(); + if (!card) { + return; + } + + auto isSameRow = [card](const CardItem *cardItem) { + return qAbs(card->scenePos().y() - cardItem->scenePos().y()) < 50; + }; + selectCardsInZone(card->getZone(), isSameRow); +} + +void PlayerActions::actSelectColumn() +{ + const CardItem *card = player->getGame()->getActiveCard(); + if (!card) { + return; + } + + auto isSameColumn = [card](const CardItem *cardItem) { return cardItem->x() == card->x(); }; + selectCardsInZone(card->getZone(), isSameColumn); +} + void PlayerActions::actDrawBottomCard() { if (player->getDeckZone()->getCards().empty()) { @@ -950,7 +929,7 @@ void PlayerActions::actCreateToken(TokenInfo tokenToCreate) if (correctedCard) { lastTokenInfo.name = correctedCard.getName(); int tableRow = lastTokenInfo.faceDown ? 2 : correctedCard.getInfo().getUiAttributes().tableRow; - lastTokenTableRow = TableZoneLogic::tableRowToGridY(tableRow); + lastTokenTableRow = TableZone::tableRowToGridY(tableRow); if (lastTokenInfo.pt.isEmpty()) { lastTokenInfo.pt = correctedCard.getInfo().getPowTough(); } @@ -996,14 +975,15 @@ void PlayerActions::setLastTokenInfo(CardInfoPtr cardInfo) return; } - lastTokenInfo = {.name = cardInfo->getName(), - .color = cardInfo->getColors().isEmpty() ? QString() : cardInfo->getColors().left(1).toLower(), - .pt = cardInfo->getPowTough(), - .annotation = getAnnotateTokens() ? cardInfo->getText() : "", - .destroy = true, - .providerId = getCardPreferenceOverride(cardInfo->getName())}; + lastTokenInfo = { + .name = cardInfo->getName(), + .color = cardInfo->getColors().isEmpty() ? QString() : cardInfo->getColors().left(1).toLower(), + .pt = cardInfo->getPowTough(), + .annotation = SettingsCache::instance().userInterface().getAnnotateTokens() ? cardInfo->getText() : "", + .destroy = true, + .providerId = SettingsCache::instance().cardOverrides().getCardPreferenceOverride(cardInfo->getName())}; - lastTokenTableRow = TableZoneLogic::tableRowToGridY(cardInfo->getUiAttributes().tableRow); + lastTokenTableRow = TableZone::tableRowToGridY(cardInfo->getUiAttributes().tableRow); emit requestEnableAndSetCreateAnotherTokenAction(lastTokenInfo.name); } @@ -1021,20 +1001,25 @@ void PlayerActions::actCreatePredefinedToken() actCreateAnotherToken(); } -void PlayerActions::actCreateRelatedCard(const CardState *sourceCard, int index) +void PlayerActions::actCreateRelatedCard() { + const CardItem *sourceCard = player->getGame()->getActiveCard(); + if (!sourceCard) { return; } + auto *action = static_cast(sender()); + // If there is a better way of passing a CardRelation through a QAction, please add it here. auto relatedCards = sourceCard->getCardInfo().getAllRelatedCards(); - CardRelation *cardRelation = relatedCards.at(index); + CardRelation *cardRelation = relatedCards.at(action->data().toInt()); actRequestCreateRelatedFromRelationDialog(sourceCard, cardRelation); } -void PlayerActions::actCreateAllRelatedCards(const CardState *sourceCard) +void PlayerActions::actCreateAllRelatedCards() { + const CardItem *sourceCard = player->getGame()->getActiveCard(); if (!sourceCard) { return; } @@ -1120,13 +1105,13 @@ void PlayerActions::actCreateAllRelatedCards(const CardState *sourceCard) } } -void PlayerActions::actRequestCreateRelatedFromRelationDialog(const CardState *sourceCard, +void PlayerActions::actRequestCreateRelatedFromRelationDialog(const CardItem *sourceCard, const CardRelation *cardRelation) { emit requestCreateRelatedFromRelationDialog(sourceCard, cardRelation); } -bool PlayerActions::createRelatedFromRelation(const CardState *sourceCard, +bool PlayerActions::createRelatedFromRelation(const CardItem *sourceCard, const CardRelation *cardRelation, int variableCount) { @@ -1178,7 +1163,7 @@ bool PlayerActions::createRelatedFromRelation(const CardState *sourceCard, return true; } -void PlayerActions::onRelatedCardCreated(const CardState *sourceCard, const CardRelation *cardRelation) +void PlayerActions::onRelatedCardCreated(const CardItem *sourceCard, const CardRelation *cardRelation) { if (sourceCard == nullptr || cardRelation == nullptr) { return; @@ -1198,7 +1183,7 @@ void PlayerActions::onRelatedCardCreated(const CardState *sourceCard, const Card setLastToken(relatedCard.getCardPtr()); } -void PlayerActions::createCard(const CardState *sourceCard, +void PlayerActions::createCard(const CardItem *sourceCard, const QString &dbCardName, CardRelationType attachType, bool persistent, @@ -1210,7 +1195,7 @@ void PlayerActions::createCard(const CardState *sourceCard, return; } - QPoint gridPoint = QPoint(-1, TableZoneLogic::tableRowToGridY(cardInfo->getUiAttributes().tableRow)); + QPoint gridPoint = QPoint(-1, TableZone::tableRowToGridY(cardInfo->getUiAttributes().tableRow)); // create the token for the related card Command_CreateToken cmd; @@ -1229,7 +1214,7 @@ void PlayerActions::createCard(const CardState *sourceCard, } cmd.set_pt(cardInfo->getPowTough().toStdString()); - if (getAnnotateTokens()) { + if (SettingsCache::instance().userInterface().getAnnotateTokens()) { cmd.set_annotation(cardInfo->getText().toStdString()); } else { cmd.set_annotation(""); @@ -1284,7 +1269,7 @@ void PlayerActions::actRequestMoveCardXCardsFromTopDialog() emit requestMoveCardXCardsFromTopDialog(defaultNumberTopCardsToPlaceBelow, deckSize); } -void PlayerActions::actMoveCardXCardsFromTop(QList selectedCards, int number) +void PlayerActions::actMoveCardXCardsFromTop(QList selectedCards, int number) { defaultNumberTopCardsToPlaceBelow = number; @@ -1318,14 +1303,14 @@ void PlayerActions::actMoveCardXCardsFromTop(QList selectedCards, i } } -void PlayerActions::actIncPT(QList selectedCards, int deltaP, int deltaT) +void PlayerActions::actIncPT(QList selectedCards, int deltaP, int deltaT) { int playerid = player->getPlayerInfo()->getId(); QList commandList; for (auto card : selectedCards) { QString pt = card->getPT(); - const auto ptList = CardState::parsePT(pt); + const auto ptList = CardItem::parsePT(pt); QString newpt; if (ptList.isEmpty()) { newpt = QString::number(deltaP) + (deltaT ? "/" + QString::number(deltaT) : ""); @@ -1351,7 +1336,7 @@ void PlayerActions::actIncPT(QList selectedCards, int deltaP, int d player->getGame()->getGameEventHandler()->sendGameCommand(prepareGameCommand(commandList), playerid); } -void PlayerActions::actResetPT(QList selectedCards) +void PlayerActions::actResetPT(QList selectedCards) { int playerid = player->getPlayerInfo()->getId(); QList commandList; @@ -1384,7 +1369,7 @@ void PlayerActions::actResetPT(QList selectedCards) } } -void PlayerActions::actRequestSetPTDialog(QList selectedCards) +void PlayerActions::actRequestSetPTDialog(QList selectedCards) { QString oldPT; @@ -1397,11 +1382,11 @@ void PlayerActions::actRequestSetPTDialog(QList selectedCards) emit requestSetPTDialog(oldPT); } -void PlayerActions::actSetPT(QList selectedCards, const QString &pt) +void PlayerActions::actSetPT(QList selectedCards, const QString &pt) { int playerid = player->getPlayerInfo()->getId(); - const auto ptList = CardState::parsePT(pt); + const auto ptList = CardItem::parsePT(pt); bool empty = ptList.isEmpty(); QList commandList; @@ -1409,7 +1394,7 @@ void PlayerActions::actSetPT(QList selectedCards, const QString &pt auto *cmd = new Command_SetCardAttr; QString newpt = QString(); if (!empty) { - const auto oldpt = CardState::parsePT(card->getPT()); + const auto oldpt = CardItem::parsePT(card->getPT()); int ptIter = 0; for (const auto &_item : ptList) { if (_item.typeId() == QMetaType::Type::Int) { @@ -1437,65 +1422,55 @@ void PlayerActions::actSetPT(QList selectedCards, const QString &pt player->getGame()->getGameEventHandler()->sendGameCommand(prepareGameCommand(commandList), playerid); } -void PlayerActions::actIncP(QList selectedCards) +void PlayerActions::actDrawArrow() +{ + auto *card = player->getGame()->getActiveCard(); + if (card) { + card->drawArrow(Qt::red); + } +} + +void PlayerActions::actIncP(QList selectedCards) { actIncPT(selectedCards, 1, 0); } -void PlayerActions::actDecP(QList selectedCards) +void PlayerActions::actDecP(QList selectedCards) { actIncPT(selectedCards, -1, 0); } -void PlayerActions::actIncT(QList selectedCards) +void PlayerActions::actIncT(QList selectedCards) { actIncPT(selectedCards, 0, 1); } -void PlayerActions::actDecT(QList selectedCards) +void PlayerActions::actDecT(QList selectedCards) { actIncPT(selectedCards, 0, -1); } -void PlayerActions::actIncPT(QList selectedCards) +void PlayerActions::actIncPT(QList selectedCards) { actIncPT(selectedCards, 1, 1); } -void PlayerActions::actDecPT(QList selectedCards) +void PlayerActions::actDecPT(QList selectedCards) { actIncPT(selectedCards, -1, -1); } -void PlayerActions::actFlowP(QList selectedCards) +void PlayerActions::actFlowP(QList selectedCards) { actIncPT(selectedCards, 1, -1); } -void PlayerActions::actFlowT(QList selectedCards) +void PlayerActions::actFlowT(QList selectedCards) { actIncPT(selectedCards, -1, 1); } -void PlayerActions::actIncrementCounter(int counterId, int delta) -{ - Command_IncCounter cmd; - cmd.set_counter_id(counterId); - cmd.set_delta(delta); - - sendGameCommand(cmd); -} - -void PlayerActions::actSetCounter(int counterId, int value) -{ - Command_SetCounter cmd; - cmd.set_counter_id(counterId); - cmd.set_value(value); - - sendGameCommand(cmd); -} - -void PlayerActions::actReduceLifeByPower(QList selectedCards) +void PlayerActions::actReduceLifeByPower(QList selectedCards) { // find life counter auto lifeCounter = player->getLifeCounter(); @@ -1506,7 +1481,7 @@ void PlayerActions::actReduceLifeByPower(QList selectedCards) // calculate total power; int total = 0; for (auto card : selectedCards) { - QVariantList parsed = CardState::parsePT(card->getPT()); + QVariantList parsed = CardItem::parsePT(card->getPT()); if (!parsed.isEmpty()) { int power = parsed.first().toInt(); // toInt will default to 0 if it's not an int total += qMax(power, 0); @@ -1520,7 +1495,17 @@ void PlayerActions::actReduceLifeByPower(QList selectedCards) sendGameCommand(prepareGameCommand(cmd)); } -void PlayerActions::actRequestSetAnnotationDialog(QList selectedCards) +void AnnotationDialog::keyPressEvent(QKeyEvent *event) +{ + if (event->key() == Qt::Key_Return && event->modifiers() & Qt::ControlModifier) { + event->accept(); + accept(); + return; + } + QInputDialog::keyPressEvent(event); +} + +void PlayerActions::actRequestSetAnnotationDialog(QList selectedCards) { QString oldAnnotation; for (auto card : selectedCards) { @@ -1532,7 +1517,7 @@ void PlayerActions::actRequestSetAnnotationDialog(QList selectedCar emit requestSetAnnotationDialog(oldAnnotation); } -void PlayerActions::actSetAnnotation(QList selectedCards, const QString &annotation) +void PlayerActions::actSetAnnotation(QList selectedCards, const QString &annotation) { QList commandList; for (auto card : selectedCards) { @@ -1546,7 +1531,17 @@ void PlayerActions::actSetAnnotation(QList selectedCards, const QSt sendGameCommand(prepareGameCommand(commandList)); } -void PlayerActions::actUnattach(QList selectedCards) +void PlayerActions::actAttach() +{ + auto *card = player->getGame()->getActiveCard(); + if (!card) { + return; + } + + card->drawAttachArrow(); +} + +void PlayerActions::actUnattach(QList selectedCards) { QList commandList; for (auto card : selectedCards) { @@ -1562,17 +1557,17 @@ void PlayerActions::actUnattach(QList selectedCards) sendGameCommand(prepareGameCommand(commandList)); } -void PlayerActions::actAddCardCounter(QList selectedCards, int counterId) +void PlayerActions::actAddCardCounter(QList selectedCards, int counterId) { offsetCardCounter(selectedCards, counterId, 1); } -void PlayerActions::actRemoveCardCounter(QList selectedCards, int counterId) +void PlayerActions::actRemoveCardCounter(QList selectedCards, int counterId) { offsetCardCounter(selectedCards, counterId, -1); } -void PlayerActions::offsetCardCounter(QList selectedCards, int counterId, int offset) +void PlayerActions::offsetCardCounter(QList selectedCards, int counterId, int offset) { QList commandList; for (auto card : selectedCards) { @@ -1598,7 +1593,7 @@ void PlayerActions::offsetCardCounter(QList selectedCards, int coun sendGameCommand(prepareGameCommand(commandList)); } -void PlayerActions::actRequestSetCardCounterDialog(QList selectedCards, int counterId) +void PlayerActions::actRequestSetCardCounterDialog(QList selectedCards, int counterId) { // If a single card is selected, we show the old value in the dialog. Otherwise, we show "x" QString oldValueForDlg = "x"; @@ -1610,7 +1605,7 @@ void PlayerActions::actRequestSetCardCounterDialog(QList selectedCa emit requestSetCardCounterDialog(counterId, oldValueForDlg); } -void PlayerActions::actSetCardCounter(QList selectedCards, int counterId, const QString &counterValue) +void PlayerActions::actSetCardCounter(QList selectedCards, int counterId, const QString &counterValue) { QList 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_drag_item.cpp b/cockatrice/src/game_graphics/board/abstract_card_drag_item.cpp index c707b4022..2e18fca2d 100644 --- a/cockatrice/src/game_graphics/board/abstract_card_drag_item.cpp +++ b/cockatrice/src/game_graphics/board/abstract_card_drag_item.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include #include #include @@ -52,20 +51,7 @@ AbstractCardDragItem::AbstractCardDragItem(AbstractCardItem *_item, update(); }); - // The source card can be destroyed mid-drag (e.g. moved by a server event). deleteLater() alone is not enough: - // a scene repaint can run before the deferred delete and paint the dead card. Detach from the scene immediately. - connect(_item, &QObject::destroyed, this, [this]() { - hide(); - if (QGraphicsScene *sc = scene()) { - sc->removeItem(this); - } - deleteLater(); - }); -} - -AbstractCardItem *AbstractCardDragItem::getItem() const -{ - return item.data(); + connect(item, &QObject::destroyed, this, &AbstractCardDragItem::deleteLater); } QPainterPath AbstractCardDragItem::shape() const @@ -79,10 +65,6 @@ QPainterPath AbstractCardDragItem::shape() const void AbstractCardDragItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { - if (item.isNull()) { - return; - } - item->paint(painter, option, widget); // adds a mask to the card so it looks like the card hasnt been placed yet diff --git a/cockatrice/src/game_graphics/board/abstract_card_drag_item.h b/cockatrice/src/game_graphics/board/abstract_card_drag_item.h index 80e40ff1d..18afdcbb8 100644 --- a/cockatrice/src/game_graphics/board/abstract_card_drag_item.h +++ b/cockatrice/src/game_graphics/board/abstract_card_drag_item.h @@ -14,7 +14,6 @@ #include #include #include -#include #include #include @@ -25,8 +24,7 @@ class AbstractCardDragItem : public QObject, public QGraphicsItem Q_OBJECT Q_INTERFACES(QGraphicsItem) protected: - /// Guarded pointer: the source card may be destroyed while this drag item still exists. - QPointer item; + AbstractCardItem *item; QPointF hotSpot; QList childDrags; @@ -46,8 +44,10 @@ public: } [[nodiscard]] QPainterPath shape() const override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; - /// @return the source card, or nullptr if it has already been destroyed. - [[nodiscard]] AbstractCardItem *getItem() const; + [[nodiscard]] AbstractCardItem *getItem() const + { + return item; + } [[nodiscard]] QPointF getHotSpot() const { return hotSpot; 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_drag_item.cpp b/cockatrice/src/game_graphics/board/card_drag_item.cpp index b263e8c92..265ea759c 100644 --- a/cockatrice/src/game_graphics/board/card_drag_item.cpp +++ b/cockatrice/src/game_graphics/board/card_drag_item.cpp @@ -130,9 +130,8 @@ void CardDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) sc->removeItem(this); QList dragItemList; - CardZoneLogic *startZone = static_cast(item.data())->getZone(); - if (currentZone && - !(static_cast(item.data())->getAttachedTo() && (startZone == currentZone->getLogic()))) { + CardZoneLogic *startZone = static_cast(item)->getZone(); + if (currentZone && !(static_cast(item)->getAttachedTo() && (startZone == currentZone->getLogic()))) { if (!occupied) { dragItemList.append(this); } @@ -140,7 +139,7 @@ void CardDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) for (int i = 0; i < childDrags.size(); i++) { CardDragItem *c = static_cast(childDrags[i]); if (!occupied && - !(static_cast(c->item.data())->getAttachedTo() && (startZone == currentZone->getLogic())) && + !(static_cast(c->item)->getAttachedTo() && (startZone == currentZone->getLogic())) && !c->occupied) { dragItemList.append(c); } 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.cpp b/cockatrice/src/game_graphics/deckview/deck_view.cpp index 6e0b0eaaa..630530792 100644 --- a/cockatrice/src/game_graphics/deckview/deck_view.cpp +++ b/cockatrice/src/game_graphics/deckview/deck_view.cpp @@ -77,7 +77,7 @@ void DeckViewCardDragItem::updatePosition(const QPointF &cursorScenePos) void DeckViewCardDragItem::handleDrop(DeckViewCardContainer *target) { - auto *card = static_cast(item.data()); + auto *card = static_cast(item); auto *start = static_cast(item->parentItem()); start->removeCard(card); target->addCard(card); diff --git a/cockatrice/src/game_graphics/deckview/deck_view_container.cpp b/cockatrice/src/game_graphics/deckview/deck_view_container.cpp index 3bef9e59e..a6e541fca 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,17 +33,16 @@ #include #include #include -#include -#include #include -#include -#include -#include +#include +#include +#include +#include +#include #include #include #include #include -#include #include #include #include @@ -107,9 +111,10 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent) connect(deckView, &DeckView::newCardAdded, this, &DeckViewContainer::newCardAdded); connect(deckView, &DeckView::sideboardPlanChanged, this, &DeckViewContainer::sideboardPlanChanged); - deckViewLayout = QtUtils::makeLayout(); + deckViewLayout = new QVBoxLayout; deckViewLayout->addLayout(buttonHBox); deckViewLayout->addWidget(deckView); + deckViewLayout->setContentsMargins(0, 0, 0, 0); setLayout(deckViewLayout); retranslateUi(); @@ -309,7 +314,9 @@ void DeckViewContainer::loadDeckFromDeckList(const DeckList &deck) return; } - PendingCommand *pend = parentGame->getGame()->getGameEventHandler()->prepareDeckSelect(deckString); + Command_DeckSelect cmd; + cmd.set_deck(deckString.toStdString()); + PendingCommand *pend = parentGame->getGame()->getGameEventHandler()->prepareGameCommand(cmd); connect(pend, &PendingCommand::finished, this, &DeckViewContainer::deckSelectFinished); parentGame->getGame()->getGameEventHandler()->sendGameCommand(pend, playerId); @@ -338,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); } @@ -425,21 +442,34 @@ void DeckViewContainer::readyStart() void DeckViewContainer::forceStart() { const auto msg = tr("Are you sure you want to force start?\nThis will kick all non-ready players from the game."); - if (!QtUtils::confirm(this, tr("Cockatrice"), msg, QMessageBox::Yes)) { + const auto res = + QMessageBox::question(this, tr("Cockatrice"), msg, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); + if (res == QMessageBox::No) { return; } - parentGame->getGame()->getGameEventHandler()->sendForceStart(playerId); + Command_ReadyStart cmd; + cmd.set_force_start(true); + cmd.set_ready(true); + parentGame->getGame()->getGameEventHandler()->sendGameCommand(cmd, playerId); } void DeckViewContainer::sideboardLockButtonClicked() { - parentGame->getGame()->getGameEventHandler()->sendSetSideboardLock(sideboardLockButton->getState(), playerId); + Command_SetSideboardLock cmd; + cmd.set_locked(sideboardLockButton->getState()); + + parentGame->getGame()->getGameEventHandler()->sendGameCommand(cmd, playerId); } void DeckViewContainer::sideboardPlanChanged() { - parentGame->getGame()->getGameEventHandler()->sendSetSideboardPlan(deckView->getSideboardPlan(), playerId); + Command_SetSideboardPlan cmd; + const QList &newPlan = deckView->getSideboardPlan(); + for (const auto &i : newPlan) { + cmd.add_move_list()->CopyFrom(i); + } + parentGame->getGame()->getGameEventHandler()->sendGameCommand(cmd, playerId); } /** @@ -451,7 +481,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 1a5e58682..b1b25b49e 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/theme_icon.h" #include "../../interface/theme_manager.h" @@ -20,16 +30,6 @@ #include #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include #include #include @@ -290,7 +290,7 @@ void MessageLogWidget::logDestroyCard(PlayerLogic *player, QString cardName) } void MessageLogWidget::logMoveCard(PlayerLogic *player, - CardState *card, + CardItem *card, CardZoneLogic *startZone, int oldX, CardZoneLogic *targetZone, @@ -669,7 +669,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.")) @@ -713,7 +713,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) { @@ -724,7 +724,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; @@ -762,7 +762,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 da4fbd4e1..9dd7e7b40 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; @@ -79,7 +78,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, @@ -100,13 +99,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 3c6cf9d02..086307bc8 100644 --- a/cockatrice/src/game_graphics/zones/view_zone_widget.cpp +++ b/cockatrice/src/game_graphics/zones/view_zone_widget.cpp @@ -1,13 +1,18 @@ #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 "../../interface/theme_icon.h" #include "../card_dimensions.h" #include "../z_values.h" #include "libcockatrice/utility/macros.h" -#include "libcockatrice/utility/qt_utils.h" #include "view_zone.h" #include @@ -40,12 +45,7 @@ #include #include #include -#include -#include -#include -#include -#include -#include +#include #include #include #include @@ -178,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(); @@ -358,8 +357,12 @@ QRectF ZoneViewWidget::closeButtonRect(QWidget *styleWidget) const QGraphicsView *ZoneViewWidget::findDragView(QWidget *eventWidget) const { - if (auto *view = QtUtils::findParentOfType(eventWidget, true)) { - return view; + QWidget *current = eventWidget; + while (current) { + if (auto *view = qobject_cast(current)) { + return view; + } + current = current->parentWidget(); } if (scene() && !scene()->views().isEmpty()) { @@ -571,7 +574,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/cockatrice/src/interface/card_database_update/card_database_update_status_bar.cpp b/cockatrice/src/interface/card_database_update/card_database_update_status_bar.cpp index 21e26d073..721fdd6c8 100644 --- a/cockatrice/src/interface/card_database_update/card_database_update_status_bar.cpp +++ b/cockatrice/src/interface/card_database_update/card_database_update_status_bar.cpp @@ -7,7 +7,6 @@ #include #include #include -#include namespace { @@ -33,7 +32,8 @@ QString stageName(CardUpdateStage stage, const QString &fallback) CardDatabaseUpdateStatusBar::CardDatabaseUpdateStatusBar(QWidget *parent) : QWidget(parent) { - layout = QtUtils::makeLayout(this); + layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); stageLabel = new QLabel(this); progressBar = new QProgressBar(this); diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp index 2d35538d2..4f77c7096 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp @@ -238,7 +238,6 @@ void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image) }); card.emitPixmapUpdated(); - emit getInstance().pixmapLoaded(card); } void CardPictureLoader::deleteAllLocalOverrides(const ExactCard &card) diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader.h b/cockatrice/src/interface/card_picture_loader/card_picture_loader.h index 0c73e5d59..c6d91570b 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader.h +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader.h @@ -146,14 +146,6 @@ signals: /** @brief Emitted after the worker has finished clearing the network and redirect caches. */ void networkCacheCleared(); - /** - * @brief Emitted once an image has been loaded and put into the pixmap cache. - * - * Views that paint cards from the loader without owning a widget per card can - * listen to this to repaint the rows whose picture just became available. - */ - void pixmapLoaded(const ExactCard &card); - private slots: /** * @brief Triggered when the user changes the picture download settings. diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp index fde6830c2..1437e23f4 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp @@ -11,7 +11,6 @@ #include #include #include -#include #include #include #include @@ -20,62 +19,35 @@ #include #include -static constexpr int REFRESH_DEBOUNCE_MS = 500; -// Watching can fail on network and FUSE mounts; the slow poll still picks those -// changes up without the old ten-second full rescan. -static constexpr int FALLBACK_REFRESH_INTERVAL_MS = 5 * 60 * 1000; +static constexpr int REFRESH_INTERVAL_MS = 10 * 1000; CardPictureLoaderLocal::CardPictureLoaderLocal(QObject *parent) - : CardPictureLoaderLocal(parent, - SettingsCache::instance().paths().getPicsPath(), - SettingsCache::instance().paths().getCustomPicsPath()) + : QObject(parent), picsPath(SettingsCache::instance().paths().getPicsPath()), + customPicsPath(SettingsCache::instance().paths().getCustomPicsPath()) { // Hook up signals to settings connect(&SettingsCache::instance().paths(), &PathsSettings::picsPathChanged, this, &CardPictureLoaderLocal::picsPathChanged); -} - -CardPictureLoaderLocal::CardPictureLoaderLocal(QObject *parent, - const QString &_picsPath, - const QString &_customPicsPath) - : QObject(parent), picsPath(_picsPath), customPicsPath(_customPicsPath), - folderWatcher(new QFileSystemWatcher(this)), indexRefreshTimer(new QTimer(this)), - fallbackRefreshTimer(new QTimer(this)) -{ - indexRefreshTimer->setSingleShot(true); - indexRefreshTimer->setInterval(REFRESH_DEBOUNCE_MS); - connect(indexRefreshTimer, &QTimer::timeout, this, &CardPictureLoaderLocal::refreshIndex); - - connect(folderWatcher, &QFileSystemWatcher::directoryChanged, indexRefreshTimer, qOverload<>(&QTimer::start)); - connect(folderWatcher, &QFileSystemWatcher::fileChanged, indexRefreshTimer, qOverload<>(&QTimer::start)); - - fallbackRefreshTimer->setInterval(FALLBACK_REFRESH_INTERVAL_MS); - connect(fallbackRefreshTimer, &QTimer::timeout, this, &CardPictureLoaderLocal::refreshIndex); - fallbackRefreshTimer->start(); refreshIndex(); + + refreshTimer = new QTimer(this); + connect(refreshTimer, &QTimer::timeout, this, &CardPictureLoaderLocal::refreshIndex); + refreshTimer->start(REFRESH_INTERVAL_MS); } void CardPictureLoaderLocal::refreshIndex() { customFolderIndex.clear(); - QSet directories; - - QDir customDir(customPicsPath); - if (customDir.exists()) { - directories.insert(customDir.absolutePath()); - } QDirIterator it(customPicsPath, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks); // Recursively check all subdirectories of the CUSTOM folder while (it.hasNext()) { - const QString thisPath = it.next(); - const QFileInfo thisFileInfo = it.fileInfo(); + QString thisPath(it.next()); + QFileInfo thisFileInfo(thisPath); - if (thisFileInfo.isDir()) { - directories.insert(thisFileInfo.absoluteFilePath()); - } else if (thisFileInfo.isFile()) { + if (thisFileInfo.isFile()) { // We don't know which name is the correctedName because there might be '.'s in the cardName. // Just add all possibilities to be sure. customFolderIndex.insert(thisFileInfo.baseName(), thisFileInfo.absoluteFilePath()); @@ -83,34 +55,8 @@ void CardPictureLoaderLocal::refreshIndex() } } - updateWatchedDirectories(directories); - qCDebug(CardPictureLoaderLocalLog) << "Finished indexing local image folder CUSTOM; map now has" << customFolderIndex.size() << "entries."; - emit indexChanged(); -} - -void CardPictureLoaderLocal::updateWatchedDirectories(const QSet &directories) -{ - const QSet stale = watchedDirectories - directories; - if (!stale.isEmpty()) { - folderWatcher->removePaths(QStringList(stale.begin(), stale.end())); - } - - const QSet added = directories - watchedDirectories; - if (!added.isEmpty()) { - const QStringList failed = folderWatcher->addPaths(QStringList(added.begin(), added.end())); - if (!failed.isEmpty()) { - qCDebug(CardPictureLoaderLocalLog) << "Could not watch custom picture directories:" << failed; - } - } - - watchedDirectories = directories; -} - -QStringList CardPictureLoaderLocal::indexedPaths(const QString &baseName) const -{ - return customFolderIndex.values(baseName); } QImage CardPictureLoaderLocal::tryLoad(const ExactCard &toLoad) const @@ -199,5 +145,4 @@ void CardPictureLoaderLocal::picsPathChanged() { picsPath = SettingsCache::instance().paths().getPicsPath(); customPicsPath = SettingsCache::instance().paths().getCustomPicsPath(); - refreshIndex(); } diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.h b/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.h index 35190366d..009897acc 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.h +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.h @@ -5,12 +5,10 @@ #include #include #include -#include #include #include class ExactCard; -class QFileSystemWatcher; class QTimer; inline Q_LOGGING_CATEGORY(CardPictureLoaderLocalLog, "card_picture_loader.local"); @@ -42,18 +40,6 @@ public: */ explicit CardPictureLoaderLocal(QObject *parent); - /** - * @brief Constructs the loader against explicit folders without SettingsCache. - * - * Used by tests and by callers that already resolved the configured paths. - */ - CardPictureLoaderLocal(QObject *parent, const QString &picsPath, const QString &customPicsPath); - - /** - * @brief Returns every indexed file whose base name matches @p baseName. - */ - [[nodiscard]] QStringList indexedPaths(const QString &baseName) const; - /** * @brief Attempts to load a card image from local disk or custom folders. * @param toLoad ExactCard object representing the card to load. @@ -68,10 +54,7 @@ private: QString customPicsPath; ///< Path to custom card image folder QMultiHash customFolderIndex; ///< Multimap from cardName to file paths in CUSTOM folder - QFileSystemWatcher *folderWatcher; ///< Watches the custom folder tree for changes - QTimer *indexRefreshTimer; ///< Debounces bursts of filesystem events - QTimer *fallbackRefreshTimer; ///< Slow poll for filesystems without change notification - QSet watchedDirectories; ///< Directories currently registered with folderWatcher + QTimer *refreshTimer; ///< Timer for periodic folder index refresh /** * @brief Rebuilds the index of the custom image folder. @@ -82,11 +65,6 @@ private: */ void refreshIndex(); - /** - * @brief Keeps the watcher registered on exactly @p directories. - */ - void updateWatchedDirectories(const QSet &directories); - /** * @brief Attempts to load a card image from disk given its set and name info. * @param setName Corrected short name of the card's set. @@ -104,12 +82,6 @@ private: const QString &collectorNumber, const QString &providerId) const; -signals: - /** - * @brief Emitted after the custom folder index has been rebuilt. - */ - void indexChanged(); - private slots: /** * @brief Updates internal paths when the user changes picture settings. diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp index 3ab5eb684..150b4c544 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp @@ -28,7 +28,6 @@ #include #include #include -#include #include #include #include @@ -47,10 +46,11 @@ CardPictureLoaderWorker::CardPictureLoaderWorker() : QObject(nullptr), picDownload(SettingsCache::instance().downloads().getPicDownload()), hostRequestLimits(SettingsCache::instance().downloads().getHostRequestLimits()) { - networkManager = NetworkManagerFactory::create(this); + networkManager = new QNetworkAccessManager(this); // We need a timeout to ensure requests don't hang indefinitely in case of // cache corruption, see related Qt bug: https://bugreports.qt.io/browse/QTBUG-111397 // Use Qt's default timeout (30s, as of 2023-02-22) + networkManager->setTransferTimeout(); cache = new QNetworkDiskCache(this); cache->setCacheDirectory(SettingsCache::instance().getNetworkCachePath()); cache->setMaximumCacheSize(1024L * 1024L * diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_to_load.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_to_load.cpp index 8e8feb5b5..e35c417c8 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_to_load.cpp +++ b/cockatrice/src/interface/card_picture_loader/card_picture_to_load.cpp @@ -21,7 +21,6 @@ #include #include #include -#include CardPictureToLoad::CardPictureToLoad(const ExactCard &_card) : card(_card), urlTemplates(SettingsCache::instance().downloads().getAllURLs()) @@ -75,20 +74,19 @@ QList CardPictureToLoad::extractSetsSorted(const ExactCard &card) //! \todo Check if going through all sets is still necessary after the ExactCard refactor. static PrintingInfo findPrintingForSet(const ExactCard &card, const QString &setName) { - const auto &setsToPrintings = card.getInfo().getSets(); + SetToPrintingsMap setsToPrintings = card.getInfo().getSets(); - const auto setPrintings = setsToPrintings.constFind(setName); - if (setPrintings == setsToPrintings.constEnd() || setPrintings.value().isEmpty()) { + if (!setsToPrintings.contains(setName)) { return PrintingInfo(); } - for (const auto &printing : setPrintings.value()) { + for (const auto &printing : setsToPrintings[setName]) { if (printing.getUuid() == card.getPrinting().getUuid()) { return printing; } } - return setPrintings.value().first(); + return setsToPrintings[setName][0]; } void CardPictureToLoad::populateSetUrls() @@ -185,14 +183,10 @@ static int parse(const QString &urlTemplate, { static const QRegularExpression rxFillWith("^(.+)_fill_with_(.+)$"); static const QRegularExpression rxSubStr("^(.+)_substr_(\\d+)_(\\d+)$"); - static const QRegularExpression rxCardProp("!prop:([^!]+)!"); - static const QRegularExpression rxCardSet("!set:([^!]+)!"); - // The property type only takes two values, so keep the expression that was - // compiled for it instead of rebuilding one per URL template. - const QRegularExpression &rxCardPropSelector = propType == "prop" ? rxCardProp : rxCardSet; + const QRegularExpression rxCardProp("!" + propType + ":([^!]+)!"); - auto matches = rxCardPropSelector.globalMatch(urlTemplate); + auto matches = rxCardProp.globalMatch(urlTemplate); while (matches.hasNext()) { auto match = matches.next(); QString templatePropertyName = match.captured(1); @@ -309,10 +303,10 @@ QString CardPictureToLoad::transformUrl(const QString &urlTemplate) const } QString transformedUrl = urlTemplate; - for (const auto &[prop, value] : std::as_const(transformMap).asKeyValueRange()) { + for (const QString &prop : transformMap.keys()) { if (transformedUrl.contains(prop)) { - if (!value.isEmpty()) { - transformedUrl.replace(prop, QUrl::toPercentEncoding(value)); + if (!transformMap[prop].isEmpty()) { + transformedUrl.replace(prop, QUrl::toPercentEncoding(transformMap[prop])); } else { /* This means the template is requesting information that is not * populated in this card, so it should return an empty string, 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_login.cpp b/cockatrice/src/interface/intents/intent_login.cpp index 8cc574cb0..0d8390015 100644 --- a/cockatrice/src/interface/intents/intent_login.cpp +++ b/cockatrice/src/interface/intents/intent_login.cpp @@ -27,9 +27,9 @@ void IntentGetLoginCredentials::onPreconditionSatisfied() ServersSettings &servers = SettingsCache::instance().servers(); const int index = servers.findServerIndex(context->hostname, context->port); - if (const auto server = servers.getServer(index)) { - context->username = server->username; - context->password = server->password; + if (index >= 0) { + context->username = servers.getValue(QString("username%1").arg(index), "server", "server_details").toString(); + context->password = servers.getValue(QString("password%1").arg(index), "server", "server_details").toString(); emitFinished(); } else { emitFailed(tr("No saved credentials for this server")); @@ -69,8 +69,9 @@ void IntentGetLoginCredentials::onPreconditionNotSatisfied() // it with the raw hostname when addNewServer updates the entry in place. QString saveName = context->hostname; const int existingIndex = servers.findServerIndex(context->hostname, context->port); - if (const auto existing = servers.getServer(existingIndex)) { - saveName = existing->saveName; + if (existingIndex >= 0) { + saveName = + servers.getValue(QString("saveName%1").arg(existingIndex), "server", "server_details").toString(); if (saveName.isEmpty()) { saveName = context->hostname; } 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.cpp b/cockatrice/src/interface/intents/intent_open_shared_deck.cpp index 2998ff78e..8abad3d88 100644 --- a/cockatrice/src/interface/intents/intent_open_shared_deck.cpp +++ b/cockatrice/src/interface/intents/intent_open_shared_deck.cpp @@ -21,7 +21,6 @@ #include #include #include -#include #include #include #include @@ -185,11 +184,13 @@ void IntentOpenSharedDeck::onItemFailure(const QString &reason) } const int downloadedCount = loadedDecks.size(); - if (QtUtils::confirm(previewDialog, tr("Open shared decks"), - tr("Could not download the deck \"%1\".\n\n%n deck(s) were already downloaded. Open them?", "", - downloadedCount) - .arg(itemNames.value(currentItemId, tr("Unknown deck"))), - QMessageBox::Yes)) { + const QMessageBox::StandardButton answer = QMessageBox::question( + previewDialog, tr("Open shared decks"), + tr("Could not download the deck \"%1\".\n\n%n deck(s) were already downloaded. Open them?", "", downloadedCount) + .arg(itemNames.value(currentItemId, tr("Unknown deck"))), + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); + + if (answer == QMessageBox::Yes) { finishAll(); } else { previewDialog->deleteLater(); 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/intents/url_parser.cpp b/cockatrice/src/interface/intents/url_parser.cpp index e0b7bb2fb..966fd1213 100644 --- a/cockatrice/src/interface/intents/url_parser.cpp +++ b/cockatrice/src/interface/intents/url_parser.cpp @@ -28,7 +28,6 @@ #include #include #include -#include #include #include #include @@ -116,7 +115,9 @@ Intent *IntentUrlParser::createJoinGameIntent(const QUrlQuery &query, PendingInt const QString gameDescription = query.queryItemValue("game", QUrl::FullyDecoded); const QString message = generateJoinGameMessage(*ctx, gameDescription); - if (!QtUtils::confirm(mainWindow, tr("Join game"), message, QMessageBox::Yes)) { + const QMessageBox::StandardButton answer = QMessageBox::question( + mainWindow, tr("Join game"), message, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); + if (answer != QMessageBox::Yes) { return nullptr; } @@ -230,10 +231,11 @@ Intent *IntentUrlParser::createOpenDeckIntent(const QUrlQuery &query, PendingInt if (client->getStatus() == StatusLoggedIn) { const QString current = QStringLiteral("%1:%2").arg(client->serverName(), QString::number(client->serverPort())); - if (!QtUtils::confirm( - mainWindow, tr("Open shared deck"), - tr("Opening this share link connects you to %1 instead of %2.\n\nContinue?").arg(target, current), - QMessageBox::Yes)) { + const QMessageBox::StandardButton answer = QMessageBox::question( + mainWindow, tr("Open shared deck"), + tr("Opening this share link connects you to %1 instead of %2.\n\nContinue?").arg(target, current), + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); + if (answer != QMessageBox::Yes) { return nullptr; } chain.migrationTargetHost = ctx->serverContext.hostname; @@ -418,10 +420,12 @@ void IntentUrlParser::restoreToPreviousServer(const PendingIntentChain &chain) ServersSettings &servers = SettingsCache::instance().servers(); const int index = servers.findServerIndex(chain.previousServerHost, chain.previousServerPort); - const auto server = servers.getServer(index); - if (server && servers.hasLoginData(chain.previousServerHost, chain.previousServerPort)) { - client->connectToServer(chain.previousServerHost, chain.previousServerPort.toUInt(), server->username, - server->password); + if (index >= 0 && servers.hasLoginData(chain.previousServerHost, chain.previousServerPort)) { + const QString username = + servers.getValue(QString("username%1").arg(index), "server", "server_details").toString(); + const QString password = + servers.getValue(QString("password%1").arg(index), "server", "server_details").toString(); + client->connectToServer(chain.previousServerHost, chain.previousServerPort.toUInt(), username, password); return; } client->disconnectFromServer(); @@ -436,9 +440,11 @@ void IntentUrlParser::restoreToPreviousServer(const PendingIntentChain &chain) // when credentials are saved, otherwise stay offline. ServersSettings &servers = SettingsCache::instance().servers(); const int index = servers.findServerIndex(chain.previousServerHost, chain.previousServerPort); - const auto server = servers.getServer(index); - if (server && servers.hasLoginData(chain.previousServerHost, chain.previousServerPort)) { - client->connectToServer(chain.previousServerHost, chain.previousServerPort.toUInt(), server->username, - server->password); + if (index >= 0 && servers.hasLoginData(chain.previousServerHost, chain.previousServerPort)) { + const QString username = + servers.getValue(QString("username%1").arg(index), "server", "server_details").toString(); + const QString password = + servers.getValue(QString("password%1").arg(index), "server", "server_details").toString(); + client->connectToServer(chain.previousServerHost, chain.previousServerPort.toUInt(), username, password); } } diff --git a/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp b/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp index b6271291d..20e16614a 100644 --- a/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp +++ b/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp @@ -25,7 +25,6 @@ #include #include #include -#include #include PaletteEditorDialog::PaletteEditorDialog(const QString &_themeDirPath, const QString &_themeName, QWidget *parent) @@ -58,7 +57,9 @@ PaletteEditorDialog::PaletteEditorDialog(const QString &_themeDirPath, const QSt void PaletteEditorDialog::setupUi() { - auto *root = QtUtils::makeLayout(this, 0, 0); + auto *root = new QVBoxLayout(this); + root->setSpacing(0); + root->setContentsMargins(0, 0, 0, 0); // Header header = new QWidget; diff --git a/cockatrice/src/interface/palette_editor/palette_grid_widget.cpp b/cockatrice/src/interface/palette_editor/palette_grid_widget.cpp index 463c4038d..de069bc80 100644 --- a/cockatrice/src/interface/palette_editor/palette_grid_widget.cpp +++ b/cockatrice/src/interface/palette_editor/palette_grid_widget.cpp @@ -18,7 +18,6 @@ #include #include #include -#include #include #include @@ -66,7 +65,8 @@ static const QMap APP_ROLE_DESCRIPTIONS = { PaletteGridWidget::PaletteGridWidget(QWidget *parent) : QWidget(parent) { - scroll = QtUtils::makeScrollArea(this, Qt::ScrollBarAsNeeded, Qt::ScrollBarAsNeeded); + scroll = new QScrollArea(this); + scroll->setWidgetResizable(true); scroll->setFrameShape(QFrame::NoFrame); gridHost = new QWidget; @@ -74,7 +74,8 @@ PaletteGridWidget::PaletteGridWidget(QWidget *parent) : QWidget(parent) refreshChromePalettes(); scroll->setWidget(gridHost); - layout = QtUtils::makeLayout(this); + layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); layout->addWidget(scroll); } @@ -101,7 +102,9 @@ void PaletteGridWidget::buildGrid(QWidget *host) auto *label = new QLabel(groupHeaders[col], host); label->setAlignment(Qt::AlignCenter); label->setToolTip(groupTips[col]); - label->setFont(QtUtils::boldFont(label->font())); + QFont f = label->font(); + f.setBold(true); + label->setFont(f); label->setAutoFillBackground(true); label->setContentsMargins(4, 4, 4, 4); grid->addWidget(label, 0, col + 1); @@ -146,7 +149,9 @@ void PaletteGridWidget::buildGrid(QWidget *host) auto *appHeader = new QLabel(tr("App colors"), host); appHeader->setToolTip(tr("Application-specific colors layered on top of the Qt palette")); - appHeader->setFont(QtUtils::boldFont(appHeader->font())); + QFont appHeaderFont = appHeader->font(); + appHeaderFont.setBold(true); + appHeader->setFont(appHeaderFont); appHeader->setAutoFillBackground(true); appHeader->setContentsMargins(4, 4, 4, 4); grid->addWidget(appHeader, appHeaderRow, 0, 1, 4); diff --git a/cockatrice/src/interface/theme_manager.cpp b/cockatrice/src/interface/theme_manager.cpp index 893cdc795..9a96b3e45 100644 --- a/cockatrice/src/interface/theme_manager.cpp +++ b/cockatrice/src/interface/theme_manager.cpp @@ -558,9 +558,6 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName, widget->update(); } - // Palette-only previews do not run themeChangedSlot(), so drop snapshots - // generated under the previous palette here as well. - clearPixmapGeneratorCaches(); emit paletteChanged(); } diff --git a/cockatrice/src/interface/widgets/cards/additional_info/color_identity_widget.cpp b/cockatrice/src/interface/widgets/cards/additional_info/color_identity_widget.cpp index 0f68f3626..ef7fb0e0e 100644 --- a/cockatrice/src/interface/widgets/cards/additional_info/color_identity_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/additional_info/color_identity_widget.cpp @@ -23,7 +23,9 @@ ColorIdentityWidget::ColorIdentityWidget(QWidget *parent, const QString &_colorIdentity) : QWidget(parent), colorIdentity(_colorIdentity) { - layout = QtUtils::makeLayout(this, 5); + layout = new QHBoxLayout(this); + layout->setSpacing(5); // Small spacing between icons + layout->setContentsMargins(0, 0, 0, 0); layout->setAlignment(Qt::AlignCenter); // Ensure icons are centered setLayout(layout); diff --git a/cockatrice/src/interface/widgets/cards/additional_info/mana_cost_widget.cpp b/cockatrice/src/interface/widgets/cards/additional_info/mana_cost_widget.cpp index f6efd267e..7da97f88c 100644 --- a/cockatrice/src/interface/widgets/cards/additional_info/mana_cost_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/additional_info/mana_cost_widget.cpp @@ -11,11 +11,12 @@ #include #include #include -#include ManaCostWidget::ManaCostWidget(QWidget *parent, CardInfoPtr _card) : QWidget(parent), card(_card) { - layout = QtUtils::makeLayout(this, 5); + layout = new QHBoxLayout(this); + layout->setSpacing(5); // Small spacing between icons + layout->setContentsMargins(0, 0, 0, 0); setLayout(layout); setFixedHeight(50); // Fixed height diff --git a/cockatrice/src/interface/widgets/cards/card_info_display_widget.cpp b/cockatrice/src/interface/widgets/cards/card_info_display_widget.cpp index 038f08612..7ec2ee38e 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_display_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_info_display_widget.cpp @@ -17,7 +17,6 @@ #include #include #include -#include class QWidget; @@ -31,8 +30,9 @@ CardInfoDisplayWidget::CardInfoDisplayWidget(const CardRef &cardRef, QWidget *pa text->setObjectName("text"); connect(text, &CardInfoTextWidget::linkActivated, this, [this](const QString &card) { setCard({card}); }); - auto *layout = QtUtils::makeLayout(); + auto *layout = new QVBoxLayout(); layout->setObjectName("layout"); + layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); layout->addWidget(pic, 0, Qt::AlignCenter); layout->addWidget(text, 0, Qt::AlignCenter); diff --git a/cockatrice/src/interface/widgets/cards/card_info_frame_widget.cpp b/cockatrice/src/interface/widgets/cards/card_info_frame_widget.cpp index 80bc4b9d0..0f66eb209 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_frame_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_info_frame_widget.cpp @@ -19,7 +19,6 @@ #include #include #include -#include #include struct CardRef; @@ -50,13 +49,15 @@ CardInfoFrameWidget::CardInfoFrameWidget(QWidget *parent) insertTab(ImageAndTextView, tab3, QString()); connect(this, &CardInfoFrameWidget::currentChanged, this, &CardInfoFrameWidget::setViewMode); - tab1Layout = QtUtils::makeLayout(); + tab1Layout = new QVBoxLayout(); tab1Layout->setObjectName("tab1Layout"); + tab1Layout->setContentsMargins(0, 0, 0, 0); tab1Layout->setSpacing(0); tab1->setLayout(tab1Layout); - tab2Layout = QtUtils::makeLayout(); + tab2Layout = new QVBoxLayout(); tab2Layout->setObjectName("tab2Layout"); + tab2Layout->setContentsMargins(0, 0, 0, 0); tab2Layout->setSpacing(0); tab2->setLayout(tab2Layout); @@ -64,8 +65,9 @@ CardInfoFrameWidget::CardInfoFrameWidget(QWidget *parent) splitter->setObjectName("splitter"); splitter->setOrientation(Qt::Vertical); - tab3Layout = QtUtils::makeLayout(); + tab3Layout = new QVBoxLayout(); tab3Layout->setObjectName("tab3Layout"); + tab3Layout->setContentsMargins(0, 0, 0, 0); tab3Layout->setSpacing(0); tab3Layout->addWidget(splitter); tab3->setLayout(tab3Layout); diff --git a/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp b/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp index 8f02ebcde..3c1e1b77f 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp @@ -43,6 +43,11 @@ #include #include +static constexpr qreal MTG_CARD_ASPECT_RATIO = 1.396; +// static constexpr qreal YUGIOH_CARD_ASPECT_RATIO = 1.457; +static constexpr qreal ASPECT_RATIO = MTG_CARD_ASPECT_RATIO; + +static constexpr int BASE_WIDTH = 200; static constexpr int BASE_HEIGHT = 200; static constexpr int ENLARGED_PIXMAP_OFFSET = 10; @@ -157,11 +162,12 @@ void CardInfoPictureWidget::resizeEvent(QResizeEvent *event) */ void CardInfoPictureWidget::setScaleFactor(const int scale) { - const QSize newSize = sizeForScaleFactor(scale); + const int newWidth = BASE_WIDTH * scale / 100; + const int newHeight = static_cast(newWidth * ASPECT_RATIO); scaleFactor = scale; - setFixedSize(newSize); + setFixedSize(newWidth, newHeight); updatePixmap(); emit cardScaleFactorChanged(scale); @@ -260,13 +266,8 @@ void CardInfoPictureWidget::paintEvent(QPaintEvent *event) */ QSize CardInfoPictureWidget::sizeHint() const { - return sizeForScaleFactor(scaleFactor); -} - -QSize CardInfoPictureWidget::sizeForScaleFactor(int scaleFactor) -{ - const int width = BASE_WIDTH * scaleFactor / 100; - return {width, static_cast(width * ASPECT_RATIO)}; + return {static_cast(BASE_WIDTH * scaleFactor / 100.0), + static_cast(BASE_WIDTH * scaleFactor / 100.0 * ASPECT_RATIO)}; } /** @@ -490,14 +491,7 @@ void CardInfoPictureWidget::showEnlargedPixmap() enlargedPixmapWidget->setCardPixmap(exactCard, enlargedSize); const QPoint cursorPos = QCursor::pos(); - QScreen *screen = QGuiApplication::screenAt(cursorPos); - if (!screen) { - screen = QGuiApplication::primaryScreen(); - } - if (!screen) { - return; - } - const QRect screenGeometry = screen->geometry(); + const QRect screenGeometry = QGuiApplication::screenAt(cursorPos)->geometry(); const QSize widgetSize = enlargedPixmapWidget->size(); int newX = cursorPos.x() + ENLARGED_PIXMAP_OFFSET; diff --git a/cockatrice/src/interface/widgets/cards/card_info_picture_widget.h b/cockatrice/src/interface/widgets/cards/card_info_picture_widget.h index 0eb731d92..8d2d63736 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_picture_widget.h +++ b/cockatrice/src/interface/widgets/cards/card_info_picture_widget.h @@ -28,11 +28,6 @@ class CardInfoPictureWidget : public QWidget Q_OBJECT public: - /// Width of a card picture at 100 % scale, in pixels. - static constexpr int BASE_WIDTH = 200; - /// Height / width of a card picture. - static constexpr qreal ASPECT_RATIO = 1.396; - explicit CardInfoPictureWidget(QWidget *parent = nullptr, bool hoverToZoomEnabled = false, bool raiseOnEnter = false); @@ -42,12 +37,6 @@ public: } [[nodiscard]] QSize sizeHint() const override; - /** - * @brief The size a card picture has at @p scaleFactor percent. - * @param scaleFactor Percentage of BASE_WIDTH, as set by the card size slider. - */ - [[nodiscard]] static QSize sizeForScaleFactor(int scaleFactor); - public slots: void setCard(const ExactCard &card); void setScaleFactor(int scale); // New slot for scaling diff --git a/cockatrice/src/interface/widgets/cards/card_info_text_widget.cpp b/cockatrice/src/interface/widgets/cards/card_info_text_widget.cpp index 218d0e964..fa6cde311 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_text_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_info_text_widget.cpp @@ -19,7 +19,6 @@ #include #include #include -#include #include #include @@ -34,7 +33,9 @@ CardInfoTextWidget::CardInfoTextWidget(QWidget *parent) : QFrame(parent) textLabel->setReadOnly(true); textLabel->setMinimumSize(35, 35); - propsScroll = QtUtils::makeScrollArea(this); + propsScroll = new QScrollArea(this); + propsScroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + propsScroll->setWidgetResizable(true); propsScroll->setAlignment(Qt::AlignLeft | Qt::AlignTop); propsScroll->setContentsMargins(0, 0, 0, 0); propsScroll->setFrameStyle(QFrame::NoFrame); diff --git a/cockatrice/src/interface/widgets/cards/card_picture_delegate.cpp b/cockatrice/src/interface/widgets/cards/card_picture_delegate.cpp deleted file mode 100644 index 781c767ef..000000000 --- a/cockatrice/src/interface/widgets/cards/card_picture_delegate.cpp +++ /dev/null @@ -1,83 +0,0 @@ -#include "card_picture_delegate.h" - -#include "../../../client/settings/cache_settings.h" -#include "../../card_picture_loader/card_picture_loader.h" -#include "card_art_utils.h" -#include "card_database_visual_display_model.h" -#include "libcockatrice/card/printing/exact_card.h" -#include "libcockatrice/settings/cards_display_settings.h" - -#include -#include -#include -#include -#include -#include - -CardPictureDelegate::CardPictureDelegate(CardDatabaseVisualDisplayModel *_model, QObject *parent) - : QStyledItemDelegate(parent), model(_model) -{ -} - -void CardPictureDelegate::setCardSize(const QSize &size) -{ - if (cardSize == size) { - return; - } - - cardSize = size; -} - -void CardPictureDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const -{ - if (model == nullptr) { - return; - } - - const ExactCard card = model->cardAt(index.row()); - if (!card) { - return; - } - - // The grid cell is the card plus SPACING; give the picture the rest. - const int inset = SPACING / 2; - const QRect cardArea = option.rect.adjusted(inset, inset, -inset, -inset); - - QPixmap pixmap; - CardPictureLoader::getCardBackLoadingInProgressPixmap(pixmap, cardArea.size()); - CardPictureLoader::getPixmap(pixmap, card, cardArea.size()); - - if (pixmap.isNull()) { - return; - } - - if (SettingsCache::instance().cardsDisplay().getAutoRotateSidewaysLayoutCards()) { - pixmap = CardArtUtils::rotateSidewaysLayoutArt(pixmap, card); - } - - // Fit the picture into the cell and center it, like CardInfoPictureWidget does. - const QSize pictureSize = pixmap.deviceIndependentSize().toSize().scaled(cardArea.size(), Qt::KeepAspectRatio); - const QRect pictureRect(cardArea.center().x() - pictureSize.width() / 2, - cardArea.center().y() - pictureSize.height() / 2, pictureSize.width(), - pictureSize.height()); - - const qreal radius = SettingsCache::instance().cardsDisplay().getRoundCardCorners() - ? 0.05 * static_cast(pictureRect.width()) - : 0.0; - - painter->save(); - painter->setRenderHint(QPainter::Antialiasing); - painter->setRenderHint(QPainter::SmoothPixmapTransform); - QPainterPath shape; - shape.addRoundedRect(pictureRect, radius, radius); - painter->setClipPath(shape); - // Source rect is in device pixels, target in logical ones: drawPixmap scales - // between them without the device pixel ratio being applied twice. - painter->drawPixmap(pictureRect, pixmap, QRectF(pixmap.rect())); - painter->restore(); -} - -QSize CardPictureDelegate::sizeHint(const QStyleOptionViewItem & /*option*/, const QModelIndex & /*index*/) const -{ - return cardSize + QSize(SPACING, SPACING); -} diff --git a/cockatrice/src/interface/widgets/cards/card_picture_delegate.h b/cockatrice/src/interface/widgets/cards/card_picture_delegate.h deleted file mode 100644 index 31e673624..000000000 --- a/cockatrice/src/interface/widgets/cards/card_picture_delegate.h +++ /dev/null @@ -1,45 +0,0 @@ -/** - * @file card_picture_delegate.h - * @ingroup CardWidgets - * @brief Paints a card picture for a visual card grid item. - */ - -#ifndef CARD_PICTURE_DELEGATE_H -#define CARD_PICTURE_DELEGATE_H - -#include -#include -#include - -class CardDatabaseVisualDisplayModel; - -/** - * @brief Draws the card of a CardDatabaseVisualDisplayModel row as a picture. - * - * The delegate only paints rows the view asks for, so a grid of the whole card - * database stays cheap: no widget per card and no pixmap loaded for items that - * are off screen. Card images come from CardPictureLoader, which caches them and - * loads missing ones asynchronously. - */ -class CardPictureDelegate : public QStyledItemDelegate -{ - Q_OBJECT - -public: - /// Gap left around a card picture inside its grid cell, in pixels. - static constexpr int SPACING = 4; - - explicit CardPictureDelegate(CardDatabaseVisualDisplayModel *_model, QObject *parent = nullptr); - - /// The picture size inside a grid cell. The cell is this size plus SPACING. - void setCardSize(const QSize &size); - - void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; - [[nodiscard]] QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; - -private: - CardDatabaseVisualDisplayModel *model; - QSize cardSize; -}; - -#endif // CARD_PICTURE_DELEGATE_H diff --git a/cockatrice/src/interface/widgets/cards/card_size_widget.cpp b/cockatrice/src/interface/widgets/cards/card_size_widget.cpp index ac1ab9cb1..ab1840591 100644 --- a/cockatrice/src/interface/widgets/cards/card_size_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_size_widget.cpp @@ -2,7 +2,6 @@ #include "../general/layout_containers/flow_widget.h" -#include #include #include #include @@ -34,8 +33,8 @@ CardSizeWidget::CardSizeWidget(QWidget *parent, FlowWidget *_flowWidget, int def cardSizeLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); cardSizeSlider = new QSlider(Qt::Horizontal, this); - cardSizeSlider->setRange(MIN_CARD_SIZE, MAX_CARD_SIZE); ///< Slider range for card size adjustment. - cardSizeSlider->setValue(defaultValue); ///< Initial slider value. + cardSizeSlider->setRange(50, 250); ///< Slider range for card size adjustment. + cardSizeSlider->setValue(defaultValue); ///< Initial slider value. cardSizeLayout->addWidget(cardSizeLabel); cardSizeLayout->addWidget(cardSizeSlider); @@ -74,26 +73,13 @@ QSlider *CardSizeWidget::getSlider() const void CardSizeWidget::enableCtrlScrollResize(QWidget *host) { - const auto watchScrollArea = [this](QAbstractScrollArea *scrollArea) { - if (scrollArea == nullptr) { - return; - } - if (QWidget *viewport = scrollArea->viewport()) { - viewport->installEventFilter(this); - } - if (auto *contentScrollArea = qobject_cast(scrollArea)) { - if (QWidget *content = contentScrollArea->widget()) { - content->installEventFilter(this); - } - } - }; - host->installEventFilter(this); - watchScrollArea(qobject_cast(host)); - const auto scrollAreas = host->findChildren(); - for (QAbstractScrollArea *scrollArea : scrollAreas) { - watchScrollArea(scrollArea); + const auto scrollAreas = host->findChildren(); + for (QScrollArea *scrollArea : scrollAreas) { + if (QWidget *content = scrollArea->widget()) { + content->installEventFilter(this); + } } } diff --git a/cockatrice/src/interface/widgets/cards/card_size_widget.h b/cockatrice/src/interface/widgets/cards/card_size_widget.h index fced90d33..7de97a5e9 100644 --- a/cockatrice/src/interface/widgets/cards/card_size_widget.h +++ b/cockatrice/src/interface/widgets/cards/card_size_widget.h @@ -24,10 +24,6 @@ class CardSizeWidget : public QWidget Q_OBJECT public: - /// Range of the card size slider, as a percentage of the base card width. - static constexpr int MIN_CARD_SIZE = 50; - static constexpr int MAX_CARD_SIZE = 250; - explicit CardSizeWidget(QWidget *parent, FlowWidget *flowWidget = nullptr, int defaultValue = 100); [[nodiscard]] QSlider *getSlider() const; @@ -35,9 +31,8 @@ public: * @brief Resizes the cards when the user Ctrl + scrolls over @p host or any of its descendants. * * Installs this widget as an event filter on the given host widget. If the host contains a - * scroll area the filter is also installed on the scroll area's content widget, and on the - * viewport of every scroll area base (QListView, QTableView, ...), so the resize intercepts - * the wheel event before the scroll area would scroll the view. + * scroll area the filter is also installed on the scroll area's content widget, so the resize + * intercepts the wheel event before the scroll area would scroll the view. */ void enableCtrlScrollResize(QWidget *host); diff --git a/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.cpp b/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.cpp index d7eef6c18..911e35285 100644 --- a/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.cpp @@ -29,7 +29,6 @@ #include #include #include -#include #include class CardSizeWidget; @@ -180,7 +179,15 @@ void DeckCardZoneDisplayWidget::displayCards() void DeckCardZoneDisplayWidget::refreshDisplayType(const DisplayType &_displayType) { displayType = _displayType; - QtUtils::clearLayoutRec(cardGroupLayout); + QLayoutItem *item; + while ((item = cardGroupLayout->takeAt(0)) != nullptr) { + if (item->widget()) { + item->widget()->deleteLater(); + } else if (item->layout()) { + item->layout()->deleteLater(); + } + delete item; + } indexToWidgetMap.clear(); diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.cpp index 8215cbdad..fc7914b94 100644 --- a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.cpp @@ -22,7 +22,6 @@ #include #include #include -#include #include #include @@ -48,7 +47,12 @@ ManaBaseWidget::ManaBaseWidget(QWidget *parent, DeckListStatisticsAnalyzer *anal void ManaBaseWidget::updateDisplay() { // Clear previous widgets - QtUtils::clearLayoutRec(barLayout); + while (QLayoutItem *item = barLayout->takeAt(0)) { + if (item->widget()) { + item->widget()->deleteLater(); + } + delete item; + } auto &pipCount = analyzer->getProductionPipCount(); auto &cardCount = analyzer->getProductionCardCount(); diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.cpp index be81ab97a..7e15058a4 100644 --- a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.cpp @@ -16,13 +16,14 @@ #include #include #include -#include constexpr int MIN_ROW_HEIGHT = 100; // Minimum readable height per row ManaCurveCategoryWidget::ManaCurveCategoryWidget(QWidget *parent) : QWidget(parent) { - layout = QtUtils::makeLayout(this, 0, 4); + layout = new QVBoxLayout(this); + layout->setSpacing(4); + layout->setContentsMargins(0, 0, 0, 0); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); } @@ -94,7 +95,9 @@ void ManaCurveCategoryWidget::updateDisplay(int minCmc, row->setFixedHeight(MIN_ROW_HEIGHT); - QHBoxLayout *rowLayout = QtUtils::makeLayout(row, 4); + QHBoxLayout *rowLayout = new QHBoxLayout(row); + rowLayout->setContentsMargins(0, 0, 0, 0); + rowLayout->setSpacing(4); QLabel *categoryLabel = new QLabel(cat, row); categoryLabel->setFixedWidth(80); diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.cpp index b958a733f..7e1f0c211 100644 --- a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.cpp @@ -21,7 +21,6 @@ #include #include #include -#include #include #include @@ -48,7 +47,12 @@ ManaDevotionWidget::ManaDevotionWidget(QWidget *parent, DeckListStatisticsAnalyz void ManaDevotionWidget::updateDisplay() { // Clear previous widgets - QtUtils::clearLayoutRec(barLayout); + while (QLayoutItem *item = barLayout->takeAt(0)) { + if (item->widget()) { + item->widget()->deleteLater(); + } + delete item; + } auto &pipCount = analyzer->getDevotionPipCount(); auto &cardCount = analyzer->getDevotionCardCount(); diff --git a/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.cpp index e0ad7af43..9f6257930 100644 --- a/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.cpp @@ -22,7 +22,6 @@ #include #include #include -#include #include DeckAnalyticsWidget::DeckAnalyticsWidget(QWidget *parent, DeckListStatisticsAnalyzer *_statsAnalyzer) @@ -55,7 +54,8 @@ DeckAnalyticsWidget::DeckAnalyticsWidget(QWidget *parent, DeckListStatisticsAnal connect(includeSideboardCheckBox, &QCheckBox::clicked, this, &DeckAnalyticsWidget::includeSideboardChanged); // Scroll area and container - scrollArea = QtUtils::makeScrollArea(this, Qt::ScrollBarAsNeeded, Qt::ScrollBarAsNeeded); + scrollArea = new QScrollArea(this); + scrollArea->setWidgetResizable(true); scrollArea->setFrameShape(QFrame::NoFrame); panelContainer = new QWidget(scrollArea); diff --git a/cockatrice/src/interface/widgets/deck_analytics/resizable_panel.cpp b/cockatrice/src/interface/widgets/deck_analytics/resizable_panel.cpp index 85dba6468..f46679406 100644 --- a/cockatrice/src/interface/widgets/deck_analytics/resizable_panel.cpp +++ b/cockatrice/src/interface/widgets/deck_analytics/resizable_panel.cpp @@ -28,7 +28,6 @@ #include #include #include -#include #include ResizablePanel::ResizablePanel(const QString &_typeId, AbstractAnalyticsPanelWidget *analyticsPanel, QWidget *parent) @@ -36,14 +35,18 @@ ResizablePanel::ResizablePanel(const QString &_typeId, AbstractAnalyticsPanelWid { setAcceptDrops(true); - auto *mainLayout = QtUtils::makeLayout(this, 0); + auto *mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(0, 0, 0, 0); + mainLayout->setSpacing(0); // Frame for selection highlight frame = new QFrame(this); frame->setFrameShape(QFrame::Box); frame->setLineWidth(2); - auto *frameLayout = QtUtils::makeLayout(frame, 0); + auto *frameLayout = new QVBoxLayout(frame); + frameLayout->setContentsMargins(0, 0, 0, 0); + frameLayout->setSpacing(0); // Add the analytics panel frameLayout->addWidget(analyticsPanel); @@ -60,7 +63,9 @@ ResizablePanel::ResizablePanel(const QString &_typeId, AbstractAnalyticsPanelWid // Bottom bar with drag button and resize handle auto *bottomBar = new QWidget(frame); - auto *bottomLayout = QtUtils::makeLayout(bottomBar, 0); + auto *bottomLayout = new QHBoxLayout(bottomBar); + bottomLayout->setContentsMargins(0, 0, 0, 0); + bottomLayout->setSpacing(0); // Drag button on the left dragButton = new QPushButton("☰", bottomBar); diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_filter_dock_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_filter_dock_widget.cpp index 8122d1847..2de187784 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_filter_dock_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_filter_dock_widget.cpp @@ -28,7 +28,6 @@ #include #include #include -#include #include DeckEditorFilterDockWidget::DeckEditorFilterDockWidget(AbstractTabDeckEditor *parent) @@ -81,8 +80,9 @@ void DeckEditorFilterDockWidget::createFiltersDock() filterDelAll->setDefaultAction(aClearFilterAll); filterDelAll->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); - auto *filterLayout = QtUtils::makeLayout(); + auto *filterLayout = new QGridLayout; filterLayout->setObjectName("filterLayout"); + filterLayout->setContentsMargins(0, 0, 0, 0); filterLayout->addWidget(filterBuilder, 0, 0, 1, 3); filterLayout->addWidget(filterView, 1, 0, 1, 3); filterLayout->addWidget(filterDelOne, 2, 0, 1, 1); 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/deck_share/shared_deck_preview_widget.cpp b/cockatrice/src/interface/widgets/deck_share/shared_deck_preview_widget.cpp index ae95ce95c..3bb829089 100644 --- a/cockatrice/src/interface/widgets/deck_share/shared_deck_preview_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_share/shared_deck_preview_widget.cpp @@ -15,7 +15,6 @@ #include #include #include -#include #include SharedDeckPreviewWidget::SharedDeckPreviewWidget(QWidget *parent, @@ -63,7 +62,8 @@ SharedDeckPreviewWidget::SharedDeckPreviewWidget(QWidget *parent, selectionRow->addWidget(selectionCheckBox); selectionRow->addStretch(1); - auto *layout = QtUtils::makeLayout(this); + auto *layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); layout->addLayout(selectionRow); layout->addWidget(bannerCardDisplayWidget, 0, Qt::AlignHCenter); layout->addWidget(colorIdentityWidget, 0, Qt::AlignHCenter); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp index 1e0f37971..9f97ee57a 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp @@ -295,15 +295,9 @@ void DlgConnect::updateDisplayInfo(const QString &saveName) const QString stored = _data.at(4); if (stored.startsWith("$")) { storedVerifier = stored; - passwordEdit->clear(); - passwordEdit->setPlaceholderText(tr("Password saved")); } else { passwordEdit->setText(stored); - passwordEdit->setPlaceholderText(QString()); } - } else { - passwordEdit->clear(); - passwordEdit->setPlaceholderText(QString()); } if (!_data.at(6).isEmpty()) { @@ -329,7 +323,6 @@ void DlgConnect::newHostSelected(bool state) portEdit->setDisabled(false); playernameEdit->clear(); passwordEdit->clear(); - passwordEdit->setPlaceholderText(QString()); storedVerifier.clear(); saveEdit->clear(); saveEdit->setPlaceholderText(tr("Unique Server Name")); 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..f2dd8c1e4 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,9 +12,7 @@ #include #include #include -#include #include -#include DialogConvertDeckToCodFormat::DialogConvertDeckToCodFormat(QWidget *parent) : QDialog(parent) { @@ -58,8 +57,11 @@ bool confirmOverwriteIfExists(QWidget *parent, const QString &filePath) QString newFileName = QDir::toNativeSeparators(fileInfo.path() + "/" + fileInfo.completeBaseName() + ".cod"); if (QFile::exists(newFileName)) { - return QtUtils::confirm(parent, QObject::tr("Overwrite Existing File?"), - QObject::tr("A .cod version of this deck already exists. Overwrite it?")); + QMessageBox::StandardButton reply = + QMessageBox::question(parent, QObject::tr("Overwrite Existing File?"), + QObject::tr("A .cod version of this deck already exists. Overwrite it?"), + QMessageBox::Yes | QMessageBox::No); + return reply == QMessageBox::Yes; } return true; // Safe to proceed } diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_default_tags_editor.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_default_tags_editor.cpp index ee71699d2..9eb064beb 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_default_tags_editor.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_default_tags_editor.cpp @@ -17,7 +17,6 @@ #include #include #include -#include DlgDefaultTagsEditor::DlgDefaultTagsEditor(QWidget *parent) : QDialog(parent) { @@ -121,7 +120,8 @@ void DlgDefaultTagsEditor::addItem() // Add new tag auto *item = new QListWidgetItem(listWidget); QWidget *widget = new QWidget(this); - QHBoxLayout *layout = QtUtils::makeLayout(widget); + QHBoxLayout *layout = new QHBoxLayout(widget); + layout->setContentsMargins(0, 0, 0, 0); // Editable tag label QLineEdit *lineEdit = new QLineEdit(newTag, this); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_edit_avatar.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_edit_avatar.cpp index 7f0141bd5..520ece007 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_edit_avatar.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_edit_avatar.cpp @@ -71,21 +71,7 @@ void DlgEditAvatar::actBrowse() QImageReader imgReader; imgReader.setDecideFormatFromContent(true); - // Honour the EXIF orientation phone cameras write; otherwise portrait - // photos are shown and stored sideways. - imgReader.setAutoTransform(true); imgReader.setFileName(fileName); - - // Decode straight to the maximum avatar size. Scaling afterwards decodes - // the full-resolution image first, which can be tens of megabytes for a - // modern camera photo. - const QSize sourceSize = imgReader.size(); - if (sourceSize.isValid() && - (sourceSize.width() > MAX_AVATAR_DIMENSION || sourceSize.height() > MAX_AVATAR_DIMENSION)) { - const QSize targetSize = sourceSize.scaled(MAX_AVATAR_DIMENSION, MAX_AVATAR_DIMENSION, Qt::KeepAspectRatio); - imgReader.setScaledSize(targetSize); - } - if (!imgReader.read(&image)) { qCWarning(DlgEditAvatarLog) << "Avatar image loading failed for file:" << fileName; imageLabel->setText(tr("Invalid image chosen.")); 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/dialogs/dlg_load_deck_from_website.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_website.cpp index 1c1ada302..88b7e2bb3 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_website.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_website.cpp @@ -27,7 +27,6 @@ #include #include #include -#include #include #include @@ -35,7 +34,7 @@ class QWidget; DlgLoadDeckFromWebsite::DlgLoadDeckFromWebsite(QWidget *parent) : QDialog(parent) { - nam = NetworkManagerFactory::create(this); + nam = new QNetworkAccessManager(this); layout = new QVBoxLayout(this); setLayout(layout); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_my_reports.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_my_reports.cpp index 2a2758ba5..52c9eaef0 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_my_reports.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_my_reports.cpp @@ -6,6 +6,8 @@ #include "libcockatrice/protocol/pb/serverinfo_report.pb.h" #include +#include +#include #include #include #include @@ -24,7 +26,6 @@ #include #include #include -#include #include #include @@ -69,7 +70,13 @@ DlgMyReports::DlgMyReports(AbstractClient *_client, QWidget *parent) auto *chatGroup = new QGroupBox(tr("Chat Log Context")); chatLogEdit = new QTextEdit; chatLogEdit->setReadOnly(true); - chatLogEdit->setFont(QtUtils::monospaceFont()); + QFont monoFont("monospace"); + monoFont.setStyleHint(QFont::Monospace); + const int systemPointSize = QFontDatabase::systemFont(QFontDatabase::GeneralFont).pointSize(); + if (systemPointSize > 0) { + monoFont.setPointSize(systemPointSize); + } + chatLogEdit->setFont(monoFont); auto *chatLayout = new QVBoxLayout(chatGroup); chatLayout->setContentsMargins(4, 4, 4, 4); chatLayout->addWidget(chatLogEdit); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_report_user.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_report_user.cpp index 3ef45020b..b1ae479fe 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_report_user.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_report_user.cpp @@ -5,6 +5,8 @@ #include #include +#include +#include #include #include #include @@ -19,7 +21,6 @@ #include #include #include -#include #include #include #include @@ -91,7 +92,13 @@ DlgReportUser::DlgReportUser(AbstractClient *_client, auto *chatLayout = new QVBoxLayout(chatGroup); chatLogEdit = new QTextEdit; chatLogEdit->setReadOnly(true); - chatLogEdit->setFont(QtUtils::monospaceFont()); + QFont monoFont("monospace"); + monoFont.setStyleHint(QFont::Monospace); + const int systemPointSize = QFontDatabase::systemFont(QFontDatabase::GeneralFont).pointSize(); + if (systemPointSize > 0) { + monoFont.setPointSize(systemPointSize); + } + chatLogEdit->setFont(monoFont); if (!_autoChatLog.isEmpty()) { chatLogEdit->setPlainText(_autoChatLog); } else { @@ -128,8 +135,12 @@ void DlgReportUser::actSubmit() return; } - if (!QtUtils::confirm(this, tr("Confirm Report"), - tr("Submit report against %1 for %2?").arg(reportedUser, categoryBox->currentText()))) { + QMessageBox::StandardButton reply = + QMessageBox::question(this, tr("Confirm Report"), + tr("Submit report against %1 for %2?").arg(reportedUser, categoryBox->currentText()), + QMessageBox::Yes | QMessageBox::No); + + if (reply != QMessageBox::Yes) { return; } diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp index 1a4f0aeb9..55c6569f7 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp @@ -50,7 +50,6 @@ #include #include #include -#include #include #include #include @@ -72,7 +71,10 @@ DlgSelectSetForCards::DlgSelectSetForCards(QWidget *parent, DeckStateManager *de QSplitter *splitter = new QSplitter(Qt::Vertical, this); // Top scroll area - scrollArea = QtUtils::makeScrollArea(this); + scrollArea = new QScrollArea(this); + scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + scrollArea->setWidgetResizable(true); listContainer = new QWidget(scrollArea); listLayout = new QVBoxLayout(listContainer); @@ -84,10 +86,14 @@ DlgSelectSetForCards::DlgSelectSetForCards(QWidget *parent, DeckStateManager *de // Left container QWidget *leftContainer = new QWidget(this); - QVBoxLayout *leftLayout = QtUtils::makeLayout(leftContainer); + QVBoxLayout *leftLayout = new QVBoxLayout(leftContainer); + leftLayout->setContentsMargins(0, 0, 0, 0); uneditedCardsLabel = new QLabel(this); - uneditedCardsArea = QtUtils::makeScrollArea(this); + uneditedCardsArea = new QScrollArea(this); + uneditedCardsArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + uneditedCardsArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + uneditedCardsArea->setWidgetResizable(true); uneditedCardsFlowWidget = new FlowWidget(uneditedCardsArea, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); @@ -99,10 +105,14 @@ DlgSelectSetForCards::DlgSelectSetForCards(QWidget *parent, DeckStateManager *de // Right container QWidget *rightContainer = new QWidget(this); - QVBoxLayout *rightLayout = QtUtils::makeLayout(rightContainer); + QVBoxLayout *rightLayout = new QVBoxLayout(rightContainer); + rightLayout->setContentsMargins(0, 0, 0, 0); modifiedCardsLabel = new QLabel(this); - modifiedCardsArea = QtUtils::makeScrollArea(this); + modifiedCardsArea = new QScrollArea(this); + modifiedCardsArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + modifiedCardsArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + modifiedCardsArea->setWidgetResizable(true); modifiedCardsFlowWidget = new FlowWidget(modifiedCardsArea, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); @@ -264,9 +274,9 @@ QMap DlgSelectSetForCards::getSetsForCards() continue; } - const auto &setMap = infoPtr->getSets(); - for (auto it = setMap.constBegin(); it != setMap.constEnd(); ++it) { - setCounts[it.key()]++; + SetToPrintingsMap setMap = infoPtr->getSets(); + for (auto &setName : setMap.keys()) { + setCounts[setName]++; } } @@ -383,8 +393,8 @@ QMap DlgSelectSetForCards::getCardsForSets() continue; } - const auto &setMap = infoPtr->getSets(); - for (auto it = setMap.constBegin(); it != setMap.constEnd(); ++it) { + SetToPrintingsMap setMap = infoPtr->getSets(); + for (auto it = setMap.begin(); it != setMap.end(); ++it) { setCards[it.key()].append(cardName); } } diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_shared_decks_preview.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_shared_decks_preview.cpp index d29aee27f..8ad91dcaf 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_shared_decks_preview.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_shared_decks_preview.cpp @@ -13,7 +13,6 @@ #include #include #include -#include #include class QWidget; @@ -35,7 +34,8 @@ DlgSharedDecksPreview::DlgSharedDecksPreview(QWidget *parent, // QLabels render AutoText and markup would otherwise be shown as rich text. auto *titleLabel = new QLabel(tr("Share: %1").arg((shareName.isEmpty() ? tr("Untitled") : shareName).toHtmlEscaped()), this); - QFont titleFont = QtUtils::boldFont(titleLabel->font()); + QFont titleFont = titleLabel->font(); + titleFont.setBold(true); titleFont.setPointSize(titleFont.pointSize() + 2); titleLabel->setFont(titleFont); mainLayout->addWidget(titleLabel); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp index ca55c7b41..76bb13248 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp @@ -30,7 +30,6 @@ #include #include #include -#include #include #include @@ -171,13 +170,16 @@ void DlgUpdate::finishedUpdateCheck(bool needToUpdate, bool isCompatible, Releas publishDate = release->getPublishDate().toString(QLocale().dateFormat(QLocale::LongFormat)); if (isCompatible) { - const QString message = tr("A new version of Cockatrice is available!") + "

" + "" + - tr("New version") + QString(": %1
").arg(release->getName()) + "" + - tr("Released") + - QString(": %1 (").arg(publishDate, release->getDescriptionUrl()) + - tr("Changelog") + ")

" + tr("Do you want to update now?"); + int reply; + reply = QMessageBox::question( + window(), tr("Update Available"), + tr("A new version of Cockatrice is available!") + "

" + "" + tr("New version") + + QString(": %1
").arg(release->getName()) + "" + tr("Released") + + QString(": %1 (").arg(publishDate, release->getDescriptionUrl()) + tr("Changelog") + + ")

" + tr("Do you want to update now?"), + QMessageBox::Yes | QMessageBox::No); - if (QtUtils::confirm(window(), tr("Update Available"), message)) { + if (reply == QMessageBox::Yes) { downloadUpdate(release->getName()); } else { closeDialog(); diff --git a/cockatrice/src/interface/widgets/dialogs/override_printing_warning.cpp b/cockatrice/src/interface/widgets/dialogs/override_printing_warning.cpp index 8230dc3bc..57f7ff7f5 100644 --- a/cockatrice/src/interface/widgets/dialogs/override_printing_warning.cpp +++ b/cockatrice/src/interface/widgets/dialogs/override_printing_warning.cpp @@ -7,7 +7,6 @@ #include #include #include -#include class QWidget; @@ -31,7 +30,10 @@ bool OverridePrintingWarning::execMessageBox(QWidget *parent, bool enable) "Are you sure you would like to disable this feature?"); } - if (QtUtils::confirm(parent, QObject::tr("Confirm Change"), message)) { + QMessageBox::StandardButton result = + QMessageBox::question(parent, QObject::tr("Confirm Change"), message, QMessageBox::Yes | QMessageBox::No); + + if (result == QMessageBox::Yes) { SettingsCache::instance().cardsDisplay().setOverrideAllCardArtWithPersonalPreference(enable); // Caches are now invalid. CardPictureLoader::clearPixmapCache(); diff --git a/cockatrice/src/interface/widgets/dialogs/settings_view.cpp b/cockatrice/src/interface/widgets/dialogs/settings_view.cpp index 6ab754f4e..a2f1df02c 100644 --- a/cockatrice/src/interface/widgets/dialogs/settings_view.cpp +++ b/cockatrice/src/interface/widgets/dialogs/settings_view.cpp @@ -60,7 +60,6 @@ #include #include #include -#include #include /** @@ -72,8 +71,10 @@ static QScrollArea *makeScrollable(QWidget *widget) { widget->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Maximum); - auto *scrollArea = QtUtils::makeScrollArea(); + auto *scrollArea = new QScrollArea; + scrollArea->setWidgetResizable(true); scrollArea->setContentsMargins(0, 0, 0, 0); + scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); scrollArea->horizontalScrollBar()->setEnabled(false); scrollArea->setWidget(widget); return scrollArea; @@ -624,8 +625,13 @@ void SettingsView::scrollToAndFlash(QWidget *target) if (!target) { return; } - if (auto *scrollArea = QtUtils::findParentOfType(target, true)) { - scrollArea->ensureWidgetVisible(target); + QWidget *widget = target; + while (widget) { + if (auto *scrollArea = qobject_cast(widget)) { + scrollArea->ensureWidgetVisible(target); + break; + } + widget = widget->parentWidget(); } flashWidget(target); } diff --git a/cockatrice/src/interface/widgets/general/home_widget.cpp b/cockatrice/src/interface/widgets/general/home_widget.cpp index 5905b2c40..c77fd76fd 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 @@ -113,32 +113,6 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor) &HomeWidget::updateButtonsToBackgroundColor); } -void HomeWidget::retranslateUi() -{ - visualDeckEditorButton->setText(tr("Create New Deck")); - visualDeckStorageButton->setText(tr("Browse Decks")); - visualDatabaseDisplayButton->setText(tr("Browse Card Database")); - edhrecButton->setText(tr("Browse EDHRec")); - archidektButton->setText(tr("Browse Archidekt")); - replayButton->setText(tr("View Replays")); - if (exitButton) { - exitButton->setText(tr("Quit")); - } - - switch (connectionStatus) { - case StatusDisconnected: - connectButton->setText(tr("Connect")); - break; - case StatusLoggedIn: - connectButton->setText(tr("Play")); - break; - default: - // Connecting, registering, activating, logging in, etc. - connectButton->setText(tr("Connecting...")); - break; - } -} - void HomeWidget::initializeBackgroundFromSource() { // The featured logo is theme/scheme-derived too; reload it alongside the @@ -310,32 +284,32 @@ QGroupBox *HomeWidget::createButtons() updateLogoOverlay(); boxLayout->addSpacing(25); - connectButton = new HomeStyledButton(QString(), gradientColors); + connectButton = new HomeStyledButton("Connect/Play", gradientColors); boxLayout->addWidget(connectButton); - visualDeckEditorButton = new HomeStyledButton(QString(), gradientColors); + auto visualDeckEditorButton = new HomeStyledButton(tr("Create New Deck"), gradientColors); connect(visualDeckEditorButton, &QPushButton::clicked, tabSupervisor, [this] { tabSupervisor->openDeckInNewTab(LoadedDeck()); }); boxLayout->addWidget(visualDeckEditorButton); - visualDeckStorageButton = new HomeStyledButton(QString(), gradientColors); + auto visualDeckStorageButton = new HomeStyledButton(tr("Browse Decks"), gradientColors); connect(visualDeckStorageButton, &QPushButton::clicked, tabSupervisor, [this] { tabSupervisor->actTabVisualDeckStorage(true); }); boxLayout->addWidget(visualDeckStorageButton); - visualDatabaseDisplayButton = new HomeStyledButton(QString(), gradientColors); + auto visualDatabaseDisplayButton = new HomeStyledButton(tr("Browse Card Database"), gradientColors); connect(visualDatabaseDisplayButton, &QPushButton::clicked, tabSupervisor, &TabSupervisor::addVisualDatabaseDisplayTab); boxLayout->addWidget(visualDatabaseDisplayButton); - edhrecButton = new HomeStyledButton(QString(), gradientColors); + auto edhrecButton = new HomeStyledButton(tr("Browse EDHRec"), gradientColors); connect(edhrecButton, &QPushButton::clicked, tabSupervisor, &TabSupervisor::addEdhrecMainTab); boxLayout->addWidget(edhrecButton); - archidektButton = new HomeStyledButton(QString(), gradientColors); + auto archidektButton = new HomeStyledButton(tr("Browse Archidekt"), gradientColors); connect(archidektButton, &QPushButton::clicked, tabSupervisor, &TabSupervisor::addArchidektTab); boxLayout->addWidget(archidektButton); - replayButton = new HomeStyledButton(QString(), gradientColors); - connect(replayButton, &QPushButton::clicked, tabSupervisor, [this] { tabSupervisor->actTabReplays(true); }); - boxLayout->addWidget(replayButton); + auto replaybutton = new HomeStyledButton(tr("View Replays"), gradientColors); + connect(replaybutton, &QPushButton::clicked, tabSupervisor, [this] { tabSupervisor->actTabReplays(true); }); + boxLayout->addWidget(replaybutton); if (qobject_cast(tabSupervisor->parentWidget())) { - exitButton = new HomeStyledButton(QString(), gradientColors); + auto exitButton = new HomeStyledButton(tr("Quit"), gradientColors); connect(exitButton, &QPushButton::clicked, qobject_cast(tabSupervisor->parentWidget()), &MainWindow::actExit); boxLayout->addWidget(exitButton); @@ -347,18 +321,20 @@ QGroupBox *HomeWidget::createButtons() void HomeWidget::updateConnectButton(const ClientStatus status) { - connectionStatus = status; disconnect(connectButton, &QPushButton::clicked, nullptr, nullptr); switch (status) { case StatusConnecting: + connectButton->setText(tr("Connecting...")); connectButton->setEnabled(false); break; case StatusDisconnected: + connectButton->setText(tr("Connect")); connectButton->setEnabled(true); connect(connectButton, &QPushButton::clicked, qobject_cast(tabSupervisor->parentWidget()), &MainWindow::actConnect); break; case StatusLoggedIn: + connectButton->setText(tr("Play")); connectButton->setEnabled(true); connect(connectButton, &QPushButton::clicked, tabSupervisor, &TabSupervisor::switchToFirstAvailableNetworkTab); @@ -366,7 +342,6 @@ void HomeWidget::updateConnectButton(const ClientStatus status) default: break; } - retranslateUi(); } QPair HomeWidget::extractDominantColors(const QPixmap &pixmap) diff --git a/cockatrice/src/interface/widgets/general/home_widget.h b/cockatrice/src/interface/widgets/general/home_widget.h index aa4745c82..fb9566cad 100644 --- a/cockatrice/src/interface/widgets/general/home_widget.h +++ b/cockatrice/src/interface/widgets/general/home_widget.h @@ -33,7 +33,6 @@ class HomeWidget : public QWidget public: HomeWidget(QWidget *parent, TabSupervisor *tabSupervisor); void updateRandomCard(); - void retranslateUi(); static QPair extractDominantColors(const QPixmap &pixmap); public slots: @@ -54,15 +53,7 @@ private: DeckList backgroundSourceDeck; QLabel *logoLabel = nullptr; QPair gradientColors; - HomeStyledButton *connectButton = nullptr; - HomeStyledButton *visualDeckEditorButton = nullptr; - HomeStyledButton *visualDeckStorageButton = nullptr; - HomeStyledButton *visualDatabaseDisplayButton = nullptr; - HomeStyledButton *edhrecButton = nullptr; - HomeStyledButton *archidektButton = nullptr; - HomeStyledButton *replayButton = nullptr; - HomeStyledButton *exitButton = nullptr; - ClientStatus connectionStatus = StatusDisconnected; + HomeStyledButton *connectButton; void setRandomCard(ExactCard &newCard); void loadBackgroundSourceDeck(); diff --git a/cockatrice/src/interface/widgets/general/layout_containers/flow_widget.cpp b/cockatrice/src/interface/widgets/general/layout_containers/flow_widget.cpp index d7260912f..caeecad86 100644 --- a/cockatrice/src/interface/widgets/general/layout_containers/flow_widget.cpp +++ b/cockatrice/src/interface/widgets/general/layout_containers/flow_widget.cpp @@ -19,7 +19,6 @@ #include #include #include -#include /** * @brief Constructs a FlowWidget. @@ -48,16 +47,21 @@ FlowWidget::FlowWidget(QWidget *parent, setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); } - mainLayout = QtUtils::makeLayout(this); + mainLayout = new QHBoxLayout(this); + mainLayout->setContentsMargins(0, 0, 0, 0); setLayout(mainLayout); const bool useScrollArea = (horizontalPolicy != Qt::ScrollBarAlwaysOff || verticalPolicy != Qt::ScrollBarAlwaysOff); // Scroll area (optional) if (useScrollArea) { - scrollArea = QtUtils::makeScrollArea(this, verticalPolicy, horizontalPolicy); + scrollArea = new QScrollArea(this); + scrollArea->setWidgetResizable(true); scrollArea->setMinimumSize(0, 0); scrollArea->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + // Set scrollbar policies + scrollArea->setHorizontalScrollBarPolicy(horizontalPolicy); + scrollArea->setVerticalScrollBarPolicy(verticalPolicy); } else { scrollArea = nullptr; } @@ -132,7 +136,13 @@ void FlowWidget::removeWidget(QWidget *widgetToRemove) const void FlowWidget::clearLayout() { if (flowLayout) { - QtUtils::clearLayoutRec(flowLayout); + QLayoutItem *item; + while ((item = flowLayout->takeAt(0))) { + if (item->widget()) { + item->widget()->deleteLater(); + } + delete item; + } } else { // Defensive fallback: recreate the layout if it was deleted externally. flowLayout = new FlowLayout(container, flowDirection); diff --git a/cockatrice/src/interface/widgets/general/layout_containers/overlap_widget.cpp b/cockatrice/src/interface/widgets/general/layout_containers/overlap_widget.cpp index bc2d1cc4d..03240211c 100644 --- a/cockatrice/src/interface/widgets/general/layout_containers/overlap_widget.cpp +++ b/cockatrice/src/interface/widgets/general/layout_containers/overlap_widget.cpp @@ -4,7 +4,6 @@ #include #include -#include /** * @class OverlapWidget @@ -81,7 +80,11 @@ void OverlapWidget::removeWidget(QWidget *widgetToRemove) const void OverlapWidget::clearLayout() { if (overlapLayout != nullptr) { - QtUtils::clearLayoutRec(overlapLayout); + QLayoutItem *item; + while ((item = overlapLayout->takeAt(0)) != nullptr) { + item->widget()->deleteLater(); + delete item; + } } // If layout is null, create a new layout; otherwise, reuse the existing one diff --git a/cockatrice/src/interface/widgets/onboarding/first_run_wizard.cpp b/cockatrice/src/interface/widgets/onboarding/first_run_wizard.cpp index c4298d6b6..426811ec2 100644 --- a/cockatrice/src/interface/widgets/onboarding/first_run_wizard.cpp +++ b/cockatrice/src/interface/widgets/onboarding/first_run_wizard.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include @@ -32,8 +31,9 @@ FirstRunWizard::FirstRunWizard(QWidget *parent) : QDialog(parent) bannerHost = new BannerHost(this); titleLabel = new QLabel(this); - QFont titleFont = QtUtils::boldFont(titleLabel->font()); + QFont titleFont = titleLabel->font(); titleFont.setPointSizeF(titleFont.pointSizeF() * 1.4); + titleFont.setBold(true); titleLabel->setFont(titleFont); subtitleLabel = new QLabel(this); @@ -51,7 +51,8 @@ FirstRunWizard::FirstRunWizard(QWidget *parent) : QDialog(parent) connect(skipButton, &QPushButton::clicked, this, &FirstRunWizard::skip); connect(nextButton, &QPushButton::clicked, this, &FirstRunWizard::goNext); - auto *headerLayout = QtUtils::makeLayout(); + auto *headerLayout = new QVBoxLayout; + headerLayout->setContentsMargins(0, 0, 0, 0); headerLayout->addWidget(bannerHost); headerLayout->addSpacing(12); headerLayout->addWidget(titleLabel); diff --git a/cockatrice/src/interface/widgets/onboarding/pages/preferences_setup_page.cpp b/cockatrice/src/interface/widgets/onboarding/pages/preferences_setup_page.cpp index 486ada155..da08e3a12 100644 --- a/cockatrice/src/interface/widgets/onboarding/pages/preferences_setup_page.cpp +++ b/cockatrice/src/interface/widgets/onboarding/pages/preferences_setup_page.cpp @@ -20,7 +20,6 @@ #include #include #include -#include namespace { @@ -69,8 +68,9 @@ PreferencesSetupPage::PreferencesSetupPage(QWidget *parent) : FirstRunWizardPage contentLayout->addStretch(); - auto *scrollArea = QtUtils::makeScrollArea(this, Qt::ScrollBarAsNeeded, Qt::ScrollBarAsNeeded); + auto *scrollArea = new QScrollArea(this); scrollArea->setWidget(content); + scrollArea->setWidgetResizable(true); scrollArea->setFrameShape(QFrame::NoFrame); auto *layout = new QVBoxLayout(this); 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/onboarding/shader_banner_widget.cpp b/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.cpp index 1074ff6e5..c17399957 100644 --- a/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.cpp +++ b/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.cpp @@ -19,7 +19,6 @@ #include #include #include -#include #include #include #include @@ -152,7 +151,8 @@ BannerHost::BannerHost(QWidget *parent) : QWidget(parent) { setFixedHeight(150); - stack = QtUtils::makeLayout(this); + stack = new QStackedLayout(this); + stack->setContentsMargins(0, 0, 0, 0); fallback = new GradientFallbackWidget(this); stack->addWidget(fallback); diff --git a/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp b/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp index eae49d7ef..4b5040741 100644 --- a/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp @@ -22,7 +22,6 @@ #include #include #include -#include #include class QModelIndex; @@ -43,7 +42,9 @@ CardAmountWidget::CardAmountWidget(QWidget *parent, : QWidget(parent), deckStateManager(deckStateManager), cardSizeSlider(cardSizeSlider), rootCard(rootCard), zoneName(zoneName), hovered(false) { - layout = QtUtils::makeLayout(this, 10); + layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(10); this->setLayout(layout); this->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); layout->setAlignment(Qt::AlignHCenter); diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp b/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp index b8438134c..27ae61b94 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp @@ -32,7 +32,6 @@ #include #include #include -#include #include /** @@ -85,7 +84,9 @@ PrintingSelector::PrintingSelector(QWidget *parent, AbstractTabDeckEditor *_deck displayOptionsWidget->addSettingsWidget(cardSizeWidget); sortAndOptionsContainer = new QWidget(this); - sortAndOptionsLayout = QtUtils::makeLayout(sortAndOptionsContainer, 0, 3); + sortAndOptionsLayout = new QHBoxLayout(sortAndOptionsContainer); + sortAndOptionsLayout->setSpacing(3); + sortAndOptionsLayout->setContentsMargins(0, 0, 0, 0); sortAndOptionsContainer->setLayout(sortAndOptionsLayout); searchBar = new PrintingSelectorCardSearchWidget(this); @@ -228,7 +229,7 @@ void PrintingSelector::getAllSetsForCurrentCard() return; } - const auto &setMap = selectedCard->getSets(); + SetToPrintingsMap setMap = selectedCard->getSets(); const QList sortedPrintings = sortToolBar->sortSets(setMap); const QList filteredPrintings = sortToolBar->filterSets(sortedPrintings, searchBar->getSearchText().trimmed().toLower()); diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp index a5177a6bc..5d535397f 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp @@ -44,7 +44,6 @@ #include #include #include -#include #include class DeckStateManager; @@ -113,7 +112,9 @@ PrintingSelectorCardOverlayWidget::PrintingSelectorCardOverlayWidget(QWidget *pa : QWidget(parent), deckEditor(_deckEditor), rootCard(_rootCard) { // Set up the main layout - auto *mainLayout = QtUtils::makeLayout(this, 0); + auto *mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(0, 0, 0, 0); + mainLayout->setSpacing(0); setLayout(mainLayout); // Add CardInfoPictureWidget diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_sorting_widget.cpp b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_sorting_widget.cpp index 1cd372259..f47ba0746 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_sorting_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_sorting_widget.cpp @@ -208,7 +208,7 @@ QList PrintingSelectorCardSortingWidget::prependPrintingsInDeck(co return {}; } - const auto &setMap = selectedCard->getSets(); + SetToPrintingsMap setMap = selectedCard->getSets(); QList> countList; // Collect sets with their counts diff --git a/cockatrice/src/interface/widgets/printing_selector/set_name_and_collectors_number_display_widget.cpp b/cockatrice/src/interface/widgets/printing_selector/set_name_and_collectors_number_display_widget.cpp index d8c67c2a9..9bc92ad90 100644 --- a/cockatrice/src/interface/widgets/printing_selector/set_name_and_collectors_number_display_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/set_name_and_collectors_number_display_widget.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include /** @@ -20,7 +19,8 @@ SetNameAndCollectorsNumberDisplayWidget::SetNameAndCollectorsNumberDisplayWidget : QWidget(parent) { // Set up the layout for the widget - layout = QtUtils::makeLayout(this); + layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(1); setLayout(layout); diff --git a/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.cpp b/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.cpp index c6a6d989f..6eaacfa74 100644 --- a/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.cpp +++ b/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.cpp @@ -15,7 +15,6 @@ #include #include #include -#include #include SettingsButtonWidget::SettingsButtonWidget(QWidget *parent) @@ -27,8 +26,9 @@ SettingsButtonWidget::SettingsButtonWidget(QWidget *parent) connect(button, &QToolButton::clicked, this, &SettingsButtonWidget::togglePopup); connect(popup, &SettingsPopupWidget::aboutToClose, this, &SettingsButtonWidget::onPopupClosed); - layout = QtUtils::makeLayout(this); + layout = new QHBoxLayout(this); layout->addWidget(button); + layout->setContentsMargins(0, 0, 0, 0); setLayout(layout); } diff --git a/cockatrice/src/interface/widgets/quick_settings/settings_popup_widget.cpp b/cockatrice/src/interface/widgets/quick_settings/settings_popup_widget.cpp index 351e3732c..7ace155a7 100644 --- a/cockatrice/src/interface/widgets/quick_settings/settings_popup_widget.cpp +++ b/cockatrice/src/interface/widgets/quick_settings/settings_popup_widget.cpp @@ -8,7 +8,6 @@ #include #include #include -#include #include SettingsPopupWidget::SettingsPopupWidget(QWidget *parent) : QWidget(parent, Qt::Popup | Qt::FramelessWindowHint) @@ -55,8 +54,11 @@ void SettingsPopupWidget::adjustSizeToFitScreen() if (contentHeight > maxHeight) { // Create a scroll area and add the container widget to it if not already created if (!scrollArea) { - scrollArea = QtUtils::makeScrollArea(this, Qt::ScrollBarAlwaysOn); + scrollArea = new QScrollArea(this); + scrollArea->setWidgetResizable(true); scrollArea->setFrameShape(QFrame::NoFrame); + scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // No horizontal scrollbar + scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); // Enable vertical scrollbar when needed } // Set maximum height for the scroll area and show vertical scrollbar 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_quick_settings_widget.cpp b/cockatrice/src/interface/widgets/replay/replay_quick_settings_widget.cpp index 0a64fb4d2..5b3829af2 100644 --- a/cockatrice/src/interface/widgets/replay/replay_quick_settings_widget.cpp +++ b/cockatrice/src/interface/widgets/replay/replay_quick_settings_widget.cpp @@ -11,7 +11,6 @@ #include #include #include -#include ReplayQuickSettingsWidget::ReplayQuickSettingsWidget(QWidget *parent) : SettingsButtonWidget(parent) { @@ -29,7 +28,8 @@ ReplayQuickSettingsWidget::ReplayQuickSettingsWidget(QWidget *parent) : Settings // putting it all together auto *widget = new QWidget; - auto *grid = QtUtils::makeLayout(widget); + auto *grid = new QGridLayout(widget); + grid->setContentsMargins(0, 0, 0, 0); grid->addWidget(&fastForwardSpeedLabel, 0, 0, 1, 1); grid->addWidget(&fastForwardSpeedBox, 0, 1, 1, 1); grid->addWidget(&skipEmptyCheckBox, 1, 0, 1, 2); 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/game_selector.cpp b/cockatrice/src/interface/widgets/server/game_selector.cpp index 2c0050afe..137658164 100644 --- a/cockatrice/src/interface/widgets/server/game_selector.cpp +++ b/cockatrice/src/interface/widgets/server/game_selector.cpp @@ -41,7 +41,6 @@ #include #include #include -#include #include #include #include @@ -421,8 +420,10 @@ void GameSelector::joinGame(const ServerInfo_Game &game, const bool asSpectator, // spectator join, so ask first instead of surprising the player. const bool gameFull = game.player_count() == game.max_players(); if (gameFull && !asSpectator && !asJudge && !overrideRestrictions) { - if (!QtUtils::confirm(this, tr("Join game"), tr("The game is full. Join as a spectator instead?"), - QMessageBox::No)) { + const QMessageBox::StandardButton answer = + QMessageBox::question(this, tr("Join game"), tr("The game is full. Join as a spectator instead?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + if (answer != QMessageBox::Yes) { return; } } diff --git a/cockatrice/src/interface/widgets/server/game_selector_quick_filter_toolbar.cpp b/cockatrice/src/interface/widgets/server/game_selector_quick_filter_toolbar.cpp index 58f9bb8a9..0fc3d879c 100644 --- a/cockatrice/src/interface/widgets/server/game_selector_quick_filter_toolbar.cpp +++ b/cockatrice/src/interface/widgets/server/game_selector_quick_filter_toolbar.cpp @@ -14,7 +14,6 @@ #include #include #include -#include class TabSupervisor; template class QMap; @@ -25,7 +24,8 @@ GameSelectorQuickFilterToolBar::GameSelectorQuickFilterToolBar(QWidget *parent, const QMap &allGameTypes) : QWidget(parent), tabSupervisor(_tabSupervisor), model(_model) { - mainLayout = QtUtils::makeLayout(this); + mainLayout = new QHBoxLayout(this); + mainLayout->setContentsMargins(0, 0, 0, 0); mainLayout->setSpacing(5); const GameFilterConfigs &filters = model->getFilters(); diff --git a/cockatrice/src/interface/widgets/server/handle_public_servers.cpp b/cockatrice/src/interface/widgets/server/handle_public_servers.cpp index 1c0cbf962..08b7837dd 100644 --- a/cockatrice/src/interface/widgets/server/handle_public_servers.cpp +++ b/cockatrice/src/interface/widgets/server/handle_public_servers.cpp @@ -13,14 +13,13 @@ #include #include #include -#include #include #include #define PUBLIC_SERVERS_JSON "https://cockatrice.github.io/public-servers.json" HandlePublicServers::HandlePublicServers(QObject *parent) - : QObject(parent), nam(NetworkManagerFactory::create(this)), reply(nullptr) + : QObject(parent), nam(new QNetworkAccessManager(this)), reply(nullptr) { } 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..c8fe97cda 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 @@ -49,7 +49,6 @@ #include #include #include -#include #include #include @@ -686,8 +685,10 @@ void UserContextMenu::execRemoveFromIgnore(const QString &userName) void UserContextMenu::execKick(int playerId) { - if (QtUtils::confirm(static_cast(parent()), tr("Kick Player"), - tr("Are you sure you want to kick this player from the game?"), QMessageBox::No)) { + auto result = QMessageBox::question(static_cast(parent()), tr("Kick Player"), + tr("Are you sure you want to kick this player from the game?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + if (result == QMessageBox::Yes) { Command_KickFromGame cmd; cmd.set_player_id(playerId); diff --git a/cockatrice/src/interface/widgets/server/user/user_info_box.cpp b/cockatrice/src/interface/widgets/server/user/user_info_box.cpp index fa7698037..60bc97edc 100644 --- a/cockatrice/src/interface/widgets/server/user/user_info_box.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_info_box.cpp @@ -32,7 +32,7 @@ #include #include #include -#include +#include #include #include @@ -53,7 +53,8 @@ UserInfoBox::UserInfoBox(AbstractClient *_client, bool _editable, QWidget *paren avatarPic.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); avatarPic.setAlignment(Qt::AlignCenter); - auto *avatarLayout = QtUtils::makeLayout(); + auto *avatarLayout = new QHBoxLayout; + avatarLayout->setContentsMargins(0, 0, 0, 0); avatarLayout->addStretch(1); avatarLayout->addWidget(&avatarPic, 3); avatarLayout->addStretch(1); @@ -298,7 +299,17 @@ void UserInfoBox::actPassword() void UserInfoBox::changePassword(const QString &oldPassword, const QString &newPassword) { - const Command_AccountPassword cmd = client->makeAccountPasswordCommand(oldPassword, newPassword); + Command_AccountPassword cmd; + cmd.set_old_password(oldPassword.toStdString()); + if (client->getServerSupportsChallengeResponse()) { + cmd.set_hashed_new_password(PasswordHasher::generatePasswordVerifier(newPassword).toStdString()); + } else if (client->getServerSupportsPasswordHash()) { + auto passwordSalt = PasswordHasher::generateRandomSalt(); + QString hashedPassword = PasswordHasher::computeHash(newPassword, passwordSalt); + cmd.set_hashed_new_password(hashedPassword.toStdString()); + } else { + cmd.set_new_password(newPassword.toStdString()); + } PendingCommand *pend = client->prepareSessionCommand(cmd); connect(pend, &PendingCommand::finished, this, &UserInfoBox::processPasswordResponse); diff --git a/cockatrice/src/interface/widgets/server/user/user_info_connection.cpp b/cockatrice/src/interface/widgets/server/user/user_info_connection.cpp index 2e864824c..d23ac63ba 100644 --- a/cockatrice/src/interface/widgets/server/user/user_info_connection.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_info_connection.cpp @@ -29,10 +29,19 @@ QMap> UserConnection_Inf ServersSettings &servers = SettingsCache::instance().servers(); - for (const auto &entry : servers.getServers()) { - UserConnection_Information userInfo(entry.saveName, entry.host, entry.port, entry.username, entry.password, - entry.savePassword, entry.site); - serverList.insert(entry.saveName, std::make_pair(entry.host, userInfo)); + int size = servers.getValue("totalServers", "server", "server_details").toInt() + 1; + + for (int i = 0; i < size; i++) { + QString _saveName = servers.getValue(QString("saveName%1").arg(i), "server", "server_details").toString(); + QString serverName = servers.getValue(QString("server%1").arg(i), "server", "server_details").toString(); + QString portNum = servers.getValue(QString("port%1").arg(i), "server", "server_details").toString(); + QString userName = servers.getValue(QString("username%1").arg(i), "server", "server_details").toString(); + QString pass = servers.getValue(QString("password%1").arg(i), "server", "server_details").toString(); + bool savePass = servers.getValue(QString("savePassword%1").arg(i), "server", "server_details").toBool(); + QString _site = servers.getValue(QString("site%1").arg(i), "server", "server_details").toString(); + + UserConnection_Information userInfo(_saveName, serverName, portNum, userName, pass, savePass, _site); + serverList.insert(_saveName, std::make_pair(serverName, userInfo)); } return serverList; @@ -44,18 +53,28 @@ QStringList UserConnection_Information::getServerInfo(const QString &find) ServersSettings &servers = SettingsCache::instance().servers(); - for (const auto &entry : servers.getServers()) { - if (find != entry.saveName) { + int size = servers.getValue("totalServers", "server", "server_details").toInt() + 1; + for (int i = 0; i < size; i++) { + QString _saveName = servers.getValue(QString("saveName%1").arg(i), "server", "server_details").toString(); + + if (find != _saveName) { continue; } - _server.append(entry.saveName); - _server.append(entry.host); - _server.append(entry.port); - _server.append(entry.username); - _server.append(entry.password); - _server.append(entry.savePassword ? "1" : "0"); - _server.append(entry.site); + QString serverName = servers.getValue(QString("server%1").arg(i), "server", "server_details").toString(); + QString portNum = servers.getValue(QString("port%1").arg(i), "server", "server_details").toString(); + QString userName = servers.getValue(QString("username%1").arg(i), "server", "server_details").toString(); + QString pass = servers.getValue(QString("password%1").arg(i), "server", "server_details").toString(); + bool savePass = servers.getValue(QString("savePassword%1").arg(i), "server", "server_details").toBool(); + QString _site = servers.getValue(QString("site%1").arg(i), "server", "server_details").toString(); + + _server.append(_saveName); + _server.append(serverName); + _server.append(portNum); + _server.append(userName); + _server.append(pass); + _server.append(savePass ? "1" : "0"); + _server.append(_site); break; } diff --git a/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp b/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp index efb3b2e31..c3e880084 100644 --- a/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp @@ -55,7 +55,6 @@ #include #include #include -#include #include #include @@ -403,7 +402,9 @@ UserInfoPopup::UserInfoPopup(TabSupervisor *_ts, void UserInfoPopup::buildUi() { - auto *root = QtUtils::makeLayout(this, 0); + auto *root = new QVBoxLayout(this); + root->setContentsMargins(0, 0, 0, 0); + root->setSpacing(0); // Header header = new UserInfoHeaderWidget(this); diff --git a/cockatrice/src/interface/widgets/server/user/user_list_panel_widget.cpp b/cockatrice/src/interface/widgets/server/user/user_list_panel_widget.cpp index 5a34c852b..78d45d120 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_panel_widget.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_list_panel_widget.cpp @@ -9,7 +9,6 @@ #include #include #include -#include namespace { @@ -32,7 +31,9 @@ QString sectionKey(UserListWidget::Section section) UserListPanelWidget::UserListPanelWidget(TabSupervisor *_tabSupervisor, AbstractClient *_client, QWidget *parent) : QWidget(parent) { - auto *mainLayout = QtUtils::makeLayout(this, 2); + auto *mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(0, 0, 0, 0); + mainLayout->setSpacing(2); searchBar = new QLineEdit(this); searchBar->setClearButtonEnabled(true); diff --git a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp index a20b39b82..b3fd1d1a9 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp @@ -13,10 +13,10 @@ #include "user_list_painter.h" #include "user_list_proxy.h" +#include #include #include #include -#include #include #include #include @@ -41,6 +41,7 @@ #include #include #include +#include #include #include #include @@ -56,9 +57,7 @@ #include #include #include -#include #include -#include #include class QPixmap; @@ -274,11 +273,6 @@ void UserListTWI::setOnline(bool online) */ bool UserListTWI::operator<(const QTreeWidgetItem &other) const { - // QCollator caches the locale data, unlike QString::localeAwareCompare, - // which rebuilds it for every pair. The tree sorts one list at a time, so a - // single shared, immutable collator is enough. - static const QCollator collator = NaturalSort::makeNaturalSortCollator(); - // Sort by online/offline if (data(0, UserListRoles::Online) != other.data(0, UserListRoles::Online)) { return data(0, UserListRoles::Online).toBool(); @@ -294,7 +288,8 @@ bool UserListTWI::operator<(const QTreeWidgetItem &other) const for (const auto &userLevelEntry : userLevelWithNoOtherPrefOrder) { if (lhsUserLevelFlags.testFlag(userLevelEntry) && lhsUserLevelFlags.testFlag(userLevelEntry) == rhsUserLevelFlags.testFlag(userLevelEntry)) { - return collator.compare(data(2, Qt::UserRole).toString(), other.data(2, Qt::UserRole).toString()) < 0; + return QString::localeAwareCompare(data(2, Qt::UserRole).toString(), + other.data(2, Qt::UserRole).toString()) < 0; } else if (lhsUserLevelFlags.testFlag(userLevelEntry) != rhsUserLevelFlags.testFlag(userLevelEntry)) { return lhsUserLevelFlags.testFlag(userLevelEntry) > rhsUserLevelFlags.testFlag(userLevelEntry); } @@ -319,7 +314,7 @@ bool UserListTWI::operator<(const QTreeWidgetItem &other) const } // Sort by name - return collator.compare(data(2, Qt::UserRole).toString(), other.data(2, Qt::UserRole).toString()) < 0; + return QString::localeAwareCompare(data(2, Qt::UserRole).toString(), other.data(2, Qt::UserRole).toString()) < 0; } UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor, @@ -900,7 +895,11 @@ void UserListWidget::showPopupForUser(UserListTWI *item) return; } - QtUtils::fadeWindow(userInfoPopup, 0.0, 1.0, 120); + auto *fade = new QPropertyAnimation(userInfoPopup, "windowOpacity", userInfoPopup); + fade->setDuration(120); + fade->setStartValue(0.0); + fade->setEndValue(1.0); + fade->start(QAbstractAnimation::DeleteWhenStopped); } void UserListWidget::positionPopup(UserListTWI *item) @@ -977,7 +976,12 @@ void UserListWidget::hidePopup(bool immediate) } // Fade out - QtUtils::fadeWindow(userInfoPopup, userInfoPopup->windowOpacity(), 0.0, 100, true); + auto *fade = new QPropertyAnimation(userInfoPopup, "windowOpacity", userInfoPopup); + fade->setDuration(100); + fade->setStartValue(userInfoPopup->windowOpacity()); + fade->setEndValue(0.0); + connect(fade, &QPropertyAnimation::finished, userInfoPopup, &QWidget::hide); + fade->start(QAbstractAnimation::DeleteWhenStopped); } bool UserListWidget::isPressInsideListUi(const QWidget *widget) const diff --git a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp index de5f67b32..be2d5735e 100644 --- a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -29,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -37,7 +39,6 @@ #include #include #include -#include #include AppearanceSettingsPage::AppearanceSettingsPage() @@ -445,7 +446,7 @@ void AppearanceSettingsPage::openThemeLocation() dirDir.cdUp(); // open if dir exists, create if parent dir does exist if (dirDir.exists() && dirDir.mkpath(dir)) { - QtUtils::openPathInFileManager(dir); + QDesktopServices::openUrl(QUrl::fromLocalFile(dir)); } else { QMessageBox::critical(this, tr("Error"), tr("Could not create themes directory at '%1'.").arg(dir)); } 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..a19417661 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 @@ -33,7 +35,6 @@ #include #include #include -#include #include #include #include @@ -49,24 +50,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 +95,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 +383,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()); @@ -540,9 +547,12 @@ void GeneralSettingsPage::cardLanguageBoxChanged(int index) } message += tr("

Run Oracle now?

"); + const QMessageBox::StandardButton answer = QMessageBox::question( + this, tr("Card text & images language changed"), message, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); + // The answer only controls whether Oracle starts right away; the caches stay // cleared so the next import or launch rebuilds them in the new language. - if (QtUtils::confirm(this, tr("Card text & images language changed"), message, QMessageBox::Yes)) { + if (answer == QMessageBox::Yes) { emit cardDatabaseUpdateRequested(); } } @@ -568,7 +578,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/settings_page/shortcut_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/shortcut_settings_page.cpp index 83c2ec585..30a539be9 100644 --- a/cockatrice/src/interface/widgets/settings_page/shortcut_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/shortcut_settings_page.cpp @@ -18,7 +18,6 @@ #include #include #include -#include #include ShortcutSettingsPage::ShortcutSettingsPage() @@ -113,15 +112,16 @@ void ShortcutSettingsPage::currentItemChanged(const QString &key) void ShortcutSettingsPage::resetShortcuts() { - if (QtUtils::confirm(this, tr("Restore all default shortcuts"), - tr("Do you really want to restore all default shortcuts?"))) { + if (QMessageBox::question(this, tr("Restore all default shortcuts"), + tr("Do you really want to restore all default shortcuts?")) == QMessageBox::Yes) { SettingsCache::instance().shortcuts().resetAllShortcuts(); } } void ShortcutSettingsPage::clearShortcuts() { - if (QtUtils::confirm(this, tr("Clear all default shortcuts"), tr("Do you really want to clear all shortcuts?"))) { + if (QMessageBox::question(this, tr("Clear all default shortcuts"), + tr("Do you really want to clear all shortcuts?")) == QMessageBox::Yes) { SettingsCache::instance().shortcuts().clearAllShortcuts(); } } diff --git a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp index 44ce01aba..0a5f57e1f 100644 --- a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp @@ -18,7 +18,6 @@ #include #include #include -#include #include enum visualDeckStoragePromptForConversionIndex @@ -249,7 +248,8 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() updateCommanderSpellbookUiState(); - auto *labelLayout = QtUtils::makeLayout(); + auto *labelLayout = new QHBoxLayout; + labelLayout->setContentsMargins(0, 0, 0, 0); labelLayout->addWidget(&commanderSpellbookIntegrationUseOfficialBracketNamesLabel); labelLayout->addWidget(&commanderSpellbookIntegrationUseOfficialBracketNamesExplainer); 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..555ec7ec5 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,9 +30,7 @@ #include #include #include -#include #include -#include #include #include @@ -67,7 +66,10 @@ ArchidektApiResponseDeckDisplayWidget::ArchidektApiResponseDeckDisplayWidget(QWi connect(displayOptionsWidget, &VisualDeckDisplayOptionsWidget::groupCriteriaChanged, this, &ArchidektApiResponseDeckDisplayWidget::onGroupCriteriaChange); - scrollArea = QtUtils::makeScrollArea(this); + scrollArea = new QScrollArea(this); + scrollArea->setWidgetResizable(true); + scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); layout->addWidget(scrollArea); @@ -238,5 +240,7 @@ void ArchidektApiResponseDeckDisplayWidget::constructZoneWidgetsFromDeckListMode void ArchidektApiResponseDeckDisplayWidget::resizeEvent(QResizeEvent *event) { QWidget::resizeEvent(event); - QtUtils::relayout(layout); + layout->invalidate(); + layout->activate(); + layout->update(); } diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.cpp index b22dac8de..b5e28fcf5 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.cpp @@ -24,7 +24,6 @@ #include #include #include -#include #include #include @@ -242,7 +241,9 @@ void ArchidektApiResponseDeckEntryDisplayWidget::updateScaledPreview() void ArchidektApiResponseDeckEntryDisplayWidget::resizeEvent(QResizeEvent *event) { QWidget::resizeEvent(event); - QtUtils::relayout(layout); + layout->invalidate(); + layout->activate(); + layout->update(); } void ArchidektApiResponseDeckEntryDisplayWidget::actRequestNavigationToDeck() diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_listings_display_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_listings_display_widget.cpp index f8bf3c0dc..9c4ea59ab 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_listings_display_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_listings_display_widget.cpp @@ -11,8 +11,6 @@ #include #include #include -#include -#include #include ArchidektApiResponseDeckListingsDisplayWidget::ArchidektApiResponseDeckListingsDisplayWidget( @@ -21,14 +19,16 @@ ArchidektApiResponseDeckListingsDisplayWidget::ArchidektApiResponseDeckListingsD CardSizeWidget *_cardSizeSlider) : QWidget(parent), cardSizeSlider(_cardSizeSlider) { - layout = QtUtils::makeLayout(this); + layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); setLayout(layout); flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); cardSizeSlider->enableCtrlScrollResize(flowWidget); - imageNetworkManager = NetworkManagerFactory::create(this); + imageNetworkManager = new QNetworkAccessManager(this); + imageNetworkManager->setTransferTimeout(); // Use Qt's default timeout imageNetworkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy); // Add widgets for deck listings @@ -64,5 +64,7 @@ void ArchidektApiResponseDeckListingsDisplayWidget::append(const ArchidektDeckLi void ArchidektApiResponseDeckListingsDisplayWidget::resizeEvent(QResizeEvent *event) { QWidget::resizeEvent(event); - QtUtils::relayout(layout); + layout->invalidate(); + layout->activate(); + layout->update(); } diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp index 5697308a9..fd61dd098 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp @@ -48,9 +48,7 @@ #include #include #include -#include #include -#include #include #include #include @@ -59,7 +57,8 @@ TabArchidekt::TabArchidekt(TabSupervisor *_tabSupervisor) : Tab(_tabSupervisor), currentPage(1), isLoadingMore(false), isListMode(true) { // Initialize network - networkManager = NetworkManagerFactory::create(this); + networkManager = new QNetworkAccessManager(this); + networkManager->setTransferTimeout(); networkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy); connect(networkManager, &QNetworkAccessManager::finished, this, &TabArchidekt::processApiJson); @@ -81,7 +80,9 @@ void TabArchidekt::initializeUi() { // Main container container = new QWidget(this); - mainLayout = QtUtils::makeLayout(container, 0); + mainLayout = new QVBoxLayout(container); + mainLayout->setContentsMargins(0, 0, 0, 0); + mainLayout->setSpacing(0); // Primary toolbar (most important filters) primaryToolbar = new QWidget(container); @@ -104,7 +105,9 @@ void TabArchidekt::initializeUi() // Color filter (inline) QWidget *colorWidget = new QWidget(primaryToolbar); - QHBoxLayout *colorLayout = QtUtils::makeLayout(colorWidget, 2); + QHBoxLayout *colorLayout = new QHBoxLayout(colorWidget); + colorLayout->setContentsMargins(0, 0, 0, 0); + colorLayout->setSpacing(2); QString colorIdentity = "WUBRG"; for (const QChar &color : colorIdentity) { @@ -183,10 +186,15 @@ void TabArchidekt::initializeUi() secondaryToolbarLayout->setSpacing(6); // Scrollable results area - scrollArea = QtUtils::makeScrollArea(container); + scrollArea = new QScrollArea(container); + scrollArea->setWidgetResizable(true); + scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); resultsContainer = new QWidget(); - resultsLayout = QtUtils::makeLayout(resultsContainer, 0); + resultsLayout = new QVBoxLayout(resultsContainer); + resultsLayout->setContentsMargins(0, 0, 0, 0); + resultsLayout->setSpacing(0); scrollArea->setWidget(resultsContainer); @@ -220,9 +228,9 @@ void TabArchidekt::setupFilterWidgets() // Advanced filters (in secondary toolbar) // EDH Bracket - bracketLabel = new QLabel(secondaryToolbar); + auto *bracketLabel = new QLabel(tr("Bracket:"), secondaryToolbar); edhBracketCombo = new QComboBox(secondaryToolbar); - edhBracketCombo->addItem(QString()); + edhBracketCombo->addItem(tr("Any")); edhBracketCombo->addItems({"1", "2", "3", "4", "5"}); edhBracketCombo->setSizeAdjustPolicy(QComboBox::AdjustToContents); @@ -276,6 +284,7 @@ void TabArchidekt::setupFilterWidgets() sizeLayout->setContentsMargins(4, 4, 4, 4); minDeckSizeSpin = new QSpinBox(sizeContainer); + minDeckSizeSpin->setSpecialValueText(tr("Any")); minDeckSizeSpin->setRange(0, 200); minDeckSizeSpin->setValue(0); @@ -283,8 +292,7 @@ void TabArchidekt::setupFilterWidgets() minDeckSizeLogicCombo->addItems({"Exact", "≥", "≤"}); minDeckSizeLogicCombo->setCurrentIndex(1); - cardsLabel = new QLabel(sizeContainer); - sizeLayout->addWidget(cardsLabel); + sizeLayout->addWidget(new QLabel(tr("Cards:"), sizeContainer)); sizeLayout->addWidget(minDeckSizeSpin); sizeLayout->addWidget(minDeckSizeLogicCombo); @@ -341,9 +349,9 @@ void TabArchidekt::connectSignals() }); // Format filters trigger search - connect(edhBracketCombo, &QComboBox::currentIndexChanged, this, &TabArchidekt::doSearch); + connect(edhBracketCombo, &QComboBox::currentTextChanged, this, &TabArchidekt::doSearch); connect(minDeckSizeSpin, qOverload(&QSpinBox::valueChanged), this, &TabArchidekt::doSearch); - connect(minDeckSizeLogicCombo, &QComboBox::currentIndexChanged, this, &TabArchidekt::doSearch); + connect(minDeckSizeLogicCombo, &QComboBox::currentTextChanged, this, &TabArchidekt::doSearch); // Allow Enter key in text fields to trigger search connect(nameField, &QLineEdit::returnPressed, this, &TabArchidekt::doSearchImmediate); @@ -393,15 +401,8 @@ void TabArchidekt::retranslateUi() commandersField->setPlaceholderText(tr("Commander...")); deckTagNameField->setPlaceholderText(tr("Tag...")); - bracketLabel->setText(tr("Bracket:")); - // Index 0 is the "Any" placeholder added in setupFilterWidgets() - edhBracketCombo->setItemText(0, tr("Any")); - formatButton->setButtonText(tr("Formats")); deckSizeButton->setButtonText(tr("Deck Size")); - cardsLabel->setText(tr("Cards:")); - minDeckSizeSpin->setSpecialValueText(tr("Any")); - minDeckSizeLogicCombo->setItemText(0, tr("Exact")); searchButton->setText(tr("Search")); @@ -491,17 +492,13 @@ QString TabArchidekt::buildSearchUrl() query.addQueryItem("size", QString::number(minDeckSizeSpin->value())); QString logic = "GTE"; - // Index order matches setupFilterWidgets(): 0 = Exact, 1 = ≥, 2 = ≤ - switch (minDeckSizeLogicCombo->currentIndex()) { - case 1: - logic = "GTE"; - break; - case 2: - logic = "LTE"; - break; - default: - logic = ""; - break; + QString selected = minDeckSizeLogicCombo->currentText(); + if (selected == "≥") { + logic = "GTE"; + } else if (selected == "≤") { + logic = "LTE"; + } else { + logic = ""; } if (!logic.isEmpty()) { @@ -602,7 +599,11 @@ void TabArchidekt::processTopDecksResponse(QJsonObject reply) // New search → clear everything if (currentPage == 1) { - QtUtils::clearLayoutRec(resultsLayout, QtUtils::DeletePolicy::Immediate); + QLayoutItem *item; + while ((item = resultsLayout->takeAt(0)) != nullptr) { + delete item->widget(); + delete item; + } listingsWidget = new ArchidektApiResponseDeckListingsDisplayWidget(resultsContainer, deckData, cardSizeSlider); @@ -628,7 +629,11 @@ void TabArchidekt::processDeckResponse(QJsonObject reply) isListMode = false; // Clear existing results for single deck view - QtUtils::clearLayoutRec(resultsLayout, QtUtils::DeletePolicy::Immediate); + QLayoutItem *item; + while ((item = resultsLayout->takeAt(0)) != nullptr) { + delete item->widget(); + delete item; + } auto display = new ArchidektApiResponseDeckDisplayWidget(resultsContainer, deckData, cardSizeSlider); connect(display, &ArchidektApiResponseDeckDisplayWidget::requestNavigation, this, &TabArchidekt::actNavigatePage); diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.h b/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.h index c74380b63..015b8590b 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.h +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.h @@ -287,11 +287,9 @@ private: SettingsButtonWidget *formatButton; ///< Collapsible button for format filters QVector formatChecks; ///< Individual checkboxes for each format - QLabel *bracketLabel; ///< Label for EDH bracket selection QComboBox *edhBracketCombo; ///< Dropdown for EDH bracket selection SettingsButtonWidget *deckSizeButton; ///< Collapsible button for deck size filter - QLabel *cardsLabel; ///< Label for deck size selection QSpinBox *minDeckSizeSpin; ///< Spinner to select minimum deck size QComboBox *minDeckSizeLogicCombo; ///< Combo box for size comparison logic @@ -302,4 +300,4 @@ private: CardInfoPtr cardToQuery; ///< Optional pre-selected card for initial filtering }; -#endif // COCKATRICE_TAB_ARCHIDEKT_H +#endif // COCKATRICE_TAB_ARCHIDEKT_H \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp index 7f1727131..6692736f0 100644 --- a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp @@ -21,7 +21,6 @@ #include #include #include -#include #include template class QSharedPointer; @@ -45,7 +44,8 @@ CommanderBracketWidget::CommanderBracketWidget(QWidget *parent) : QWidget(parent connect(bracketRefreshButton, &QToolButton::clicked, this, &CommanderBracketWidget::requestBracketEstimate); - auto *layout = QtUtils::makeLayout(this); + auto *layout = new QGridLayout(this); + layout->setContentsMargins(0, 0, 0, 0); layout->addWidget(bracketLabel, 0, 0); auto *bracketRow = new QHBoxLayout; @@ -110,7 +110,8 @@ bool CommanderBracketWidget::promptCommanderSpellbookIntegration() // Create label + explainer button auto *labelWidget = new QWidget(&dialog); - auto *labelLayout = QtUtils::makeLayout(labelWidget); + auto *labelLayout = new QHBoxLayout(labelWidget); + labelLayout->setContentsMargins(0, 0, 0, 0); auto *namingLabel = new QLabel(tr("Bracket naming:"), labelWidget); auto *explainerButton = new QToolButton(labelWidget); diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp index 407f53e99..22200ffa2 100644 --- a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp @@ -12,13 +12,12 @@ #include #include #include -#include #include static const QUrl COMMANDER_BRACKET_JSON_URL(QStringLiteral("https://cockatrice.github.io/commander-brackets.json")); HandleCommanderBrackets::HandleCommanderBrackets(QObject *parent) - : QObject(parent), nam(NetworkManagerFactory::create(this)), reply(nullptr) + : QObject(parent), nam(new QNetworkAccessManager(this)), reply(nullptr) { } diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_details_display_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_details_display_widget.cpp index b2ddace01..0728b3154 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_details_display_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_details_display_widget.cpp @@ -15,7 +15,6 @@ #include #include #include -#include #include EdhrecApiResponseCardDetailsDisplayWidget::EdhrecApiResponseCardDetailsDisplayWidget( @@ -48,7 +47,15 @@ EdhrecApiResponseCardDetailsDisplayWidget::EdhrecApiResponseCardDetailsDisplayWi layout->addWidget(backgroundPlateWidget); - auto *parentTab = QtUtils::findParentOfType(this); + QWidget *currentParent = parentWidget(); + TabEdhRecMain *parentTab = nullptr; + + while (currentParent) { + if ((parentTab = qobject_cast(currentParent))) { + break; + } + currentParent = currentParent->parentWidget(); + } if (parentTab) { cardPictureWidget->setScaleFactor(parentTab->getCardSizeSlider()->getSlider()->value()); diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_api_response_commander_details_display_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_api_response_commander_details_display_widget.cpp index 2ff22f2b3..3da050edd 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_api_response_commander_details_display_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_api_response_commander_details_display_widget.cpp @@ -14,7 +14,6 @@ #include #include #include -#include #include EdhrecCommanderResponseCommanderDetailsDisplayWidget::EdhrecCommanderResponseCommanderDetailsDisplayWidget( @@ -34,7 +33,15 @@ EdhrecCommanderResponseCommanderDetailsDisplayWidget::EdhrecCommanderResponseCom commanderPicture = new CardInfoPictureWidget(this); commanderPicture->setCard(CardDatabaseManager::query()->getCard({commanderDetails.getName()})); - auto *parentTab = QtUtils::findParentOfType(this); + QWidget *currentParent = parentWidget(); + TabEdhRecMain *parentTab = nullptr; + + while (currentParent) { + if ((parentTab = qobject_cast(currentParent))) { + break; + } + currentParent = currentParent->parentWidget(); + } if (parentTab) { connect(parentTab->getCardSizeSlider()->getSlider(), &QSlider::valueChanged, commanderPicture, diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_display_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_display_widget.cpp index 6dc987b76..111d91b91 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_display_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_display_widget.cpp @@ -21,7 +21,6 @@ #include #include #include -#include #include #include @@ -62,7 +61,10 @@ EdhrecCommanderApiResponseDisplayWidget::EdhrecCommanderApiResponseDisplayWidget } // Create a QScrollArea to hold the card display widgets - scrollArea = QtUtils::makeScrollArea(splitter); + scrollArea = new QScrollArea(splitter); + scrollArea->setWidgetResizable(true); + scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // Set the cardDisplayLayout inside the scroll area auto scrollWidget = new QWidget(scrollArea); @@ -107,7 +109,9 @@ void EdhrecCommanderApiResponseDisplayWidget::resizeEvent(QResizeEvent *event) { QWidget::resizeEvent(event); qDebug() << event->size(); - QtUtils::relayout(layout); + layout->invalidate(); + layout->activate(); + layout->update(); if (scrollArea && scrollArea->widget()) { scrollArea->widget()->resize(event->size()); } diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_navigation_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_navigation_widget.cpp index 220dcfbda..fd02c4261 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_navigation_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_navigation_widget.cpp @@ -7,7 +7,6 @@ #include #include #include -#include EdhrecCommanderApiResponseNavigationWidget::EdhrecCommanderApiResponseNavigationWidget( QWidget *parent, @@ -41,7 +40,15 @@ EdhrecCommanderApiResponseNavigationWidget::EdhrecCommanderApiResponseNavigation layout->addWidget(bracketNavigationWidget, 1, 0, 1, 2); layout->addWidget(budgetNavigationWidget, 2, 0, 1, 2); - auto *parentTab = QtUtils::findParentOfType(this); + QWidget *currentParent = parentWidget(); + TabEdhRecMain *parentTab = nullptr; + + while (currentParent) { + if ((parentTab = qobject_cast(currentParent))) { + break; + } + currentParent = currentParent->parentWidget(); + } if (parentTab) { connect(comboPushButton, &QPushButton::clicked, this, diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/top_cards/edhrec_top_cards_api_response_display_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/top_cards/edhrec_top_cards_api_response_display_widget.cpp index 680926ef4..6f32db102 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/top_cards/edhrec_top_cards_api_response_display_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/top_cards/edhrec_top_cards_api_response_display_widget.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #include EdhrecTopCardsApiResponseDisplayWidget::EdhrecTopCardsApiResponseDisplayWidget(QWidget *parent, @@ -30,7 +29,10 @@ EdhrecTopCardsApiResponseDisplayWidget::EdhrecTopCardsApiResponseDisplayWidget(Q } // Create a QScrollArea to hold the card display widgets - scrollArea = QtUtils::makeScrollArea(this); + scrollArea = new QScrollArea(this); + scrollArea->setWidgetResizable(true); + scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // Set the cardDisplayLayout inside the scroll area auto scrollWidget = new QWidget(scrollArea); @@ -43,7 +45,9 @@ EdhrecTopCardsApiResponseDisplayWidget::EdhrecTopCardsApiResponseDisplayWidget(Q void EdhrecTopCardsApiResponseDisplayWidget::resizeEvent(QResizeEvent *event) { QWidget::resizeEvent(event); - QtUtils::relayout(layout); + layout->invalidate(); + layout->activate(); + layout->update(); if (scrollArea && scrollArea->widget()) { scrollArea->widget()->resize(event->size()); } diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/top_commander/edhrec_top_commanders_api_response_display_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/top_commander/edhrec_top_commanders_api_response_display_widget.cpp index 105386597..4ab333fd4 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/top_commander/edhrec_top_commanders_api_response_display_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/top_commander/edhrec_top_commanders_api_response_display_widget.cpp @@ -12,7 +12,6 @@ #include #include #include -#include #include #include @@ -34,7 +33,10 @@ EdhrecTopCommandersApiResponseDisplayWidget::EdhrecTopCommandersApiResponseDispl } // Create a QScrollArea to hold the card display widgets - scrollArea = QtUtils::makeScrollArea(this); + scrollArea = new QScrollArea(this); + scrollArea->setWidgetResizable(true); + scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // Set the cardDisplayLayout inside the scroll area auto scrollWidget = new QWidget(scrollArea); @@ -48,7 +50,9 @@ void EdhrecTopCommandersApiResponseDisplayWidget::resizeEvent(QResizeEvent *even { QWidget::resizeEvent(event); qDebug() << event->size(); - QtUtils::relayout(layout); + layout->invalidate(); + layout->activate(); + layout->update(); if (scrollArea && scrollArea->widget()) { scrollArea->widget()->resize(event->size()); } diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/top_tags/edhrec_top_tags_api_response_display_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/top_tags/edhrec_top_tags_api_response_display_widget.cpp index ae8686f3a..9c7ce8d06 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/top_tags/edhrec_top_tags_api_response_display_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/top_tags/edhrec_top_tags_api_response_display_widget.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #include EdhrecTopTagsApiResponseDisplayWidget::EdhrecTopTagsApiResponseDisplayWidget(QWidget *parent, @@ -30,7 +29,10 @@ EdhrecTopTagsApiResponseDisplayWidget::EdhrecTopTagsApiResponseDisplayWidget(QWi } // Create a QScrollArea to hold the card display widgets - scrollArea = QtUtils::makeScrollArea(this); + scrollArea = new QScrollArea(this); + scrollArea->setWidgetResizable(true); + scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // Set the cardDisplayLayout inside the scroll area auto scrollWidget = new QWidget(scrollArea); @@ -43,7 +45,9 @@ EdhrecTopTagsApiResponseDisplayWidget::EdhrecTopTagsApiResponseDisplayWidget(QWi void EdhrecTopTagsApiResponseDisplayWidget::resizeEvent(QResizeEvent *event) { QWidget::resizeEvent(event); - QtUtils::relayout(layout); + layout->invalidate(); + layout->activate(); + layout->update(); if (scrollArea && scrollArea->widget()) { scrollArea->widget()->resize(event->size()); } diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/tab_edhrec.cpp b/cockatrice/src/interface/widgets/tabs/api/edhrec/tab_edhrec.cpp index 9dd0cca03..3aa395126 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/tab_edhrec.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/tab_edhrec.cpp @@ -18,14 +18,14 @@ #include #include #include -#include #include class TabSupervisor; TabEdhRec::TabEdhRec(TabSupervisor *_tabSupervisor) : Tab(_tabSupervisor) { - networkManager = NetworkManagerFactory::create(this); + networkManager = new QNetworkAccessManager(this); + networkManager->setTransferTimeout(); // Use Qt's default timeout networkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy); connect(networkManager, &QNetworkAccessManager::finished, this, &TabEdhRec::processApiJson); } 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..ca575dc02 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,12 +28,10 @@ #include #include #include -#include #include #include #include #include -#include #include #include @@ -45,7 +44,8 @@ static bool canBeCommander(const CardInfoPtr &cardInfo) TabEdhRecMain::TabEdhRecMain(TabSupervisor *_tabSupervisor) : Tab(_tabSupervisor) { - networkManager = NetworkManagerFactory::create(this); + networkManager = new QNetworkAccessManager(this); + networkManager->setTransferTimeout(); // Use Qt's default timeout networkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy); connect(networkManager, SIGNAL(finished(QNetworkReply *)), this, SLOT(processApiJson(QNetworkReply *))); diff --git a/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.cpp b/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.cpp index f18aa82b9..1fc2854ce 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.cpp @@ -18,7 +18,6 @@ #include #include #include -#include #include #include #include @@ -30,9 +29,7 @@ #include #include #include -#include #include -#include #include class QObject; @@ -175,7 +172,6 @@ void TabCardArtRules::setupUi() addBtn = new QPushButton; removeBtn = new QPushButton; refreshBtn = new QPushButton; - clearBannedArtBtn = new QPushButton; modeBox->addItems({"ALLOW", "DENY"}); @@ -192,14 +188,10 @@ void TabCardArtRules::setupUi() form->addRow(tr("Mode:"), modeBox); form->addRow(tr("Reason:"), reasonEdit); - // The bulk clear acts on every user, so keep it visibly apart from the - // rules this tab edits. auto *buttons = new QHBoxLayout; buttons->addWidget(addBtn); buttons->addWidget(removeBtn); buttons->addWidget(refreshBtn); - buttons->addStretch(); - buttons->addWidget(clearBannedArtBtn); auto *layout = new QVBoxLayout; layout->addLayout(form); @@ -215,8 +207,6 @@ void TabCardArtRules::setupUi() connect(refreshBtn, &QPushButton::clicked, this, &TabCardArtRules::refresh); - connect(clearBannedArtBtn, &QPushButton::clicked, this, &TabCardArtRules::clearBlacklistedBanners); - retranslateUi(); } @@ -273,7 +263,6 @@ void TabCardArtRules::retranslateUi() addBtn->setText(tr("Add rule")); removeBtn->setText(tr("Remove rule")); refreshBtn->setText(tr("Refresh")); - clearBannedArtBtn->setText(tr("Clear blacklisted banners")); } void TabCardArtRules::refresh() @@ -310,47 +299,4 @@ void TabCardArtRules::removeSelected() client->sendCommand(client->prepareModeratorCommand(cmd)); refresh(); -} - -void TabCardArtRules::clearBlacklistedBanners() -{ - if (!QtUtils::confirm(this, tr("Clear blacklisted banners"), - tr("Remove the banner card from every user whose current banner matches a DENY rule?\n\n" - "Users who are online keep showing their banner until they log in again."), - QMessageBox::No)) { - return; - } - - Command_ClearBlacklistedBanners cmd; - PendingCommand *pend = client->prepareModeratorCommand(cmd); - connect(pend, &PendingCommand::finished, this, &TabCardArtRules::clearBlacklistedBannersResponse); - client->sendCommand(pend); -} - -void TabCardArtRules::clearBlacklistedBannersResponse(const Response &r) -{ - switch (r.response_code()) { - case Response::RespOk: - break; - case Response::RespFunctionNotAllowed: - QMessageBox::warning(this, tr("Not supported"), - tr("This server does not support clearing blacklisted banners. It may be running an " - "older version.")); - return; - case Response::RespInternalError: - QMessageBox::critical(this, tr("Error"), - tr("The server could not clear blacklisted banners because of a database error.")); - return; - default: - QMessageBox::critical(this, tr("Error"), - tr("The server rejected the request: %1") - .arg(QString::fromStdString(Response_ResponseCode_Name(r.response_code())))); - return; - } - - const auto &resp = r.GetExtension(Response_ClearBlacklistedBanners::ext); - QMessageBox::information(this, tr("Banners cleared"), - tr("%n banner card(s) cleared.\n\n" - "Users who are online keep showing their banner until they log in again.", - "number of banners cleared", resp.cleared_count())); -} +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.h b/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.h index 1e0155ab7..f34300179 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.h +++ b/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.h @@ -78,8 +78,6 @@ private slots: void addRule(); void removeSelected(); void refresh(); - void clearBlacklistedBanners(); - void clearBlacklistedBannersResponse(const Response &r); private: AbstractClient *client; @@ -99,7 +97,6 @@ private: QPushButton *addBtn; QPushButton *removeBtn; QPushButton *refreshBtn; - QPushButton *clearBannedArtBtn; QTableView *table; CardArtRulesModel *tableModel; diff --git a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp index 8af9d5e6e..498ec09b3 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 "../../theme_icon.h" #include "../cards/additional_info/deck_color_identity.h" #include "../deck_share/deck_share_utils.h" @@ -18,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -42,14 +45,13 @@ #include #include #include +#include #include #include #include #include #include -#include #include -#include #include #include #include @@ -63,7 +65,6 @@ #include #include #include -#include #include #include #include @@ -523,7 +524,7 @@ void TabDeckStorage::actDeleteLocalDeck() void TabDeckStorage::actOpenDecksFolder() { QString dir = localDirModel->rootPath(); - QtUtils::openPathInFileManager(dir); + QDesktopServices::openUrl(QUrl::fromLocalFile(dir)); } void TabDeckStorage::actRemoteDoubleClick(const QModelIndex &curRight) diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.cpp b/cockatrice/src/interface/widgets/tabs/tab_game.cpp index 9e724727f..4cf5919ba 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 @@ -81,7 +78,6 @@ #include #include #include -#include #include #include #include @@ -100,8 +96,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 +140,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(); @@ -611,14 +605,15 @@ void TabGame::actConcede() return; } if (!player->getConceded()) { - if (!QtUtils::confirm(this, tr("Concede"), tr("Are you sure you want to concede this game?"), - QMessageBox::No)) { + if (QMessageBox::question(this, tr("Concede"), tr("Are you sure you want to concede this game?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) { return; } emit game->getPlayerManager()->activeLocalPlayerConceded(); } else { - if (!QtUtils::confirm(this, tr("Unconcede"), - tr("You have already conceded. Do you want to return to this game?"), QMessageBox::No)) { + if (QMessageBox::question(this, tr("Unconcede"), + tr("You have already conceded. Do you want to return to this game?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) { return; } emit game->getPlayerManager()->activeLocalPlayerUnconceded(); @@ -635,8 +630,8 @@ bool TabGame::leaveGame() if (!game->getGameState()->isGameClosed()) { if (!game->getPlayerManager()->isSpectator()) { tabSupervisor->setCurrentWidget(this); - if (!QtUtils::confirm(this, tr("Leave game"), tr("Are you sure you want to leave this game?"), - QMessageBox::No)) { + if (QMessageBox::question(this, tr("Leave game"), tr("Are you sure you want to leave this game?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) { return false; } } @@ -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); @@ -1298,7 +1287,8 @@ void TabGame::createPlayAreaWidget(bool bReplay) connect(game->getGameEventHandler(), &GameEventHandler::arrowDeleted, scene, &GameScene::deleteArrow); gameView = new GameView(scene); - auto gamePlayAreaVBox = QtUtils::makeLayout(); + auto gamePlayAreaVBox = new QVBoxLayout; + gamePlayAreaVBox->setContentsMargins(0, 0, 0, 0); gamePlayAreaVBox->addWidget(gameView); gamePlayAreaWidget = new QWidget; @@ -1330,7 +1320,8 @@ void TabGame::createDeckViewContainerWidget(bool bReplay) deckViewContainerWidget = new QWidget(); deckViewContainerWidget->setObjectName("deckViewContainerWidget"); - deckViewContainerLayout = QtUtils::makeLayout(); + deckViewContainerLayout = new QVBoxLayout; + deckViewContainerLayout->setContentsMargins(0, 0, 0, 0); deckViewContainerWidget->setLayout(deckViewContainerLayout); } @@ -1345,7 +1336,8 @@ void TabGame::createCardInfoDock(bool bReplay) cardInfoFrameWidget = new CardInfoFrameWidget(); auto cardHInfoLayout = new QHBoxLayout; - auto cardVInfoLayout = QtUtils::makeLayout(); + auto cardVInfoLayout = new QVBoxLayout; + cardVInfoLayout->setContentsMargins(0, 0, 0, 0); cardVInfoLayout->addWidget(cardInfoFrameWidget); cardVInfoLayout->addLayout(cardHInfoLayout); @@ -1372,7 +1364,9 @@ void TabGame::createPlayerListDock(bool bReplay) playerListWidget->setFocusPolicy(Qt::NoFocus); auto *playerListBox = new QWidget(this); - auto *vbox = QtUtils::makeLayout(playerListBox, 0); + auto *vbox = new QVBoxLayout(playerListBox); + vbox->setContentsMargins(0, 0, 0, 0); + vbox->setSpacing(0); vbox->addWidget(playerListWidget); @@ -1397,7 +1391,8 @@ void TabGame::createPlayerListDock(bool bReplay) void TabGame::createMessageDock(bool bReplay) { - auto messageLogLayout = QtUtils::makeLayout(); + auto messageLogLayout = new QVBoxLayout; + messageLogLayout->setContentsMargins(0, 0, 0, 0); // clock if (!bReplay) { diff --git a/cockatrice/src/interface/widgets/tabs/tab_home.cpp b/cockatrice/src/interface/widgets/tabs/tab_home.cpp index af9994fdd..1358645d0 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_home.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_home.cpp @@ -12,5 +12,4 @@ TabHome::TabHome(TabSupervisor *_tabSupervisor, AbstractClient *_client) : Tab(_ void TabHome::retranslateUi() { - homeWidget->retranslateUi(); } diff --git a/cockatrice/src/interface/widgets/tabs/tab_logs.cpp b/cockatrice/src/interface/widgets/tabs/tab_logs.cpp index 9509f48ec..67e076983 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_logs.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_logs.cpp @@ -13,7 +13,6 @@ #include #include #include -#include #include #include #include @@ -22,7 +21,6 @@ #include #include #include -#include #include #include #include @@ -33,17 +31,6 @@ class TabSupervisor; -namespace -{ -/** @brief Identifies the newest row of a page well enough to recognise a repeated page. */ -QString pageStartKey(const Response_ViewLogHistory &response) -{ - const ServerInfo_ChatMessage &first = response.log_message(0); - return QString::fromStdString(first.time()) + QChar(0x1f) + QString::fromStdString(first.sender_name()) + - QChar(0x1f) + QString::fromStdString(first.message()); -} -} // namespace - TabLog::TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client, bool _canUseDeveloperCommands) : Tab(_tabSupervisor), client(_client), canUseDeveloperCommands(_canUseDeveloperCommands) { @@ -65,20 +52,12 @@ TabLog::TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client, bool _can chatTable->setHorizontalHeaderLabels( QString(tr("Time;SenderName;SenderIP;Message;TargetID;TargetName")).split(";")); - tabManager = new QTabWidget(); + QTabWidget *tabManager = new QTabWidget(); tabManager->addTab(roomTable, tr("Room Logs")); tabManager->addTab(gameTable, tr("Game Logs")); tabManager->addTab(chatTable, tr("Chat Logs")); setCentralWidget(tabManager); - connect(roomTable->verticalScrollBar(), &QScrollBar::valueChanged, this, &TabLog::checkFetchMore); - connect(gameTable->verticalScrollBar(), &QScrollBar::valueChanged, this, &TabLog::checkFetchMore); - connect(chatTable->verticalScrollBar(), &QScrollBar::valueChanged, this, &TabLog::checkFetchMore); - // A tab that gains only a few rows has nothing to scroll, so its scrollbar never fires: - // switching to it would leave its next page unfetched until the moderator scrolled - // another tab. Re-check on every tab switch so the visible table can continue the browse. - connect(tabManager, &QTabWidget::currentChanged, this, &TabLog::checkFetchMore); - createDock(); restartLayout(); clearClicked(); @@ -95,9 +74,12 @@ void TabLog::retranslateUi() void TabLog::getClicked() { - // Empty filters are a valid browse: room/game history going back from - // today, paginated. Only require a date range + location, both of which - // get sensible defaults below so a bare "Get" just works. + if (findUsername->text().isEmpty() && findIPAddress->text().isEmpty() && findGameName->text().isEmpty() && + findGameID->text().isEmpty() && findMessage->text().isEmpty()) { + QMessageBox::critical(this, tr("Error"), tr("You must select at least one filter.")); + return; + } + if (!lastHour->isChecked() && !today->isChecked() && !pastDays->isChecked()) { pastDays->setChecked(true); pastXDays->setValue(20); @@ -117,71 +99,39 @@ void TabLog::getClicked() } if (maximumResults->value() == 0) { - maximumResults->setValue(200); + maximumResults->setValue(1000); } - pageSize = qBound(1, maximumResults->value(), 1000); - // Build the command once, here, and let every page copy it: rebuilding it from the - // widgets per page would run page 2+ against filters edited after Get was clicked, - // appending rows selected by a different query under the first page's results. - currentQuery.Clear(); - currentQuery.set_user_name(findUsername->text().toStdString()); - currentQuery.set_ip_address(findIPAddress->text().toStdString()); - currentQuery.set_game_name(findGameName->text().toStdString()); - currentQuery.set_game_id(findGameID->text().toStdString()); - currentQuery.set_message(findMessage->text().toStdString()); - if (mainRoom->isChecked()) { - currentQuery.add_log_location("room"); - } - if (gameRoom->isChecked()) { - currentQuery.add_log_location("game"); - } - if (privateChat->isChecked()) { - currentQuery.add_log_location("chat"); - } - currentQuery.set_date_range(dateRangeForSelection()); - currentQuery.set_maximum_results(pageSize); - hasCurrentQuery = true; - snapshotBefore.clear(); - lastPageStartKey.clear(); - - // Fresh search: reset pagination state and clear previous pages. - currentOffset = 0; - roomCounter = 0; - gameCounter = 0; - chatCounter = 0; - roomTable->setRowCount(0); - gameTable->setRowCount(0); - chatTable->setRowCount(0); - - fetchPage(0); -} - -int TabLog::dateRangeForSelection() const -{ + int dateRange = 0; if (lastHour->isChecked()) { - return 1; + dateRange = 1; } + if (today->isChecked()) { - return 24; + dateRange = 24; } + if (pastDays->isChecked()) { - return pastXDays->value() * 24; - } - return 0; -} - -void TabLog::fetchPage(int offset) -{ - if (isFetching || !hasCurrentQuery) { - return; + dateRange = pastXDays->value() * 24; } - Command_ViewLogHistory cmd = currentQuery; - cmd.set_offset(offset); - if (!snapshotBefore.isEmpty()) { - cmd.set_before_time(snapshotBefore.toStdString()); - } + Command_ViewLogHistory cmd; + cmd.set_user_name(findUsername->text().toStdString()); + cmd.set_ip_address(findIPAddress->text().toStdString()); + cmd.set_game_name(findGameName->text().toStdString()); + cmd.set_game_id(findGameID->text().toStdString()); + cmd.set_message(findMessage->text().toStdString()); + if (mainRoom->isChecked()) { + cmd.add_log_location("room"); + }; + if (gameRoom->isChecked()) { + cmd.add_log_location("game"); + }; + if (privateChat->isChecked()) { + cmd.add_log_location("chat"); + }; + cmd.set_date_range(dateRange); + cmd.set_maximum_results(maximumResults->value()); PendingCommand *pend; if (canUseDeveloperCommands) { @@ -191,42 +141,10 @@ void TabLog::fetchPage(int offset) pend = client->prepareModeratorCommand(cmd); } - isFetching = true; - getButton->setEnabled(false); - connect(pend, &PendingCommand::finished, this, &TabLog::viewLogHistory_processResponse); client->sendCommand(pend); } -void TabLog::checkFetchMore() -{ - if (isFetching || !hasMoreResults) { - return; - } - - // Only the table the moderator is looking at may pull the next page. Checking all - // three would make an empty or hidden table nearBottom by definition (maximum() and - // value() are both zero), so a room-only browse chained page after page without any - // scrolling - pulling the whole range with a growing OFFSET scan per page, and - // looping forever against a server that ignores `offset`. - auto *table = qobject_cast(tabManager->currentWidget()); - if (table == nullptr || table->rowCount() == 0) { - return; - } - - // ScrollPerItem (the default) counts rows, not pixels: `value` is the top visible row - // and `maximum` the last one that can be scrolled to. Prefetch half a page ahead so - // scrolling never visibly stalls, without the fixed 300-row threshold firing the next - // page the moment a page loads. - const QScrollBar *bar = table->verticalScrollBar(); - const int prefetchRows = qMax(1, pageSize / 2); - if (bar->maximum() - bar->value() > prefetchRows) { - return; - } - - fetchPage(currentOffset); -} - void TabLog::clearClicked() { findUsername->clear(); @@ -248,10 +166,6 @@ void TabLog::clearClicked() pastDays->setAutoExclusive(true); today->setAutoExclusive(true); lastHour->setAutoExclusive(true); - currentOffset = 0; - hasMoreResults = false; - hasCurrentQuery = false; - snapshotBefore.clear(); } void TabLog::createDock() @@ -295,15 +209,14 @@ void TabLog::createDock() pastXDays = new QSpinBox; pastXDays->setMaximum(20); - labelMaximum = new QLabel(tr("Results per page: ")); + labelMaximum = new QLabel(tr("Maximum Results: ")); maximumResults = new QSpinBox; maximumResults->setMaximum(1000); - maximumResults->setToolTip(tr("Page size for smooth scrolling: more rows load automatically as you scroll.")); - labelDescription = new QLabel(tr("Leave filters empty to browse recent room and game messages.\n" - "More rows load automatically as you scroll.")); + labelDescription = new QLabel(tr( + "At least one filter is required.\nThe more information you put in, the more specific your results will be.")); - getButton = new QPushButton(tr("Get Logs")); + getButton = new QPushButton(tr("Get User Logs")); getButton->setAutoDefault(true); connect(getButton, &QPushButton::clicked, this, &TabLog::getClicked); @@ -349,7 +262,7 @@ void TabLog::createDock() maxResultsGrid->addWidget(labelMaximum, 0, 0); maxResultsGrid->addWidget(maximumResults, 0, 1); - maxResultsGroupBox = new QGroupBox(tr("Results per page")); + maxResultsGroupBox = new QGroupBox(tr("Maximum Results")); maxResultsGroupBox->setLayout(maxResultsGrid); descriptionGrid = new QGridLayout; @@ -382,134 +295,84 @@ void TabLog::createDock() searchDock->setWidget(searchDockContents); } -void TabLog::appendChatMessage(QTableWidget *table, - int &counter, - const QString &time, - const QString &senderName, - const QString &senderIp, - const QString &messageText, - const QString &targetId, - const QString &targetName, - const QString &channel) -{ - table->insertRow(counter); - table->setItem(counter, 0, new QTableWidgetItem(time)); - table->setItem(counter, 1, new QTableWidgetItem(senderName)); - table->setItem(counter, 2, new QTableWidgetItem(senderIp)); - table->setItem(counter, 3, new QTableWidgetItem(messageText)); - table->setItem(counter, 4, new QTableWidgetItem(targetId)); - table->setItem(counter, 5, new QTableWidgetItem(targetName)); - if (table->columnCount() > 6) { - table->setItem(counter, 6, new QTableWidgetItem(channel)); - } - ++counter; -} - void TabLog::viewLogHistory_processResponse(const Response &resp) { const Response_ViewLogHistory &response = resp.GetExtension(Response_ViewLogHistory::ext); - const bool isFirstPage = (currentOffset == 0); - isFetching = false; - getButton->setEnabled(true); - if (resp.response_code() != Response::RespOk) { QMessageBox::critical(static_cast(parent()), tr("Message History"), tr("Failed to collect message history information.")); return; } - if (isFirstPage && response.log_message_size() == 0) { + if (response.log_message_size() == 0) { QMessageBox::information(static_cast(parent()), tr("Message History"), tr("There are no messages for the selected filters.")); - hasMoreResults = false; return; } - const QString thisPageStart = response.log_message_size() > 0 ? pageStartKey(response) : QString(); - - if (isFirstPage) { - // Rows come newest first, so the first one fixes the snapshot every later page is - // bounded by. Without it, each message logged between two page fetches pushes the - // window down by one and repeats a row the moderator already has. - snapshotBefore = QString::fromStdString(response.log_message(0).time()); - } else if (!response.has_has_more() && !thisPageStart.isEmpty() && thisPageStart == lastPageStartKey) { - // A server that predates `offset` ignores it and returns the same page again. It - // also predates `has_more`, so the full-page heuristic below can never prove - // exhaustion and every scroll would append the same rows forever. The identical - // first row is the proof; stop instead of looping. - hasMoreResults = false; - return; - } - lastPageStartKey = thisPageStart; + int roomCounter = 0, gameCounter = 0, chatCounter = 0; + roomTable->setRowCount(roomCounter); + gameTable->setRowCount(gameCounter); + chatTable->setRowCount(chatCounter); for (int i = 0; i < response.log_message_size(); ++i) { ServerInfo_ChatMessage message = response.log_message(i); const QString targetType = QString::fromStdString(message.target_type()); if (targetType == "room" || targetType == "room_channel") { - appendChatMessage(roomTable, roomCounter, QString::fromStdString(message.time()), - QString::fromStdString(message.sender_name()), - QString::fromStdString(message.sender_ip()), QString::fromStdString(message.message()), - QString::fromStdString(message.target_id()), - QString::fromStdString(message.target_name()), QString::fromStdString(message.channel())); + roomTable->insertRow(roomCounter); + roomTable->setItem(roomCounter, 0, new QTableWidgetItem(QString::fromStdString(message.time()))); + roomTable->setItem(roomCounter, 1, new QTableWidgetItem(QString::fromStdString(message.sender_name()))); + roomTable->setItem(roomCounter, 2, new QTableWidgetItem(QString::fromStdString(message.sender_ip()))); + roomTable->setItem(roomCounter, 3, new QTableWidgetItem(QString::fromStdString(message.message()))); + roomTable->setItem(roomCounter, 4, new QTableWidgetItem(QString::fromStdString(message.target_id()))); + roomTable->setItem(roomCounter, 5, new QTableWidgetItem(QString::fromStdString(message.target_name()))); + roomTable->setItem(roomCounter, 6, new QTableWidgetItem(QString::fromStdString(message.channel()))); + ++roomCounter; } if (targetType == "game") { - appendChatMessage(gameTable, gameCounter, QString::fromStdString(message.time()), - QString::fromStdString(message.sender_name()), - QString::fromStdString(message.sender_ip()), QString::fromStdString(message.message()), - QString::fromStdString(message.target_id()), - QString::fromStdString(message.target_name())); + gameTable->insertRow(gameCounter); + gameTable->setItem(gameCounter, 0, new QTableWidgetItem(QString::fromStdString(message.time()))); + gameTable->setItem(gameCounter, 1, new QTableWidgetItem(QString::fromStdString(message.sender_name()))); + gameTable->setItem(gameCounter, 2, new QTableWidgetItem(QString::fromStdString(message.sender_ip()))); + gameTable->setItem(gameCounter, 3, new QTableWidgetItem(QString::fromStdString(message.message()))); + gameTable->setItem(gameCounter, 4, new QTableWidgetItem(QString::fromStdString(message.target_id()))); + gameTable->setItem(gameCounter, 5, new QTableWidgetItem(QString::fromStdString(message.target_name()))); + ++gameCounter; } if (targetType == "chat") { - appendChatMessage(chatTable, chatCounter, QString::fromStdString(message.time()), - QString::fromStdString(message.sender_name()), - QString::fromStdString(message.sender_ip()), QString::fromStdString(message.message()), - QString::fromStdString(message.target_id()), - QString::fromStdString(message.target_name())); + chatTable->insertRow(chatCounter); + chatTable->setItem(chatCounter, 0, new QTableWidgetItem(QString::fromStdString(message.time()))); + chatTable->setItem(chatCounter, 1, new QTableWidgetItem(QString::fromStdString(message.sender_name()))); + chatTable->setItem(chatCounter, 2, new QTableWidgetItem(QString::fromStdString(message.sender_ip()))); + chatTable->setItem(chatCounter, 3, new QTableWidgetItem(QString::fromStdString(message.message()))); + chatTable->setItem(chatCounter, 4, new QTableWidgetItem(QString::fromStdString(message.target_id()))); + chatTable->setItem(chatCounter, 5, new QTableWidgetItem(QString::fromStdString(message.target_name()))); + ++chatCounter; } } - currentOffset += response.log_message_size(); - if (response.has_has_more()) { - hasMoreResults = response.has_more(); - } else { - // Legacy server without has_more: a full page implies more may exist. - hasMoreResults = (response.log_message_size() >= pageSize); - } - if (roomCounter) { roomTable->show(); - if (isFirstPage) { - roomTable->resizeColumnsToContents(); - } + roomTable->resizeColumnsToContents(); } else { roomTable->hide(); } if (gameCounter) { - if (isFirstPage) { - gameTable->resizeColumnsToContents(); - } + gameTable->resizeColumnsToContents(); gameTable->show(); } else { gameTable->hide(); } if (chatCounter) { - if (isFirstPage) { - chatTable->resizeColumnsToContents(); - } + chatTable->resizeColumnsToContents(); chatTable->show(); } else { chatTable->hide(); } - - // The new rows may already fill less than a viewport (or the user is - // still parked at the bottom), so chain the next page without waiting - // for another scroll event. The prefetch stays invisible: rows simply - // keep appearing as you scroll. - checkFetchMore(); } void TabLog::restartLayout() diff --git a/cockatrice/src/interface/widgets/tabs/tab_logs.h b/cockatrice/src/interface/widgets/tabs/tab_logs.h index f2f5842c5..fc8ef1ea0 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_logs.h +++ b/cockatrice/src/interface/widgets/tabs/tab_logs.h @@ -10,7 +10,6 @@ #include "tab.h" #include -#include #include class AbstractClient; @@ -26,7 +25,6 @@ class QWidget; class QGridLayout; class QVBoxLayout; class QTableWidget; -class QTabWidget; class Response; class TabSupervisor; @@ -50,37 +48,8 @@ private: *buttonGroupBox; QVBoxLayout *mainLayout; QTableWidget *roomTable, *gameTable, *chatTable; - QTabWidget *tabManager = nullptr; - - //! The filters of the search in flight, captured when Get was clicked so that - //! later pages cannot pick up edits the moderator made after starting it. - Command_ViewLogHistory currentQuery; - bool hasCurrentQuery = false; - //! Newest `log_time` of the first page, sent back as the upper bound of every - //! later page so messages logged in the meantime do not shift the window. - QString snapshotBefore; - //! Identity of the previous page's first row, used to spot a server that ignores - //! `offset` and hands back the same page forever. - QString lastPageStartKey; - int currentOffset = 0; - int pageSize = 200; - int roomCounter = 0, gameCounter = 0, chatCounter = 0; - bool isFetching = false; - bool hasMoreResults = false; void createDock(); - [[nodiscard]] int dateRangeForSelection() const; - void fetchPage(int offset); - void appendChatMessage(QTableWidget *table, - int &counter, - const QString &time, - const QString &senderName, - const QString &senderIp, - const QString &messageText, - const QString &targetId, - const QString &targetName, - const QString &channel = QString()); - void checkFetchMore(); signals: private slots: diff --git a/cockatrice/src/interface/widgets/tabs/tab_moderation.cpp b/cockatrice/src/interface/widgets/tabs/tab_moderation.cpp index be1adeef8..09a640540 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_moderation.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_moderation.cpp @@ -30,14 +30,12 @@ #include #include #include -#include #include #include #include #include #include #include -#include #include #include @@ -87,16 +85,12 @@ TabModeration::TabModeration(TabSupervisor *_tabSupervisor, AbstractClient *_cli removeAvatarButton = new QPushButton; connect(removeAvatarButton, &QPushButton::clicked, this, &TabModeration::removeAvatar); - removeBannerButton = new QPushButton; - connect(removeBannerButton, &QPushButton::clicked, this, &TabModeration::removeBanner); - auto *topBar = new QHBoxLayout; topBar->addWidget(searchEdit); topBar->addWidget(investigateButton); topBar->addStretch(); topBar->addWidget(resetPasswordButton); topBar->addWidget(removeAvatarButton); - topBar->addWidget(removeBannerButton); userInfoGroup = new QGroupBox; userInfoNameLabel = new QLabel; @@ -192,7 +186,6 @@ void TabModeration::retranslateUi() investigateButton->setText(tr("Investigate")); resetPasswordButton->setText(tr("Reset Password")); removeAvatarButton->setText(tr("Remove Avatar")); - removeBannerButton->setText(tr("Remove Banner")); refreshStaffButton->setText(tr("Refresh")); userInfoGroup->setTitle(tr("User Info")); @@ -234,7 +227,6 @@ void TabModeration::clearUserData() sessionsTable->setRowCount(0); resetPasswordButton->setEnabled(false); removeAvatarButton->setEnabled(false); - removeBannerButton->setEnabled(false); } void TabModeration::investigate(const QString &userName) @@ -255,7 +247,6 @@ void TabModeration::investigateUser() currentUser = userName; resetPasswordButton->setEnabled(true); removeAvatarButton->setEnabled(true); - removeBannerButton->setEnabled(true); altsTable->setRowCount(0); sessionsTable->setRowCount(0); requestUserInfo(userName); @@ -423,11 +414,13 @@ void TabModeration::resetPassword() return; } - if (!QtUtils::confirm(this, tr("Reset Password"), - tr("Reset the password of %1? A temporary password will be generated and shown to you. " - "The user must change it on their first login.") - .arg(currentUser), - QMessageBox::No)) { + QMessageBox::StandardButton choice = + QMessageBox::warning(this, tr("Reset Password"), + tr("Reset the password of %1? A temporary password will be generated and shown to you. " + "The user must change it on their first login.") + .arg(currentUser), + QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Cancel); + if (choice != QMessageBox::Ok) { return; } @@ -458,9 +451,11 @@ void TabModeration::removeAvatar() return; } - if (!QtUtils::confirm(this, tr("Remove Avatar"), - tr("Remove the avatar of %1? The user will have to upload a new one.").arg(currentUser), - QMessageBox::No)) { + QMessageBox::StandardButton choice = + QMessageBox::warning(this, tr("Remove Avatar"), + tr("Remove the avatar of %1? The user will have to upload a new one.").arg(currentUser), + QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Cancel); + if (choice != QMessageBox::Ok) { return; } @@ -482,67 +477,3 @@ void TabModeration::removeAvatarResponse(const Response &response) QMessageBox::information(this, tr("Avatar removed"), tr("The avatar of %1 has been removed.").arg(QString::fromStdString(resp.user_name()))); } - -void TabModeration::removeBanner() -{ - if (currentUser.isEmpty()) { - return; - } - - if (!QtUtils::confirm(this, tr("Remove Banner"), - tr("Remove the banner card of %1? The user will have to choose a new one.\n\n" - "Other players stop seeing the banner the next time %1 logs in.") - .arg(currentUser), - QMessageBox::No)) { - return; - } - - Command_RemoveUserBanner cmd; - cmd.set_user_name(currentUser.toStdString()); - PendingCommand *pend = client->prepareModeratorCommand(cmd); - connect(pend, &PendingCommand::finished, this, &TabModeration::removeBannerResponse); - client->sendCommand(pend); -} - -void TabModeration::removeBannerResponse(const Response &response) -{ - const QString target = currentUser.isEmpty() ? tr("this user") : currentUser; - - switch (response.response_code()) { - case Response::RespOk: - break; - case Response::RespNameNotFound: - QMessageBox::warning(this, tr("User not found"), - tr("The server does not know a user named %1.").arg(target)); - return; - case Response::RespFunctionNotAllowed: - QMessageBox::warning(this, tr("Not supported"), - tr("This server does not support removing banner cards. It may be running an older " - "version.")); - return; - case Response::RespInternalError: - QMessageBox::critical( - this, tr("Error"), - tr("The server could not clear the banner card of %1 because of a database error.").arg(target)); - return; - default: - QMessageBox::critical( - this, tr("Error"), - tr("The server rejected the request for %1: %2") - .arg(target, QString::fromStdString(Response_ResponseCode_Name(response.response_code())))); - return; - } - - const Response_RemoveUserBanner &resp = response.GetExtension(Response_RemoveUserBanner::ext); - const QString removedUser = resp.user_name().empty() ? target : QString::fromStdString(resp.user_name()); - if (!resp.had_banner()) { - QMessageBox::information(this, tr("No banner set"), - tr("%1 has no banner card set, so there was nothing to remove.").arg(removedUser)); - return; - } - - QMessageBox::information(this, tr("Banner removed"), - tr("The banner card of %1 has been removed.\n\n" - "Other players stop seeing it the next time %1 logs in.") - .arg(removedUser)); -} diff --git a/cockatrice/src/interface/widgets/tabs/tab_moderation.h b/cockatrice/src/interface/widgets/tabs/tab_moderation.h index ce23b5a7a..e352e90bb 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_moderation.h +++ b/cockatrice/src/interface/widgets/tabs/tab_moderation.h @@ -20,7 +20,7 @@ class TabSupervisor; /** * Staff investigation tool. Lets moderators look up a user's account data, * alternate accounts, login sessions, and staff login activity, and offers - * the password-reset, remove-avatar, and remove-banner actions. + * the password-reset and remove-avatar actions. */ class TabModeration : public Tab { @@ -44,8 +44,6 @@ private slots: void resetPasswordResponse(const Response &response); void removeAvatar(); void removeAvatarResponse(const Response &response); - void removeBanner(); - void removeBannerResponse(const Response &response); private: void requestUserInfo(const QString &userName); @@ -62,7 +60,6 @@ private: QPushButton *investigateButton; QPushButton *resetPasswordButton; QPushButton *removeAvatarButton; - QPushButton *removeBannerButton; QGroupBox *userInfoGroup; QLabel *userInfoNameLabel; diff --git a/cockatrice/src/interface/widgets/tabs/tab_public_decks.cpp b/cockatrice/src/interface/widgets/tabs/tab_public_decks.cpp index 73d4da420..1ad973895 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 "../../theme_icon.h" #include "../cards/card_size_widget.h" #include "../general/layout_containers/flow_widget.h" @@ -28,14 +29,12 @@ #include #include #include -#include #include #include #include #include #include #include -#include #include #include @@ -46,7 +45,9 @@ TabPublicDecks::TabPublicDecks(TabSupervisor *_tabSupervisor, AbstractClient *_c cardSize = SettingsCache::instance().cardsDisplay().getVisualDeckStorageCardSize(); titleLabel = new QLabel(tr("Public decks of %1").arg(userName.toHtmlEscaped()), this); - titleLabel->setFont(QtUtils::boldFont(titleLabel->font())); + QFont titleFont = titleLabel->font(); + titleFont.setBold(true); + titleLabel->setFont(titleFont); auto *headerLayout = new QHBoxLayout; headerLayout->addWidget(titleLabel); diff --git a/cockatrice/src/interface/widgets/tabs/tab_replays.cpp b/cockatrice/src/interface/widgets/tabs/tab_replays.cpp index bd2a69017..423ab2851 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_replays.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_replays.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -44,6 +45,7 @@ #include #include #include +#include #include #include #include @@ -59,7 +61,6 @@ #include #include #include -#include #include #include #include @@ -399,7 +400,7 @@ void TabReplays::actDeleteLocalReplay() void TabReplays::actOpenReplaysFolder() { QString dir = localDirModel->rootPath(); - QtUtils::openPathInFileManager(dir); + QDesktopServices::openUrl(QUrl::fromLocalFile(dir)); } void TabReplays::actRemoteDoubleClick(const QModelIndex &curRight) diff --git a/cockatrice/src/interface/widgets/tabs/tab_report.cpp b/cockatrice/src/interface/widgets/tabs/tab_report.cpp index 988c3e7d9..9018e2477 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_report.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_report.cpp @@ -12,6 +12,8 @@ #include #include #include +#include +#include #include #include #include @@ -50,7 +52,6 @@ #include #include #include -#include #include #include @@ -137,7 +138,13 @@ TabReport::TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client) : T chatGroup = new QGroupBox; chatLogEdit = new QTextEdit; chatLogEdit->setReadOnly(true); - chatLogEdit->setFont(QtUtils::monospaceFont()); + QFont monoFont("monospace"); + monoFont.setStyleHint(QFont::Monospace); + const int systemPointSize = QFontDatabase::systemFont(QFontDatabase::GeneralFont).pointSize(); + if (systemPointSize > 0) { + monoFont.setPointSize(systemPointSize); + } + chatLogEdit->setFont(monoFont); auto *chatLayout = new QVBoxLayout(chatGroup); chatLayout->setContentsMargins(4, 4, 4, 4); chatLayout->addWidget(chatLogEdit); @@ -210,7 +217,8 @@ TabReport::TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client) : T sgLayout->addWidget(statsTrendLabel); sgLayout->addWidget(statsCategoriesLabel); sgLayout->addWidget(statsDetailText); - auto *statsGroupLayout = QtUtils::makeLayout(statsGroup); + auto *statsGroupLayout = new QVBoxLayout(statsGroup); + statsGroupLayout->setContentsMargins(0, 0, 0, 0); statsGroupLayout->addWidget(statsContent); statsGroup->setChecked(true); connect(statsGroup, &QGroupBox::toggled, this, [this, statsContent](bool checked) { diff --git a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp index 2f5b21280..2430c8cd6 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" @@ -48,6 +52,8 @@ #include #include #include +#include +#include #include #include #include @@ -60,10 +66,6 @@ #include #include #include -#include -#include -#include -#include #include #include #include @@ -78,7 +80,6 @@ #include #include #include -#include #include #include #include @@ -291,11 +292,48 @@ void TabSupervisor::retranslateUi() aTabDeveloper->setText(tr("Developer")); // tabs - for (int i = 0; i < count(); ++i) { - if (auto *tab = qobject_cast(widget(i))) { + QList tabs; + tabs.append(tabServer); + tabs.append(tabReplays); + tabs.append(tabDeckStorage); + tabs.append(tabAdmin); + tabs.append(tabAccount); + tabs.append(tabLog); + tabs.append(tabReport); + tabs.append(tabModeration); + tabs.append(tabCardArtRules); + tabs.append(tabDeveloper); + QMapIterator roomIterator(roomTabs); + while (roomIterator.hasNext()) { + tabs.append(roomIterator.next().value()); + } + QMapIterator gameIterator(gameTabs); + while (gameIterator.hasNext()) { + tabs.append(gameIterator.next().value()); + } + QMapIterator publicDecksIterator(publicDecksTabs); + while (publicDecksIterator.hasNext()) { + tabs.append(publicDecksIterator.next().value()); + } + QListIterator replayIterator(replayTabs); + while (replayIterator.hasNext()) { + tabs.append(replayIterator.next()); + } + QListIterator deckEditorIterator(deckEditorTabs); + while (deckEditorIterator.hasNext()) { + tabs.append(deckEditorIterator.next()); + } + QMapIterator messageIterator(messageTabs); + while (messageIterator.hasNext()) { + tabs.append(messageIterator.next().value()); + } + + for (auto &tab : tabs) { + if (tab) { + int idx = indexOf(tab); QString tabText = tab->getTabText(); - setTabText(i, sanitizeTabName(tabText)); - setTabToolTip(i, sanitizeHtml(tabText)); + setTabText(idx, sanitizeTabName(tabText)); + setTabToolTip(idx, sanitizeHtml(tabText)); tab->retranslateUi(); } } @@ -325,8 +363,9 @@ void TabSupervisor::closeEvent(QCloseEvent *event) QTabWidget::closeEvent(event); if (getGameCount()) { - if (!QtUtils::confirm(this, tr("Are you sure?"), - tr("There are still open games. Are you sure you want to quit?"), QMessageBox::No)) { + if (QMessageBox::question(this, tr("Are you sure?"), + tr("There are still open games. Are you sure you want to quit?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::No) { event->ignore(); return; } diff --git a/cockatrice/src/interface/widgets/tabs/tab_visual_database_display.cpp b/cockatrice/src/interface/widgets/tabs/tab_visual_database_display.cpp index 26d29463b..f4b007072 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_visual_database_display.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_visual_database_display.cpp @@ -23,10 +23,9 @@ TabVisualDatabaseDisplay::TabVisualDatabaseDisplay(TabSupervisor *_tabSupervisor void TabVisualDatabaseDisplay::retranslateUi() { - visualDatabaseDisplayWidget->retranslateUi(); } void TabVisualDatabaseDisplay::openEdhrecTab(const CardInfoPtr &info, bool isCommander) const { getTabSupervisor()->addEdhrecTab(info, isCommander); -} +} \ No newline at end of file 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..cb6dfab9c 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,15 +17,12 @@ #include #include #include -#include #include -#include #include #include #include #include #include -#include #include #include @@ -47,7 +46,9 @@ TabDeckStorageVisual::TabDeckStorageVisual(TabSupervisor *_tabSupervisor, Abstra }); auto *widget = new QWidget(this); - auto *layout = QtUtils::makeLayout(widget, 0); + auto *layout = new QVBoxLayout(widget); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); widget->setLayout(layout); this->setCentralWidget(widget); layout->addWidget(visualDeckStorageWidget); diff --git a/cockatrice/src/interface/widgets/utility/card_completer_delegate.cpp b/cockatrice/src/interface/widgets/utility/card_completer_delegate.cpp index a629df43c..af07590a8 100644 --- a/cockatrice/src/interface/widgets/utility/card_completer_delegate.cpp +++ b/cockatrice/src/interface/widgets/utility/card_completer_delegate.cpp @@ -34,7 +34,6 @@ #include #include #include -#include #include class QObject; @@ -182,8 +181,9 @@ void CardCompleterDelegate::drawManaSymbol(QPainter *p, QPoint centre, const QSt p->setBrush(Qt::NoBrush); p->drawEllipse(pip.adjusted(1, 1, -1, -1)); - QFont f = QtUtils::boldFont(p->font()); + QFont f = p->font(); f.setPixelSize(qMax(radius - 1, 7)); + f.setBold(true); p->setFont(f); p->setPen(mc.text); diff --git a/cockatrice/src/interface/widgets/utility/card_completer_styler.cpp b/cockatrice/src/interface/widgets/utility/card_completer_styler.cpp index 8f1941412..5b33303a0 100644 --- a/cockatrice/src/interface/widgets/utility/card_completer_styler.cpp +++ b/cockatrice/src/interface/widgets/utility/card_completer_styler.cpp @@ -31,7 +31,6 @@ #include #include #include -#include #include template class QSharedPointer; @@ -347,7 +346,12 @@ void CardCompleterStyler::showPreview() preview->show(); preview->raise(); - QtUtils::fadeWindow(preview, preview->windowOpacity(), 1.0, FadeDuration); + auto *fade = new QPropertyAnimation(preview, "windowOpacity", preview); + + fade->setDuration(FadeDuration); + fade->setStartValue(preview->windowOpacity()); + fade->setEndValue(1.0); + fade->start(QAbstractAnimation::DeleteWhenStopped); } // --------------------------------------------------------------------------- diff --git a/cockatrice/src/interface/widgets/utility/sequence_edit.cpp b/cockatrice/src/interface/widgets/utility/sequence_edit.cpp index b09e6da1e..a96b84dbb 100644 --- a/cockatrice/src/interface/widgets/utility/sequence_edit.cpp +++ b/cockatrice/src/interface/widgets/utility/sequence_edit.cpp @@ -15,7 +15,6 @@ #include #include #include -#include SequenceEdit::SequenceEdit(const QString &_shortcutName, QWidget *parent) : QWidget(parent) { @@ -27,7 +26,9 @@ SequenceEdit::SequenceEdit(const QString &_shortcutName, QWidget *parent) : QWid clearButton->setIcon(loadThemedIcon(QStringLiteral("icons/clearsearch"))); defaultButton->setIcon(loadThemedIcon(QStringLiteral("icons/reload"))); - auto *layout = QtUtils::makeLayout(this, 1); + auto *layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(1); layout->addWidget(lineEdit); layout->addWidget(clearButton); layout->addWidget(defaultButton); diff --git a/cockatrice/src/interface/widgets/visual_database_display/card_database_visual_view.cpp b/cockatrice/src/interface/widgets/visual_database_display/card_database_visual_view.cpp deleted file mode 100644 index 7b0d526f9..000000000 --- a/cockatrice/src/interface/widgets/visual_database_display/card_database_visual_view.cpp +++ /dev/null @@ -1,203 +0,0 @@ -#include "card_database_visual_view.h" - -#include "../../card_picture_loader/card_picture_loader.h" -#include "../cards/card_info_picture_widget.h" -#include "../cards/card_picture_delegate.h" -#include "../cards/card_size_widget.h" -#include "card_database_visual_display_model.h" -#include "libcockatrice/card/printing/exact_card.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -CardDatabaseVisualView::CardDatabaseVisualView(CardDatabaseVisualDisplayModel *_model, QWidget *parent) - : QListView(parent), model(_model) -{ - auto *cardDelegate = new CardPictureDelegate(model, this); - setItemDelegate(cardDelegate); - setModel(model); - - setViewMode(QListView::IconMode); - setFlow(QListView::LeftToRight); - setWrapping(true); - setResizeMode(QListView::Adjust); - setMovement(QListView::Static); - setUniformItemSizes(true); - setLayoutMode(QListView::SinglePass); - setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); - setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - setFrameShape(QFrame::NoFrame); - setWordWrap(false); - - // QListView hands the delegate's item height to the scroll bar as its single - // step under ScrollPerPixel, so one wheel notch would jump wheelScrollLines() - // whole card rows. Give the bar a text line instead; the wheel handler below - // scales that line to the grid. A positive step also stops Qt from taking the - // item height back on the next layout. - verticalScrollBar()->setSingleStep(qMax(1, fontMetrics().height())); - - // The grid is a read-only surface: clicks are actions on a card, not a - // selection, and they must not steal focus from the search field. - setSelectionMode(QAbstractItemView::NoSelection); - setEditTriggers(QAbstractItemView::NoEditTriggers); - setFocusPolicy(Qt::NoFocus); - - // Hover drives the card preview. - setMouseTracking(true); - viewport()->setMouseTracking(true); - - connect(&CardPictureLoader::getInstance(), &CardPictureLoader::pixmapLoaded, this, - [this] { viewport()->update(); }); - - // A new search result starts at the top, like the widget-per-card grid did. - connect(model, &QAbstractItemModel::modelReset, this, [this] { - verticalScrollBar()->setValue(0); - // The rows under a stationary cursor changed with the reset: re-resolve the - // hover so the preview follows the grid, as enterEvent did per card widget. - hoveredRow = -1; - updateHoveredRow(viewport()->mapFromGlobal(QCursor::pos())); - }); - - setCardSizeFactor(cardSizeFactor); -} - -void CardDatabaseVisualView::setCardSizeFactor(int factor) -{ - cardSizeFactor = qBound(CardSizeWidget::MIN_CARD_SIZE, factor, CardSizeWidget::MAX_CARD_SIZE); - - const QSize cardSize = CardInfoPictureWidget::sizeForScaleFactor(cardSizeFactor); - if (auto *cardDelegate = qobject_cast(itemDelegate())) { - cardDelegate->setCardSize(cardSize); - } - // The delegate's sizeHint does not emit sizeHintChanged, so the grid size is what - // makes a resize take effect. Keep the two in step. - setGridSize(cardSize + QSize(CardPictureDelegate::SPACING, CardPictureDelegate::SPACING)); -} - -void CardDatabaseVisualView::mousePressEvent(QMouseEvent *event) -{ - const QModelIndex index = indexAt(event->pos()); - if (index.isValid() && model != nullptr) { - const ExactCard card = model->cardAt(index.row()); - if (card) { - emit cardClicked(event, card); - event->accept(); - return; - } - } - - QListView::mousePressEvent(event); -} - -void CardDatabaseVisualView::mouseMoveEvent(QMouseEvent *event) -{ - QListView::mouseMoveEvent(event); - updateHoveredRow(event->pos()); -} - -bool CardDatabaseVisualView::viewportEvent(QEvent *event) -{ - // A per-card widget reported its hover from enterEvent, which also fires when the - // grid changes under a stationary cursor. A QListView only reports moves, so - // re-resolve the hover on enter and clear it on leave. - if (event->type() == QEvent::Enter) { - updateHoveredRow(viewport()->mapFromGlobal(QCursor::pos())); - } else if (event->type() == QEvent::Leave) { - hoveredRow = -1; - } - - return QListView::viewportEvent(event); -} - -void CardDatabaseVisualView::wheelEvent(QWheelEvent *event) -{ - // Ctrl+wheel is the card-size gesture and is filtered out before it reaches - // the view; any other modifier keeps QAbstractSlider's own page scrolling. - if (event->modifiers() != Qt::NoModifier) { - QListView::wheelEvent(event); - return; - } - - QScrollBar *bar = verticalScrollBar(); - if (!bar->isVisible()) { - event->ignore(); - return; - } - - // Touchpads and high-resolution wheels already report pixels: follow them. - if (!event->pixelDelta().isNull()) { - bar->setValue(bar->value() - event->pixelDelta().y()); - event->accept(); - return; - } - - if (event->angleDelta().y() == 0) { - QListView::wheelEvent(event); - return; - } - - // Accumulate wheel deltas smaller than a notch, like QAbstractSlider does, so - // a fine-grained wheel still moves. - wheelRemainder += static_cast(event->angleDelta().y()) / 120.0; - const int notches = static_cast(wheelRemainder); - if (notches == 0) { - event->accept(); - return; - } - wheelRemainder -= notches; - - const int pixels = -notches * wheelStep(); - bar->setValue(qBound(bar->minimum(), bar->value() + pixels, bar->maximum())); - event->accept(); -} - -int CardDatabaseVisualView::wheelStep() const -{ - // A card row is far too coarse for one wheel line, and a text line is too - // fine, so a line is an eighth of the grid cell. A notch then moves about a - // third of a card row at any card size, and the OS line count still applies. - const int lineHeight = qMax(1, gridSize().height() / 8); - return qMax(1, QApplication::wheelScrollLines()) * lineHeight; -} - -void CardDatabaseVisualView::updateHoveredRow(const QPoint &position) -{ - // indexAt() also answers for points outside the viewport, so a cursor parked - // below the grid (on the size slider, say) would resolve to the row just under - // the visible area. Only a point inside the viewport can hover a card. - if (model == nullptr || !viewport()->rect().contains(position)) { - return; - } - - const QModelIndex index = indexAt(position); - const int row = index.isValid() ? index.row() : -1; - if (row == hoveredRow) { - return; - } - - hoveredRow = row; - if (row >= 0) { - const ExactCard card = model->cardAt(row); - if (card) { - emit cardHovered(card); - } - } -} - -void CardDatabaseVisualView::leaveEvent(QEvent *event) -{ - hoveredRow = -1; - QListView::leaveEvent(event); -} diff --git a/cockatrice/src/interface/widgets/visual_database_display/card_database_visual_view.h b/cockatrice/src/interface/widgets/visual_database_display/card_database_visual_view.h deleted file mode 100644 index 5f004484d..000000000 --- a/cockatrice/src/interface/widgets/visual_database_display/card_database_visual_view.h +++ /dev/null @@ -1,67 +0,0 @@ -/** - * @file card_database_visual_view.h - * @ingroup VisualCardDatabaseWidgets - * @brief Icon-mode view of the card database, one card picture per item. - */ - -#ifndef CARD_DATABASE_VISUAL_VIEW_H -#define CARD_DATABASE_VISUAL_VIEW_H - -#include -#include -#include - -class CardDatabaseVisualDisplayModel; -class ExactCard; -class QEvent; -class QMouseEvent; -class QPoint; -class QWheelEvent; - -/** - * @brief A wrapping grid of card pictures. - * - * The view virtualizes painting: it only asks the delegate for the items that are - * on screen, so the whole card database can sit in the model without a widget (or - * pixmap) per card. Clicks and hover are reported with the exact card under the - * cursor, which is what the deck editor needs to add, decrement or preview a card. - */ -class CardDatabaseVisualView : public QListView -{ - Q_OBJECT - -public: - explicit CardDatabaseVisualView(CardDatabaseVisualDisplayModel *_model, QWidget *parent = nullptr); - - /** - * @brief Sets the card size as a percentage of the base card width. - * @param factor Percentage in the range of the card size slider. - */ - void setCardSizeFactor(int factor); - -signals: - /** @brief A card was pressed. The event is valid for the duration of the emission. */ - void cardClicked(QMouseEvent *event, const ExactCard &card); - /** @brief The cursor moved onto a different card. */ - void cardHovered(const ExactCard &card); - -protected: - void mousePressEvent(QMouseEvent *event) override; - void mouseMoveEvent(QMouseEvent *event) override; - void leaveEvent(QEvent *event) override; - bool viewportEvent(QEvent *event) override; - void wheelEvent(QWheelEvent *event) override; - -private: - void updateHoveredRow(const QPoint &position); - /// Pixels one wheel notch moves, scaled to the grid and the OS line count. - [[nodiscard]] int wheelStep() const; - - CardDatabaseVisualDisplayModel *model; - /// Fraction of a line left over from a wheel event smaller than one notch. - qreal wheelRemainder = 0.0; - int cardSizeFactor = 100; - int hoveredRow = -1; -}; - -#endif // CARD_DATABASE_VISUAL_VIEW_H diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp index 464d62e4a..cf815ace0 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -48,7 +49,7 @@ VisualDatabaseDisplayColorFilterWidget::VisualDatabaseDisplayColorFilterWidget(Q &VisualDatabaseDisplayColorFilterWidget::updateFilterMode); connect(filterModel, &FilterTreeModel::layoutChanged, this, - [this]() { syncDebouncer.call([this] { syncWithFilterModel(); }); }); + [this]() { QTimer::singleShot(100, this, &VisualDatabaseDisplayColorFilterWidget::syncWithFilterModel); }); // Call retranslateUi to set the initial text retranslateUi(); diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.h b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.h index 184f1f96c..38812b02a 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.h +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.h @@ -12,7 +12,6 @@ #include #include #include -#include #include class FilterTreeModel; @@ -72,7 +71,6 @@ private: QHBoxLayout *layout; QComboBox *modeComboBox; FilterMode currentMode = FilterMode::Includes; // Default mode - QtUtils::Debouncer syncDebouncer{100}; }; #endif // VISUAL_DATABASE_DISPLAY_COLOR_FILTER_WIDGET_H diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp index ecb949b67..cfa33f85b 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp @@ -29,7 +29,6 @@ #include #include #include -#include #include class DeckListModel; @@ -111,10 +110,14 @@ VisualDatabaseDisplayFilterToolbarWidget::VisualDatabaseDisplayFilterToolbarWidg void VisualDatabaseDisplayFilterToolbarWidget::initialize() { - auto sortLayout = QtUtils::makeLayout(sortGroupBox, 0); + auto sortLayout = new QHBoxLayout(sortGroupBox); + sortLayout->setContentsMargins(0, 0, 0, 0); + sortLayout->setSpacing(0); sortLayout->setAlignment(Qt::AlignLeft); - auto filterLayout = QtUtils::makeLayout(filterGroupBox, 2); + auto filterLayout = new QHBoxLayout(filterGroupBox); + filterLayout->setContentsMargins(0, 0, 0, 0); + filterLayout->setSpacing(2); filterLayout->setAlignment(Qt::AlignLeft); // create settings widgets diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_format_legality_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_format_legality_filter_widget.cpp index e0734fbb8..a5725ff3f 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_format_legality_filter_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_format_legality_filter_widget.cpp @@ -16,12 +16,12 @@ #include #include #include +#include #include #include #include #include #include -#include #include #include @@ -48,7 +48,8 @@ VisualDatabaseDisplayFormatLegalityFilterWidget::VisualDatabaseDisplayFormatLega layout->addWidget(flowWidget); // Create a container for the threshold control - auto *thresholdLayout = QtUtils::makeLayout(); + auto *thresholdLayout = new QHBoxLayout(); + thresholdLayout->setContentsMargins(0, 0, 0, 0); thresholdLabel = new QLabel(this); thresholdLayout->addWidget(thresholdLabel); @@ -71,8 +72,9 @@ VisualDatabaseDisplayFormatLegalityFilterWidget::VisualDatabaseDisplayFormatLega layout->addWidget(toggleButton); connect(toggleButton, &QPushButton::clicked, this, &VisualDatabaseDisplayFormatLegalityFilterWidget::updateFilterMode); - connect(filterModel, &FilterTreeModel::layoutChanged, this, - [this]() { syncDebouncer.call([this] { syncWithFilterModel(); }); }); + connect(filterModel, &FilterTreeModel::layoutChanged, this, [this]() { + QTimer::singleShot(100, this, &VisualDatabaseDisplayFormatLegalityFilterWidget::syncWithFilterModel); + }); createFormatButtons(); // Populate buttons initially updateFilterMode(); // Initialize toggle button text diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_format_legality_filter_widget.h b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_format_legality_filter_widget.h index 6cc55062e..e94593e67 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_format_legality_filter_widget.h +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_format_legality_filter_widget.h @@ -4,7 +4,6 @@ #include #include #include -#include #include class FilterTreeModel; @@ -43,7 +42,6 @@ private: QMap formatButtons; // Store toggle buttons bool exactMatchMode = true; // Toggle between "Exact Match" and "Includes" - QtUtils::Debouncer syncDebouncer{100}; }; #endif // COCKATRICE_VISUAL_DATABASE_DISPLAY_FORMAT_LEGALITY_FILTER_WIDGET_H diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_main_type_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_main_type_filter_widget.cpp index 018841b30..eff0fe10b 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_main_type_filter_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_main_type_filter_widget.cpp @@ -16,11 +16,11 @@ #include #include #include +#include #include #include #include #include -#include #include VisualDatabaseDisplayMainTypeFilterWidget::VisualDatabaseDisplayMainTypeFilterWidget(QWidget *parent, @@ -42,7 +42,8 @@ VisualDatabaseDisplayMainTypeFilterWidget::VisualDatabaseDisplayMainTypeFilterWi layout->addWidget(flowWidget); // Create a container for the threshold control - auto *thresholdLayout = QtUtils::makeLayout(); + auto *thresholdLayout = new QHBoxLayout(); + thresholdLayout->setContentsMargins(0, 0, 0, 0); thresholdLabel = new QLabel(this); thresholdLayout->addWidget(thresholdLabel); @@ -64,8 +65,9 @@ VisualDatabaseDisplayMainTypeFilterWidget::VisualDatabaseDisplayMainTypeFilterWi toggleButton = new QPushButton(this); layout->addWidget(toggleButton); connect(toggleButton, &QPushButton::clicked, this, &VisualDatabaseDisplayMainTypeFilterWidget::updateFilterMode); - connect(filterModel, &FilterTreeModel::layoutChanged, this, - [this]() { syncDebouncer.call([this] { syncWithFilterModel(); }); }); + connect(filterModel, &FilterTreeModel::layoutChanged, this, [this]() { + QTimer::singleShot(100, this, &VisualDatabaseDisplayMainTypeFilterWidget::syncWithFilterModel); + }); createMainTypeButtons(); // Populate buttons initially updateFilterMode(); // Initialize toggle button text diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_main_type_filter_widget.h b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_main_type_filter_widget.h index 1e2fa2b39..b5a6f332c 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_main_type_filter_widget.h +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_main_type_filter_widget.h @@ -10,7 +10,6 @@ #include #include #include -#include #include class FilterTreeModel; @@ -49,7 +48,6 @@ private: QMap typeButtons; // Store toggle buttons bool exactMatchMode = true; // Toggle between "Exact Match" and "Includes" - QtUtils::Debouncer syncDebouncer{100}; }; #endif // VISUAL_DATABASE_DISPLAY_MAIN_TYPE_FILTER_WIDGET_H diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_name_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_name_filter_widget.cpp index e335cd4c0..d3e497b21 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_name_filter_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_name_filter_widget.cpp @@ -16,7 +16,6 @@ #include #include #include -#include #include #include #include @@ -62,7 +61,7 @@ VisualDatabaseDisplayNameFilterWidget::VisualDatabaseDisplayNameFilterWidget(QWi &VisualDatabaseDisplayNameFilterWidget::actLoadFromClipboard); connect(filterModel, &FilterTreeModel::layoutChanged, this, - [this]() { syncDebouncer.call([this] { syncWithFilterModel(); }); }); + [this]() { QTimer::singleShot(100, this, &VisualDatabaseDisplayNameFilterWidget::syncWithFilterModel); }); retranslateUi(); } @@ -140,19 +139,20 @@ void VisualDatabaseDisplayNameFilterWidget::updateFilterModel() emit filterModel->layoutAboutToBeChanged(); filterModel->clearFiltersOfType(CardFilter::Attr::AttrNameExact); - { - // Scope the blockers so both models are unblocked before the explicit - // change notifications below. - const QSignalBlocker modelBlocker(filterModel); - const QSignalBlocker treeBlocker(filterModel->filterTree()); + filterModel->blockSignals(true); + filterModel->filterTree()->blockSignals(true); - for (const auto &name : activeFilters.keys()) { - QString nameString = name; - filterModel->addFilter( - new CardFilter(nameString, CardFilter::Type::TypeOr, CardFilter::Attr::AttrNameExact)); - } + for (const auto &name : activeFilters.keys()) { + QString nameString = name; + filterModel->addFilter(new CardFilter(nameString, CardFilter::Type::TypeOr, CardFilter::Attr::AttrNameExact)); } + filterModel->blockSignals(false); + filterModel->filterTree()->blockSignals(false); + + filterModel->blockSignals(false); + filterModel->filterTree()->blockSignals(false); + emit filterModel->filterTree()->changed(); emit filterModel->layoutChanged(); } diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_name_filter_widget.h b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_name_filter_widget.h index ba2684e05..1629cb93b 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_name_filter_widget.h +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_name_filter_widget.h @@ -10,7 +10,6 @@ #include #include #include -#include #include class DeckListModel; @@ -47,7 +46,6 @@ private: QPushButton *loadFromClipboardButton; QMap activeFilters; // Store active name filter buttons - QtUtils::Debouncer syncDebouncer{100}; private slots: void actLoadFromDeck(); diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_set_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_set_filter_widget.cpp index fe583361a..3d76fb78a 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_set_filter_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_set_filter_widget.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -29,13 +30,13 @@ #include #include #include -#include #include VisualDatabaseDisplayRecentSetFilterSettingsWidget::VisualDatabaseDisplayRecentSetFilterSettingsWidget(QWidget *parent) : QWidget(parent) { - layout = QtUtils::makeLayout(this); + layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); setLayout(layout); @@ -100,7 +101,7 @@ VisualDatabaseDisplaySetFilterWidget::VisualDatabaseDisplaySetFilterWidget(QWidg layout->addWidget(toggleButton); connect(toggleButton, &QPushButton::clicked, this, &VisualDatabaseDisplaySetFilterWidget::updateFilterMode); connect(filterModel, &FilterTreeModel::layoutChanged, this, - [this]() { syncDebouncer.call([this] { syncWithFilterModel(); }); }); + [this]() { QTimer::singleShot(100, this, &VisualDatabaseDisplaySetFilterWidget::syncWithFilterModel); }); createSetButtons(); // Populate buttons initially updateFilterMode(); @@ -303,5 +304,5 @@ void VisualDatabaseDisplaySetFilterWidget::updateFilterMode() // Reconnect the layoutChanged -> sync lambda connect(filterModel, &FilterTreeModel::layoutChanged, this, - [this]() { syncDebouncer.call([this] { syncWithFilterModel(); }); }); + [this]() { QTimer::singleShot(100, this, &VisualDatabaseDisplaySetFilterWidget::syncWithFilterModel); }); } diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_set_filter_widget.h b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_set_filter_widget.h index 4874df79b..468e97e0d 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_set_filter_widget.h +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_set_filter_widget.h @@ -10,7 +10,6 @@ #include #include #include -#include #include class FilterTreeModel; @@ -65,7 +64,6 @@ private: QMap activeSets; // Track active set filters bool exactMatchMode = true; // Toggle between "Exact Match" and "Includes" - QtUtils::Debouncer syncDebouncer{100}; }; #endif // VISUAL_DATABASE_DISPLAY_SET_FILTER_WIDGET_H diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_sub_type_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_sub_type_filter_widget.cpp index 68a1edf11..705bb8efa 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_sub_type_filter_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_sub_type_filter_widget.cpp @@ -17,11 +17,11 @@ #include #include #include +#include #include #include #include #include -#include #include VisualDatabaseDisplaySubTypeFilterWidget::VisualDatabaseDisplaySubTypeFilterWidget(QWidget *parent, @@ -47,7 +47,8 @@ VisualDatabaseDisplaySubTypeFilterWidget::VisualDatabaseDisplaySubTypeFilterWidg layout->addWidget(flowWidget); // Create a container for the threshold control - auto *thresholdLayout = QtUtils::makeLayout(); + auto *thresholdLayout = new QHBoxLayout(); + thresholdLayout->setContentsMargins(0, 0, 0, 0); thresholdLabel = new QLabel(this); thresholdLayout->addWidget(thresholdLabel); @@ -69,8 +70,9 @@ VisualDatabaseDisplaySubTypeFilterWidget::VisualDatabaseDisplaySubTypeFilterWidg toggleButton = new QPushButton(this); layout->addWidget(toggleButton); connect(toggleButton, &QPushButton::clicked, this, &VisualDatabaseDisplaySubTypeFilterWidget::updateFilterMode); - connect(filterModel, &FilterTreeModel::layoutChanged, this, - [this]() { syncDebouncer.call([this] { syncWithFilterModel(); }); }); + connect(filterModel, &FilterTreeModel::layoutChanged, this, [this]() { + QTimer::singleShot(100, this, &VisualDatabaseDisplaySubTypeFilterWidget::syncWithFilterModel); + }); createSubTypeButtons(); // Populate buttons initially updateFilterMode(); // Initialize the toggle button text diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_sub_type_filter_widget.h b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_sub_type_filter_widget.h index 8bef870ed..7929063fd 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_sub_type_filter_widget.h +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_sub_type_filter_widget.h @@ -10,7 +10,6 @@ #include #include #include -#include #include class FilterTreeModel; @@ -51,7 +50,6 @@ private: QMap typeButtons; // Store toggle buttons bool exactMatchMode = true; // Toggle between "Exact Match" and "Includes" - QtUtils::Debouncer syncDebouncer{100}; }; #endif // VISUAL_DATABASE_DISPLAY_SUB_TYPE_FILTER_WIDGET_H diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp index af5c74969..a76e3be5f 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp @@ -13,23 +13,32 @@ #include "../utility/custom_line_edit.h" #include "card_database_display_model.h" #include "card_database_model.h" -#include "card_database_visual_display_model.h" -#include "card_database_visual_view.h" #include "deck_list_model.h" #include "libcockatrice/card/card_localization.h" #include "libcockatrice/card/database/card_database_loader.h" #include "libcockatrice/card/database/card_database_querier.h" #include "libcockatrice/card/printing/exact_card.h" +#include "libcockatrice/card/printing/printing_info.h" #include "libcockatrice/filters/filter_card.h" #include "libcockatrice/filters/filter_tree.h" #include "visual_database_display_color_filter_widget.h" #include "visual_database_display_filter_toolbar_widget.h" +#include #include +#include #include #include #include +#include +#include +#include +#include +#include #include +#include +#include +#include #include #include #include @@ -39,11 +48,13 @@ #include #include #include +#include +#include #include #include #include #include -#include +#include class QMouseEvent; @@ -52,6 +63,11 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent, DeckListModel *deckListModel) : QWidget(parent) { + debounceTimer = new QTimer(this); + debounceTimer->setSingleShot(true); // Ensure it only fires once after the timeout + + connect(debounceTimer, &QTimer::timeout, this, &VisualDatabaseDisplayWidget::onSearchModelChanged); + // Create display model databaseDisplayModel = new CardDatabaseDisplayModel(this); databaseDisplayModel->setObjectName("databaseDisplayModel"); @@ -69,11 +85,24 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent, connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardSearchLanguageChanged, this, applyCardSearchLanguage); + cards = new QList; + connect(databaseDisplayModel, &CardDatabaseDisplayModel::modelDirty, this, + &VisualDatabaseDisplayWidget::modelDirty); + // Set up main layout and widgets setMinimumSize(0, 0); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - mainLayout = QtUtils::makeLayout(this, 0, 1); + mainLayout = new QVBoxLayout(this); setLayout(mainLayout); + mainLayout->setSpacing(1); + mainLayout->setContentsMargins(0, 0, 0, 0); + + flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarPolicy::ScrollBarAsNeeded); + cardSizeWidget = new CardSizeWidget(this, flowWidget, + SettingsCache::instance().cardsDisplay().getVisualDatabaseDisplayCardSize()); + connect(cardSizeWidget, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance().cardsDisplay(), + &CardsDisplaySettings::setVisualDatabaseDisplayCardSize); + cardSizeWidget->enableCtrlScrollResize(flowWidget); searchContainer = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAlwaysOff); @@ -88,7 +117,7 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent, setFocusProxy(searchEdit); setFocusPolicy(Qt::ClickFocus); - displayModeButton = new QPushButton(this); + displayModeButton = new QPushButton(tr("Visual"), this); displayModeButton->setCheckable(true); // Toggle button connect(displayModeButton, &QPushButton::toggled, this, &VisualDatabaseDisplayWidget::onDisplayModeChanged); @@ -131,25 +160,6 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent, connect(databaseView, &CardDatabaseView::relatedCardClicked, this, &VisualDatabaseDisplayWidget::onRelatedCardClicked); - // The visual grid is a real view on a flattened model: one row per displayed - // card picture, painted on demand. - visualDisplayModel = new CardDatabaseVisualDisplayModel(databaseDisplayModel, CardDatabaseManager::query(), this); - - cardView = new CardDatabaseVisualView(visualDisplayModel, this); - cardView->setObjectName("visualCardView"); - connect(cardView, &CardDatabaseVisualView::cardClicked, this, - &VisualDatabaseDisplayWidget::cardClickedDatabaseDisplay); - connect(cardView, &CardDatabaseVisualView::cardHovered, this, - &VisualDatabaseDisplayWidget::cardHoveredDatabaseDisplay); - - const int cardSize = SettingsCache::instance().cardsDisplay().getVisualDatabaseDisplayCardSize(); - cardSizeWidget = new CardSizeWidget(this, nullptr, cardSize); - connect(cardSizeWidget, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance().cardsDisplay(), - &CardsDisplaySettings::setVisualDatabaseDisplayCardSize); - connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, cardView, &CardDatabaseVisualView::setCardSizeFactor); - cardView->setCardSizeFactor(cardSize); - cardSizeWidget->enableCtrlScrollResize(cardView); - colorFilterWidget = new VisualDatabaseDisplayColorFilterWidget(this, filterModel); filterContainer = new VisualDatabaseDisplayFilterToolbarWidget(this, deckListModel); @@ -181,6 +191,8 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent, databaseLoadIndicator->setVisible(false); } + QScrollBar *scrollBar = flowWidget->scrollArea->verticalScrollBar(); + connect(scrollBar, &QScrollBar::valueChanged, [this](int /*value*/) { loadCurrentPage(); }); retranslateUi(); } @@ -199,7 +211,7 @@ void VisualDatabaseDisplayWidget::initialize() mainLayout->addWidget(databaseView); - mainLayout->addWidget(cardView); + mainLayout->addWidget(flowWidget); mainLayout->addWidget(cardSizeWidget); @@ -230,6 +242,8 @@ void VisualDatabaseDisplayWidget::initializeFilters() databaseDisplayModel->setFilterTree(filterModel->filterTree()); + QTimer::singleShot(5000, this, [this] { loadCurrentPage(); }); + retranslateUi(); }); } @@ -249,10 +263,6 @@ void VisualDatabaseDisplayWidget::retranslateUi() { databaseLoadIndicator->setText(tr("Loading database ...")); clearFilterWidget->setToolTip(tr("Clear all filters")); - searchEdit->setPlaceholderText(tr("Search by card name (or search expressions)")); - updateDisplayModeButton(); - colorFilterWidget->retranslateUi(); - filterContainer->retranslateUi(); } void VisualDatabaseDisplayWidget::highlightAllSearchEdit() @@ -260,41 +270,72 @@ void VisualDatabaseDisplayWidget::highlightAllSearchEdit() searchEdit->setSelection(0, searchEdit->text().length()); } -void VisualDatabaseDisplayWidget::updateDisplayModeButton() +void VisualDatabaseDisplayWidget::resizeEvent(QResizeEvent *event) { - displayModeButton->setText(displayModeButton->isChecked() ? tr("Table") : tr("Visual")); + QWidget::resizeEvent(event); + loadCurrentPage(); } void VisualDatabaseDisplayWidget::onDisplayModeChanged(bool checked) { - updateDisplayModeButton(); if (checked) { // Table mode - cardView->setVisible(false); + displayModeButton->setText(tr("Table")); + flowWidget->setVisible(false); cardSizeWidget->setVisible(false); databaseView->setItemDelegate(new QStyledItemDelegate(databaseView)); databaseView->setVisible(true); } else { // Visual mode - cardView->setVisible(true); + displayModeButton->setText(tr("Visual")); + flowWidget->setVisible(true); cardSizeWidget->setVisible(true); databaseView->setVisible(false); + populateCards(); } } +void VisualDatabaseDisplayWidget::onClick(QMouseEvent *event, const ExactCard &card) +{ + emit cardClickedDatabaseDisplay(event, card); +} + +void VisualDatabaseDisplayWidget::onHover(const ExactCard &hoveredCard) +{ + emit cardHoveredDatabaseDisplay(hoveredCard); +} + +void VisualDatabaseDisplayWidget::addCardToDisplay(const ExactCard &cardToAdd) +{ + cards->append(cardToAdd); + auto *display = new CardInfoPictureWithTextOverlayWidget(flowWidget, false); + display->setScaleFactor(cardSizeWidget->getSlider()->value()); + display->setCard(cardToAdd); + flowWidget->addWidget(display); + connect(display, &CardInfoPictureWithTextOverlayWidget::cardClicked, this, &VisualDatabaseDisplayWidget::onClick); + connect(display, &CardInfoPictureWithTextOverlayWidget::hoveredOnCard, this, &VisualDatabaseDisplayWidget::onHover); + connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, display, &CardInfoPictureWidget::setScaleFactor); +} + bool VisualDatabaseDisplayWidget::isVisualDisplayMode() const { return !displayModeButton->isChecked(); } -QStringList VisualDatabaseDisplayWidget::collectSetFilterTerms() const -{ - return filterModel->getFilterTermsOfType(CardFilter::AttrSet); -} - void VisualDatabaseDisplayWidget::onSearchModelChanged() { - visualDisplayModel->setSetFilterTerms(collectSetFilterTerms()); + if (flowWidget->isVisible()) { + // Clear the current page and prepare for new data + flowWidget->clearLayout(); // Clear existing cards + cards->clear(); // Clear the card list + // Reset scrollbar position to the top after loading new cards + QScrollBar *scrollBar = flowWidget->scrollArea->verticalScrollBar(); + scrollBar->setValue(0); // Reset scrollbar to top + + currentPage = 0; + loadCurrentPage(); + qCDebug(VisualDatabaseDisplayLog) << "Search model changed"; + } } void VisualDatabaseDisplayWidget::onSelectedCardChanged(const QString &cardName) @@ -320,3 +361,112 @@ void VisualDatabaseDisplayWidget::onRelatedCardClicked(const QString &relatedCar ExactCard exactCard = CardDatabaseManager::query()->guessCard({relatedCard}); emit cardInfoRequested(exactCard); } + +bool VisualDatabaseDisplayWidget::nearEndOfPage() const +{ + if (!flowWidget->isVisible()) { + return false; + } + + QScrollBar *scrollBar = flowWidget->scrollArea->verticalScrollBar(); + if (scrollBar->value() + scrollBar->pageStep() * 2 < scrollBar->maximum()) { + return false; // there is at least two pages of space to scroll remaining + } + + return true; +} + +void VisualDatabaseDisplayWidget::loadCurrentPage() +{ + // Ensure only the initial page is loaded + if (currentPage == 0) { + if (!initialLoadScheduled) { + initialLoadScheduled = true; + qCDebug(VisualDatabaseDisplayLog) << "Loading the first page"; + // Defer the first page so the tab switch stays responsive. The card + // grid builds one event loop turn later. This also applies to + // search-driven reloads, which reset currentPage back to 0. + QTimer::singleShot(0, this, [this] { + initialLoadScheduled = false; + populateCards(); + }); + } + } else if (nearEndOfPage()) { + // If not the first page, just load the next page and append to the flow widget + loadNextPage(); + } +} + +void VisualDatabaseDisplayWidget::populateCards() +{ + cards->clear(); + loadNextPage(); +} + +void VisualDatabaseDisplayWidget::loadNextPage() +{ + // Calculate the start and end indices for the next page + int rowCount = databaseDisplayModel->rowCount(); + int start = currentPage * cardsPerPage; + int end = qMin(start + cardsPerPage, rowCount); + + // Load more cards if we are at the end of the current list and can fetch more + if (end >= rowCount && databaseDisplayModel->canFetchMore(QModelIndex())) { + databaseDisplayModel->fetchMore(QModelIndex()); + } + + // Load the next page of cards and add them to the flow widget + flowWidget->setUpdatesEnabled(false); + loadPage(start, end); + flowWidget->setUpdatesEnabled(true); +} + +void VisualDatabaseDisplayWidget::loadPage(int start, int end) +{ + QList setFilters = filterModel->getFiltersOfType(CardFilter::AttrSet); + for (int row = start; row < end; ++row) { + qCDebug(VisualDatabaseDisplayLog) << "Adding " << row; + QModelIndex index = databaseDisplayModel->index(row, CardDatabaseModel::NameColumn); + QVariant name = databaseDisplayModel->data(index, Qt::DisplayRole); + qCDebug(VisualDatabaseDisplayLog) << name.toString(); + + if (CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(name.toString())) { + if (!setFilters.empty()) { + SetToPrintingsMap setMap = info->getSets(); + for (const CardFilter *setFilter : setFilters) { + if (setMap.contains(setFilter->term())) { + for (PrintingInfo printing : setMap[setFilter->term()]) { + addCardToDisplay(ExactCard(info, printing)); + } + } + } + } else { + addCardToDisplay(CardDatabaseManager::query()->getPreferredCard(info)); + } + } else { + qCDebug(VisualDatabaseDisplayLog) << "Card not found in database!"; + } + } + currentPage++; +} + +void VisualDatabaseDisplayWidget::modelDirty() const +{ + debounceTimer->start(debounceTime); +} + +void VisualDatabaseDisplayWidget::sortCardList(const QStringList &properties, + Qt::SortOrder order = Qt::AscendingOrder) const +{ + CardInfoComparator comparator(properties, order); + std::sort(cards->begin(), cards->end(), [comparator](const ExactCard &a, const ExactCard &b) { + return comparator(a.getCardPtr(), b.getCardPtr()); + }); +} + +void VisualDatabaseDisplayWidget::databaseDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + (void)topLeft; + (void)bottomRight; + qCDebug(VisualDatabaseDisplayLog) << "Database Data changed"; +} diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h index 6148670a3..9771e55c1 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h @@ -9,6 +9,8 @@ #include "libcockatrice/card/card_info.h" +#include +#include #include #include #include @@ -19,21 +21,26 @@ class CardDatabaseDisplayModel; class CardDatabaseModel; class CardDatabaseView; -class CardDatabaseVisualDisplayModel; -class CardDatabaseVisualView; class CardSizeWidget; +class DeckList; class DeckListModel; class ExactCard; class FilterTreeModel; class FlowWidget; +class OverlapControlWidget; class QLabel; -class QMouseEvent; +class QModelIndex; class QPushButton; +class QScrollArea; +class QTimer; class QToolButton; class QVBoxLayout; class SearchLineEdit; class VisualDatabaseDisplayColorFilterWidget; class VisualDatabaseDisplayFilterToolbarWidget; +template class QList; + +inline Q_LOGGING_CATEGORY(VisualDatabaseDisplayLog, "visual_database_display"); class VisualDatabaseDisplayWidget : public QWidget { @@ -45,6 +52,14 @@ public: DeckListModel *deckListModel = nullptr); void retranslateUi(); + void adjustCardsPerPage(); + void populateCards(); + void loadPage(int start, int end); + void loadNextPage(); + void loadCurrentPage(); + void sortCardList(const QStringList &properties, Qt::SortOrder order) const; + void setDeckList(const DeckList &new_deck_list_model); + /** * @brief Sets the callback used to create a custom zone from the add-to-zone menu. * The callback returns the name of the created zone, or an empty string if creation was cancelled. @@ -72,13 +87,6 @@ public: bool isVisualDisplayMode() const; public slots: - /** - * @brief Pushes the active set filters to the visual grid. - * - * The grid's rows follow the display model's filter and sort signals on their - * own; this only forwards the set filters, which decide which printings the - * grid shows. - */ void onSearchModelChanged(); signals: @@ -93,6 +101,11 @@ signals: protected slots: void initialize(); + void onClick(QMouseEvent *event, const ExactCard &card); + void onHover(const ExactCard &hoveredCard); + void addCardToDisplay(const ExactCard &cardToAdd); + void databaseDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); + void modelDirty() const; void onDisplayModeChanged(bool checked); void onSelectedCardChanged(const QString &cardName); @@ -112,21 +125,30 @@ private: QToolButton *clearFilterWidget; VisualDatabaseDisplayFilterToolbarWidget *filterContainer; CardDatabaseDisplayModel *databaseDisplayModel; - CardDatabaseVisualDisplayModel *visualDisplayModel; CardDatabaseView *databaseView; - CardDatabaseVisualView *cardView; std::function newZoneCreator; + QList *cards; QVBoxLayout *mainLayout; + QScrollArea *scrollArea; + FlowWidget *flowWidget; + QWidget *overlapCategories; + QVBoxLayout *overlapCategoriesLayout; + OverlapControlWidget *overlapControlWidget; CardSizeWidget *cardSizeWidget; + QTimer *debounceTimer; + int debounceTime = 300; // in Ms + int currentPage = 0; // Current page index + int cardsPerPage = 100; // Number of cards per page bool filtersInitialized = false; + bool initialLoadScheduled = false; void initializeFilters(); void highlightAllSearchEdit(); - void updateDisplayModeButton(); - [[nodiscard]] QStringList collectSetFilterTerms() const; + bool nearEndOfPage() const; protected: + void resizeEvent(QResizeEvent *event) override; void showEvent(QShowEvent *event) override; }; diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_filter_display_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_filter_display_widget.cpp index d13db1273..0f6ac51ec 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_filter_display_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_filter_display_widget.cpp @@ -13,7 +13,6 @@ #include #include #include -#include #include FilterDisplayWidget::FilterDisplayWidget(QWidget *parent, const QString &filename, FilterTreeModel *_filterModel) @@ -70,8 +69,10 @@ void FilterDisplayWidget::loadFilter() void FilterDisplayWidget::deleteFilter() { // Show confirmation dialog before deleting the filter - if (QtUtils::confirm(this, tr("Confirm Delete"), - tr("Are you sure you want to delete the filter '%1'?").arg(filterFilename))) { + QMessageBox::StandardButton reply = QMessageBox::question( + this, tr("Confirm Delete"), tr("Are you sure you want to delete the filter '%1'?").arg(filterFilename), + QMessageBox::Yes | QMessageBox::No); + if (reply == QMessageBox::Yes) { // If confirmed, delete the filter QString filePath = SettingsCache::instance().paths().getFiltersPath() + QDir::separator() + filterFilename; QFile file(filePath); diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.cpp index f0c054c3a..a9af51bf1 100644 --- a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #include #include #include @@ -26,7 +25,9 @@ VisualDeckDisplayOptionsWidget::VisualDeckDisplayOptionsWidget(QWidget *parent) : QWidget(parent) { - groupAndSortLayout = QtUtils::makeLayout(this, 3); + groupAndSortLayout = new QHBoxLayout(this); + groupAndSortLayout->setContentsMargins(0, 0, 0, 0); + groupAndSortLayout->setSpacing(3); groupAndSortLayout->setAlignment(Qt::AlignLeft); this->setLayout(groupAndSortLayout); @@ -109,33 +110,12 @@ void VisualDeckDisplayOptionsWidget::retranslateUi() sortByLabel->setText(tr("Sort by:")); sortLabel->setText(tr("Click and drag to change the sort order within the groups")); sortCriteriaButton->setToolTip(tr("Configure how cards are sorted within their groups")); - updateDisplayTypeButton(); + displayTypeButton->setButtonText(tr("Toggle Layout: Overlap")); + displayTypeButton->setButtonIcon(loadThemedIcon(QStringLiteral("icons/scales"))); displayTypeButton->setToolTip( tr("Change how cards are displayed within zones (i.e. overlapped or fully visible.)")); } -void VisualDeckDisplayOptionsWidget::updateDisplayTypeButton() -{ - switch (currentDisplayType) { - case DisplayType::Flat: - displayTypeButton->setButtonText(tr("Toggle Layout: Flat")); - displayTypeButton->setButtonIcon(loadThemedIcon(QStringLiteral("icons/scroll"))); - break; - case DisplayType::Overlap: - displayTypeButton->setButtonText(tr("Toggle Layout: Overlap")); - displayTypeButton->setButtonIcon(loadThemedIcon(QStringLiteral("icons/scales"))); - break; - } -} - -void VisualDeckDisplayOptionsWidget::changeEvent(QEvent *event) -{ - if (event->type() == QEvent::LanguageChange) { - retranslateUi(); - } - QWidget::changeEvent(event); -} - void VisualDeckDisplayOptionsWidget::onSortCriteriaChange() { QStringList selectedCriteria; @@ -152,8 +132,17 @@ void VisualDeckDisplayOptionsWidget::updateDisplayType() // Toggle the display type currentDisplayType = (currentDisplayType == DisplayType::Overlap) ? DisplayType::Flat : DisplayType::Overlap; - // Update only the toggle button, not every string in the widget - updateDisplayTypeButton(); + // Update UI and emit signal + switch (currentDisplayType) { + case DisplayType::Flat: + displayTypeButton->setButtonText(tr("Toggle Layout: Flat")); + displayTypeButton->setButtonIcon(loadThemedIcon(QStringLiteral("icons/scroll"))); + break; + case DisplayType::Overlap: + displayTypeButton->setButtonText(tr("Toggle Layout: Overlap")); + displayTypeButton->setButtonIcon(loadThemedIcon(QStringLiteral("icons/scales"))); + break; + } emit displayTypeChanged(currentDisplayType); } diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.h b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.h index c40b8ffeb..96600c587 100644 --- a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.h @@ -11,7 +11,6 @@ class CompactPushButton; class QComboBox; -class QEvent; class QHBoxLayout; class QLabel; class QListWidget; @@ -58,7 +57,6 @@ public slots: * @brief Updates all UI text for retranslation/localization. * * Called when the application language changes. - * Also refreshes the layout toggle button text for the current display type. */ void retranslateUi(); void updateCompactMode(bool mode); @@ -96,10 +94,6 @@ public: return activeSortCriteria; } -protected: - /** @brief Retranslates on QEvent::LanguageChange. */ - void changeEvent(QEvent *event) override; - private slots: /** * @brief Slot triggered whenever the sort list is reordered. @@ -116,8 +110,6 @@ private slots: void updateDisplayType(); private: - /** @brief Updates the layout toggle button's text and icon for the current display type. */ - void updateDisplayTypeButton(); /** @brief Layout for grouping and sorting UI elements. */ QHBoxLayout *groupAndSortLayout; diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.cpp index 91ad63433..1554dceb8 100644 --- a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.cpp @@ -25,7 +25,6 @@ #include #include #include -#include #include #include #include @@ -42,7 +41,9 @@ VisualDeckEditorSampleHandWidget::VisualDeckEditorSampleHandWidget(QWidget *pare layout->setSpacing(0); setLayout(layout); - auto upperLayout = QtUtils::makeLayout(nullptr, 0); + auto upperLayout = new QVBoxLayout(); + upperLayout->setContentsMargins(0, 0, 0, 0); + upperLayout->setSpacing(0); resetAndHandSizeContainerWidget = new QWidget(this); resetAndHandSizeLayout = new QHBoxLayout(resetAndHandSizeContainerWidget); diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp index 0e0b5e4b4..b2580ea8f 100644 --- a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp @@ -42,7 +42,6 @@ #include #include #include -#include #include VisualDeckEditorWidget::VisualDeckEditorWidget(QWidget *parent, @@ -189,9 +188,14 @@ void VisualDeckEditorWidget::initializeDisplayOptionsAndSearchWidget() void VisualDeckEditorWidget::initializeScrollAreaAndZoneContainer() { - scrollArea = QtUtils::makeScrollArea(this); + scrollArea = new QScrollArea(this); + scrollArea->setWidgetResizable(true); scrollArea->setMinimumSize(0, 0); + // Set scrollbar policies + scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + zoneContainer = new QWidget(scrollArea); zoneContainer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); zoneContainer->setObjectName("zoneContainer"); 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..dc619e9fa 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,12 +14,8 @@ #include #include #include -#include #include -#include -#include #include -#include #include #include @@ -28,7 +26,8 @@ DeckPreviewDeckTagsDisplayWidget::DeckPreviewDeckTagsDisplayWidget(QWidget *_par setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); // Create layout - auto *layout = QtUtils::makeLayout(this); + auto *layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); setFixedHeight(100); @@ -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_tag_item_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_item_widget.cpp index 8850060ea..7796f893b 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_item_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_item_widget.cpp @@ -2,12 +2,13 @@ #include #include -#include DeckPreviewTagItemWidget::DeckPreviewTagItemWidget(const QString &tagName, bool isChecked, QWidget *parent) : QWidget(parent), checkBox_(new QCheckBox(this)) { - auto *layout = QtUtils::makeLayout(this, 5); + auto *layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(5); checkBox_->setText(tagName); // Set the tag name as the checkbox label checkBox_->setChecked(isChecked); // Set the initial state of the checkbox 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/deck_preview/deck_preview_widget.h b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h index f3fc982ac..cdeed87c7 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h @@ -16,7 +16,6 @@ #include #include #include -#include #include class QFrame; @@ -132,9 +131,13 @@ protected: // If popup is not open, forward event to parent scroll area if (!combo->view()->isVisible()) { // Try to find a scrollable parent and manually send the event - if (auto *scroll = QtUtils::findParentOfType(combo)) { - QApplication::sendEvent(scroll->viewport(), event); - return true; // Mark event as handled + QWidget *parent = combo->parentWidget(); + while (parent) { + if (auto *scroll = qobject_cast(parent)) { + QApplication::sendEvent(scroll->viewport(), event); + return true; // Mark event as handled + } + parent = parent->parentWidget(); } // If no scrollable parent found, just block return true; diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/public_deck_preview_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/public_deck_preview_widget.cpp index 11a9da66f..361747423 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/public_deck_preview_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/public_deck_preview_widget.cpp @@ -16,7 +16,6 @@ #include #include #include -#include #include PublicDeckPreviewWidget::PublicDeckPreviewWidget(QWidget *parent, const RemotePublicDecksModel::DeckEntry &entry) @@ -37,7 +36,8 @@ PublicDeckPreviewWidget::PublicDeckPreviewWidget(QWidget *parent, const RemotePu tagsFlowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); tagsFlowWidget->setSpacing(3, 3); - auto *layout = QtUtils::makeLayout(this); + auto *layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); layout->addWidget(bannerCardDisplayWidget); layout->addWidget(uploadTimeLabel); layout->addWidget(colorIdentityWidget); 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_search_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_search_widget.cpp index 10a3bf33a..f5fd4d8a4 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_search_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_search_widget.cpp @@ -9,7 +9,6 @@ #include #include #include -#include /** * @brief Constructs a search bar for filtering decks in the Visual Deck Storage. @@ -21,7 +20,8 @@ */ VisualDeckStorageSearchWidget::VisualDeckStorageSearchWidget(QWidget *parent) : QWidget(parent) { - layout = QtUtils::makeLayout(this); + layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); setLayout(layout); searchBar = new QLineEdit(this); 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/widgets/visual_deck_storage/visual_deck_storage_sort_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_widget.cpp index e4285e360..f798a72a9 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_widget.cpp @@ -7,11 +7,11 @@ #include #include #include -#include VisualDeckStorageSortWidget::VisualDeckStorageSortWidget(VisualDeckStorageWidget *parent) : QWidget(parent) { - layout = QtUtils::makeLayout(this); + layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); setLayout(layout); // Initialize the ComboBox diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp index bbf68adb9..a2fa8dac9 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp @@ -33,7 +33,6 @@ #include #include #include -#include #include VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(parent) @@ -99,7 +98,10 @@ VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(pare &VisualDeckStorageWidget::updateSelectionAnimationEnabled); // deck area - scrollArea = QtUtils::makeScrollArea(this); + scrollArea = new QScrollArea(this); + scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + scrollArea->setWidgetResizable(true); // putting everything together layout->addWidget(searchAndSortContainer); @@ -184,9 +186,7 @@ void VisualDeckStorageWidget::resizeEvent(QResizeEvent *event) void VisualDeckStorageWidget::retranslateUi() { - if (databaseLoadIndicator) { - databaseLoadIndicator->setText(tr("Loading database ...")); - } + databaseLoadIndicator->setText(tr("Loading database ...")); refreshButton->setToolTip(tr("Refresh loaded files")); shareButton->setToolTip(tr("Select decks to share")); @@ -281,14 +281,6 @@ void VisualDeckStorageWidget::createRootFolderWidget() folderWidget = new VisualDeckStorageFolderDisplayWidget(this, this, QString(), false, quickSettingsWidget->getShowFolders()); - // QScrollArea::setWidget() deletes the widget it currently holds. If that is still the - // database load indicator, take it back first so the label stays alive for retranslateUi(). - if (databaseLoadIndicator && scrollArea->widget() == databaseLoadIndicator) { - scrollArea->takeWidget(); - databaseLoadIndicator->setParent(this); - databaseLoadIndicator->setVisible(false); - } - scrollArea->setWidget(folderWidget); // this automatically destroys the old folderWidget scrollArea->widget()->setMaximumWidth(scrollArea->viewport()->width()); scrollArea->widget()->adjustSize(); diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.h b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.h index 19d16f107..b512dd87c 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.h @@ -6,7 +6,6 @@ #ifndef VISUAL_DECK_STORAGE_WIDGET_H #define VISUAL_DECK_STORAGE_WIDGET_H -#include #include #include #include @@ -88,7 +87,7 @@ private: QVBoxLayout *layout; QWidget *searchAndSortContainer; QHBoxLayout *searchAndSortLayout; - QPointer databaseLoadIndicator; + QLabel *databaseLoadIndicator; VisualDeckStorageSortWidget *sortWidget; VisualDeckStorageSearchWidget *searchWidget; DeckPreviewColorIdentityFilterWidget *deckPreviewColorIdentityFilterWidget; diff --git a/cockatrice/src/interface/window_main.cpp b/cockatrice/src/interface/window_main.cpp index 4e2e90556..76afaa132 100644 --- a/cockatrice/src/interface/window_main.cpp +++ b/cockatrice/src/interface/window_main.cpp @@ -63,6 +63,7 @@ #include #include #include +#include #include #include #include @@ -112,7 +113,6 @@ #include #include #include -#include #include #include #include @@ -255,11 +255,7 @@ void MainWindow::actWatchReplay() void MainWindow::localGameEnded() { - // Defer deletion so the LocalClients (already scheduled with deleteLater() in TabSupervisor::stop) - // and any queued commands are processed before the server and its interfaces are destroyed. - if (localServer) { - localServer->deleteLater(); - } + delete localServer; localServer = nullptr; aConnect->setEnabled(true); @@ -346,7 +342,7 @@ void MainWindow::actViewLog() void MainWindow::actOpenSettingsFolder() { QString dir = SettingsCache::instance().getSettingsPath(); - QtUtils::openPathInFileManager(dir); + QDesktopServices::openUrl(QUrl::fromLocalFile(dir)); } void MainWindow::retranslateUi() @@ -928,28 +924,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(); @@ -959,22 +938,22 @@ void MainWindow::closeEvent(QCloseEvent *event) : tr("A card database update is still running: %1.\nQuitting now will cancel it.\n" "Are you sure you want to quit?") .arg(runningFor); - if (!QtUtils::confirm(this, tr("Are you sure?"), message, QMessageBox::No)) { + if (QMessageBox::question(this, tr("Are you sure?"), message, QMessageBox::Yes | QMessageBox::No, + QMessageBox::No) == 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(); } @@ -985,36 +964,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) { @@ -1537,13 +1492,13 @@ void MainWindow::refreshShortcuts() void MainWindow::actOpenCustomFolder() { QString dir = SettingsCache::instance().paths().getCustomPicsPath(); - QtUtils::openPathInFileManager(dir); + QDesktopServices::openUrl(QUrl::fromLocalFile(dir)); } void MainWindow::actOpenCustomsetsFolder() { QString dir = SettingsCache::instance().getCustomCardDatabasePath(); - QtUtils::openPathInFileManager(dir); + QDesktopServices::openUrl(QUrl::fromLocalFile(dir)); } void MainWindow::actAddCustomSet() 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/format.sh b/format.sh index fba76e93a..9e3a6069b 100755 --- a/format.sh +++ b/format.sh @@ -19,8 +19,7 @@ libcockatrice_* \ "oracle/src" \ "servatrice/src" \ "cmake/pch" \ -"tests" \ -"tools/installer-holder-probe") +"tests") exclude=("libcockatrice_rng/libcockatrice/rng/sfmt/" \ "libcockatrice_utility/libcockatrice/utility/peglib.h" \ "oracle/src/lzma/" \ @@ -33,7 +32,6 @@ cmakefile="CMakeLists.txt" cmakedir="cmake/.*\\.cmake" cmakeinclude=("cmake/gtest-CMakeLists.txt.in") scripts="*.sh" -scriptsrx="[^/]*\\.sh$" color="--" verbosity=0 sep="----------" @@ -255,7 +253,7 @@ if [[ $branch ]]; then if [[ $do_shell ]]; then shell_names=() for name in "${basenames[@]}"; do - filerx="(^|/)$scriptsrx" + filerx="(^|/)$scripts$" if [[ $name =~ $filerx ]]; then shell_names+=("$name") fi diff --git a/libcockatrice_card/CMakeLists.txt b/libcockatrice_card/CMakeLists.txt index d2e6ee83a..b3583522c 100644 --- a/libcockatrice_card/CMakeLists.txt +++ b/libcockatrice_card/CMakeLists.txt @@ -1,3 +1,7 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + set(HEADERS libcockatrice/card/card_info.h libcockatrice/card/card_info_comparator.h @@ -17,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..bd33f18e7 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, @@ -80,16 +67,10 @@ CardInfo::CardInfo(const QString &_name, reverseRelatedCards(_reverseRelatedCards), setsToPrintings(std::move(_sets)), uiAttributes(_uiAttributes), altNames(std::move(_altNames)) { - // propertiesBlob is materialized lazily on first query. The binary cache supplies simpleName - // and altNames to skip the Unicode normalization and the printing scan; the XML parsers leave - // them empty and get the same derived state the property-hash constructor computes. - if (simpleName.isEmpty()) { - simpleName = simplifyName(name); - } + // propertiesBlob is materialized lazily on first query; simpleName and + // altNames are supplied by the caller (binary cache). Only the set-name + // display list still depends on the current enabled-set state. refreshCachedSetNames(); - if (altNames.isEmpty()) { - refreshCachedAltNames(); - } } CardInfoPtr CardInfo::newInstance(const QString &_name) @@ -110,6 +91,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 +120,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 +163,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 +182,7 @@ void CardInfo::combineLegalities(const QHash &props) properties.insert(it.key(), it.value()); } } - notifyChanged(); + emit cardInfoChanged(smartThis); } void CardInfo::refreshCachedSets() @@ -269,7 +245,7 @@ QString CardInfo::simplifyName(const QString &name) QChar CardInfo::getColorChar() const { - const QString colors = getColors(); + QString colors = getColors(); switch (colors.size()) { case 0: return QChar(); @@ -288,13 +264,6 @@ void CardInfo::resetReverseRelatedCards2Me() reverseRelatedCardsToMe = QList(); } -void CardInfo::clearDirectRelations() -{ - relatedCards = QList(); - reverseRelatedCards = QList(); - reverseRelatedCardsToMe = QList(); -} - // Back-compatibility methods. Remove ASAP QString CardInfo::getCardType() const { diff --git a/libcockatrice_card/libcockatrice/card/card_info.h b/libcockatrice_card/libcockatrice/card/card_info.h index 5452ef98b..356da0546 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. @@ -121,12 +121,14 @@ public: QMap _localizedTexts = {}); /** - * @brief Constructs a CardInfo from a serialized properties blob. + * @brief Constructs a CardInfo from a cache snapshot with precomputed derived + * state. * - * Properties stay in @p _propertiesBlob so the QHash is not - * built at load time (it is materialized on first query). @p _simpleName and - * @p _altNames are filled in when empty; the binary cache supplies them to - * skip the Unicode normalization and the printing scan. + * Used by the binary cache reader to skip recomputing @p _simpleName and + * @p _altNames (which otherwise require a Unicode normalization and a full + * printing scan). Properties are supplied as a pre-serialized @p _propertiesBlob + * so the QHash is not built at load time (it is materialized on first + * query). * * @param _name The card name. * @param _text Rules text or description of the card. @@ -136,8 +138,8 @@ public: * @param _reverseRelatedCards Reverse relationships. * @param _sets Printing information per set. * @param _uiAttributes Attributes that affect display and game logic. - * @param _simpleName Precomputed simplified name; derived from @p _name when empty. - * @param _altNames Precomputed alternate names; derived from the printings when empty. + * @param _simpleName Precomputed simplified name. + * @param _altNames Precomputed alternate names. * @param _localizedNames Localized card names, keyed by language code. * @param _localizedTexts Localized rules text, keyed by language code. */ @@ -162,14 +164,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. } /** @@ -209,26 +210,20 @@ public: QMap _localizedTexts = {}); /** - * @brief Creates a new instance from a serialized properties blob. - * - * The properties stay in blob form, so the QHash is not - * built at load time (it is materialized on first query). The derived state - * is required so it is never recomputed here, and the overload cannot be a - * candidate for a shorter call that passes a property hash instead of a blob. + * @brief Creates a new instance from a cache snapshot with precomputed + * derived state. * * @param _name Name of the card. * @param _text Rules text or description. * @param _isToken Token flag. - * @param _propertiesBlob Serialized properties blob. + * @param _propertiesBlob Pre-serialized properties blob (as written by the cache). * @param _relatedCards Forward relationships. * @param _reverseRelatedCards Reverse relationships. * @param _sets Printing information per set. * @param _uiAttributes Attributes that affect display and game logic. - * @param _simpleName Precomputed simplified name; pass {} to derive it from - * @p _name. - * @param _altNames Precomputed alternate names; pass {} to derive them from - * the printings. - * @param _appendToSets When true, the card is appended to each of + * @param _simpleName Precomputed simplified name. + * @param _altNames Precomputed alternate names. + * @param _appendToSets When true (default), the card is appended to each of * its CardSets. Pass false when building cards in parallel so the * (non-thread-safe) set membership is populated in a later * single-threaded pass. @@ -246,20 +241,34 @@ public: UiAttributes _uiAttributes, QString _simpleName, QSet _altNames, - bool _appendToSets, + bool _appendToSets = true, QMap _localizedNames = {}, QMap _localizedTexts = {}); /** * @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 */ //@{ @@ -271,7 +280,7 @@ public: { return simpleName; } - const QSet &getAltNames() const + const QSet &getAltNames() { return altNames; } @@ -282,7 +291,7 @@ public: void setText(const QString &_text) { text = _text; - notifyChanged(); + emit cardInfoChanged(smartThis); } /** @@ -345,7 +354,7 @@ public: return; } localizedNames.insert(lang, _localizedName); - notifyChanged(); + emit cardInfoChanged(smartThis); } /** @@ -360,7 +369,7 @@ public: return; } localizedTexts.insert(lang, _localizedText); - notifyChanged(); + emit cardInfoChanged(smartThis); } /** @@ -380,34 +389,18 @@ public: } [[nodiscard]] QStringList getProperties() const { - return properties.keys(); + return getPropertiesHash().keys(); } - - /** - * @brief Returns the properties in their serialized blob form. - * - * Cheap while the properties were never materialized (the cache and the XML parsers pass the blob through). - */ - [[nodiscard]] QByteArray getPropertiesBlob() const - { - return properties.serialized(); - } - - /** - * @brief Returns the materialized property hash, forcing deserialization of the blob. - * - * Prefer getProperty(), hasProperty() or getPropertiesBlob(), which avoid building the hash. - */ [[nodiscard]] const QHash &getPropertiesHash() const; [[nodiscard]] QString getProperty(const QString &propertyName) const { - return properties.value(propertyName); + return getPropertiesHash().value(propertyName); } void setProperty(const QString &_name, const QString &_value); [[nodiscard]] bool hasProperty(const QString &propertyName) const { - return properties.contains(propertyName); + return getPropertiesHash().contains(propertyName); } [[nodiscard]] const SetToPrintingsMap &getSets() const { @@ -440,14 +433,6 @@ public: return result; } void resetReverseRelatedCards2Me(); - /** - * @brief Clears the raw relation lists without deleting the CardRelation objects. - * - * Callers that schedule the relations for deletion (e.g. CardDatabase::removeCard()) - * must use this to drop the now-dangling pointers from this CardInfo, which may - * outlive them through external shared pointers. - */ - void clearDirectRelations(); void addReverseRelatedCards2Me(CardRelation *cardRelation) { reverseRelatedCardsToMe.append(cardRelation); @@ -539,14 +524,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..7b0d5b2f9 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(); } @@ -198,10 +168,6 @@ void CardDatabase::unindexCard(const CardInfoPtr &card) cardRelation->deleteLater(); } - // The relations are only scheduled for deletion; the CardInfo may outlive - // them through an external shared pointer, so drop the raw pointers now. - card->clearDirectRelations(); - QMutexLocker locker(removeCardMutex); cards.remove(card->getName()); simpleNameCards.remove(card->getSimpleName()); @@ -305,15 +271,8 @@ void CardDatabase::swapInDatabaseData(CardDatabaseData data) for (auto *rel : card->getReverseRelatedCards2Me()) { rel->deleteLater(); } - // Replacement cards take over the database; old CardInfo objects may - // still be referenced by the UI, so clear their dangling relations. - 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/database/card_database_cache.cpp b/libcockatrice_card/libcockatrice/card/database/card_database_cache.cpp index 611b24ad8..4ab4d1fca 100644 --- a/libcockatrice_card/libcockatrice/card/database/card_database_cache.cpp +++ b/libcockatrice_card/libcockatrice/card/database/card_database_cache.cpp @@ -53,10 +53,13 @@ QString readString(QDataStream &in) // Stores a QHash as a single pre-serialized blob. The reader keeps the // blob as-is and materializes the QHash lazily on first query, which is // what removes the allocation storm from database load (see card_info.cpp / -// printing_info.cpp). Writing the already-serialized blob also keeps cache writes from -// materializing every card's property hash. -void writeBlob(QDataStream &out, const QByteArray &blob) +// printing_info.cpp). +void writeHashBlob(QDataStream &out, const QHash &h) { + QByteArray blob; + QDataStream blobOut(&blob, QIODevice::WriteOnly); + blobOut.setVersion(QDataStream::Qt_6_4); + blobOut << h; out << static_cast(blob.size()); out.writeRawData(blob.constData(), blob.size()); } @@ -153,7 +156,7 @@ CardRelation *readRelation(QDataStream &in) void writePrinting(QDataStream &out, const PrintingInfo &p) { writeString(out, p.getSet() ? p.getSet()->getShortName() : QString()); - writeBlob(out, p.getPropertiesBlob()); + writeHashBlob(out, p.getPropertiesHash()); } PrintingInfo readPrinting(QDataStream &in, const SetNameMap &sets) @@ -195,7 +198,7 @@ void writeCard(QDataStream &out, const CardInfoPtr &card) writeString(out, card->getName()); writeString(out, card->getText()); out << card->getIsToken(); - writeBlob(out, card->getPropertiesBlob()); + writeHashBlob(out, card->getPropertiesHash()); CardInfo::UiAttributes ui = card->getUiAttributes(); out << ui.cipt; @@ -207,7 +210,7 @@ void writeCard(QDataStream &out, const CardInfoPtr &card) // per-printing alt-name scan entirely. writeString(out, card->getSimpleName()); { - const QSet &altNames = card->getAltNames(); + const QSet altNames = card->getAltNames(); out << static_cast(altNames.size()); for (const QString &alt : altNames) { writeString(out, alt); @@ -215,7 +218,7 @@ void writeCard(QDataStream &out, const CardInfoPtr &card) } // setsToPrintings - const auto &sets = card->getSets(); + const SetToPrintingsMap sets = card->getSets(); out << static_cast(sets.size()); for (auto it = sets.constBegin(); it != sets.constEnd(); ++it) { writeString(out, it.key()); diff --git a/libcockatrice_card/libcockatrice/card/database/card_database_querier.cpp b/libcockatrice_card/libcockatrice/card/database/card_database_querier.cpp index 8df0d2e20..7c41daa3b 100644 --- a/libcockatrice_card/libcockatrice/card/database/card_database_querier.cpp +++ b/libcockatrice_card/libcockatrice/card/database/card_database_querier.cpp @@ -12,7 +12,6 @@ #include #include #include -#include #include CardDatabaseQuerier::CardDatabaseQuerier(QObject *_parent, @@ -53,7 +52,6 @@ CardInfoPtr CardDatabaseQuerier::getCardInfo(const QString &cardName) const QList CardDatabaseQuerier::getCardInfos(const QStringList &cardNames) const { QList cardInfos; - cardInfos.reserve(cardNames.size()); for (const QString &cardName : cardNames) { CardInfoPtr ptr = db->cards.value(cardName); if (ptr) { @@ -92,7 +90,6 @@ CardInfoPtr CardDatabaseQuerier::lookupCardByName(const QString &name) const QList CardDatabaseQuerier::getCards(const QList &cardRefs) const { QList cards; - cards.reserve(cardRefs.size()); for (const auto &cardRef : cardRefs) { ExactCard card = getCard(cardRef); if (card) { @@ -152,10 +149,10 @@ ExactCard CardDatabaseQuerier::getRandomCard() const return {}; } - const auto randomIndex = QRandomGenerator::global()->bounded(db->cards.size()); - auto it = db->cards.constBegin(); - std::advance(it, randomIndex); - CardInfoPtr randomCard = it.value(); + const auto keys = db->cards.keys(); + int randomIndex = QRandomGenerator::global()->bounded(keys.size()); + const QString &randomKey = keys.at(randomIndex); + CardInfoPtr randomCard = getCardInfo(randomKey); return ExactCard{randomCard, getPreferredPrinting(randomCard)}; } @@ -218,23 +215,20 @@ PrintingInfo CardDatabaseQuerier::getSpecificPrinting(const QString &cardName, return PrintingInfo(nullptr); } - const auto &setMap = cardInfo->getSets(); + SetToPrintingsMap setMap = cardInfo->getSets(); if (setMap.empty()) { return PrintingInfo(nullptr); } for (const auto &printings : setMap) { - for (const auto &cardInfoForSet : printings) { - const CardSetPtr &set = cardInfoForSet.getSet(); - if (!set) { - continue; - } + for (auto &cardInfoForSet : printings) { if (!collectorNumber.isEmpty()) { - if (set->getShortName() == setShortName && cardInfoForSet.getProperty("num") == collectorNumber) { + if (cardInfoForSet.getSet()->getShortName() == setShortName && + cardInfoForSet.getProperty("num") == collectorNumber) { return cardInfoForSet; } } else { - if (set->getShortName() == setShortName) { + if (cardInfoForSet.getSet()->getShortName() == setShortName) { return cardInfoForSet; } } @@ -290,10 +284,10 @@ PrintingInfo CardDatabaseQuerier::getPreferredPrinting(const CardInfoPtr &cardIn const auto &pinnedPrintingProviderId = prefs->getCardPreferenceOverride(cardInfo->getName()); if (!pinnedPrintingProviderId.isEmpty()) { - return findPrintingWithId(cardInfo, pinnedPrintingProviderId); + return getSpecificPrinting({cardInfo->getName(), pinnedPrintingProviderId}); } - const auto &setMap = cardInfo->getSets(); + SetToPrintingsMap setMap = cardInfo->getSets(); if (setMap.empty()) { return PrintingInfo(nullptr); } @@ -303,8 +297,8 @@ PrintingInfo CardDatabaseQuerier::getPreferredPrinting(const CardInfoPtr &cardIn SetPriorityComparator comparator; for (const auto &printings : setMap) { - for (const auto &printing : printings) { - const CardSetPtr ¤tSet = printing.getSet(); + for (auto &printing : printings) { + CardSetPtr currentSet = printing.getSet(); if (!preferredSet || comparator(currentSet, preferredSet)) { preferredSet = currentSet; preferredPrinting = printing; @@ -321,18 +315,17 @@ PrintingInfo CardDatabaseQuerier::getPreferredPrinting(const CardInfoPtr &cardIn QString CardDatabaseQuerier::getPreferredPrintingProviderId(const QString &cardName) const { - CardInfoPtr cardInfo = getCardInfo(cardName); - if (!cardInfo) { - return cardName; - } - - PrintingInfo preferredPrinting = getPreferredPrinting(cardInfo); - const QString &uuid = preferredPrinting.getUuid(); + PrintingInfo preferredPrinting = getPreferredPrinting(cardName); + QString uuid = preferredPrinting.getUuid(); if (!uuid.isEmpty()) { return uuid; } - return cardInfo->getName(); + CardInfoPtr defaultCardInfo = getCardInfo(cardName); + if (defaultCardInfo.isNull()) { + return cardName; + } + return defaultCardInfo->getName(); } QStringList CardDatabaseQuerier::getAllMainCardTypes() const @@ -346,8 +339,8 @@ QMap CardDatabaseQuerier::getAllMainCardTypesWithCount() const // produces an empty map, so the cache is only ever empty when it needs a // (trivially cheap) rebuild. if (mainCardTypeCountsCache.isEmpty()) { - for (const auto &card : db->cards) { - const QString type = card->getMainCardType(); + for (const auto &card : db->cards.values()) { + QString type = card->getMainCardType(); mainCardTypeCountsCache[type]++; } } @@ -358,13 +351,13 @@ QMap CardDatabaseQuerier::getAllMainCardTypesWithCount() const QMap CardDatabaseQuerier::getAllSubCardTypesWithCount() const { if (subCardTypeCountsCache.isEmpty()) { - for (const auto &card : db->cards) { - const QString type = card->getCardType(); + for (const auto &card : db->cards.values()) { + QString type = card->getCardType(); - const QStringList parts = type.split(" — "); + QStringList parts = type.split(" — "); if (parts.size() > 1) { // Ensure there are subtypes - const QStringList subtypes = parts[1].split(" ", Qt::SkipEmptyParts); + QStringList subtypes = parts[1].split(" ", Qt::SkipEmptyParts); for (const QString &subtype : subtypes) { subCardTypeCountsCache[subtype]++; @@ -384,13 +377,13 @@ FormatRulesPtr CardDatabaseQuerier::getFormat(const QString &formatName) const QMap CardDatabaseQuerier::getAllFormatsWithCount() const { if (formatsCountCache.isEmpty()) { - static const QString formatPrefix = QStringLiteral("format-"); + for (const auto &card : db->cards.values()) { + QStringList allProps = card->getProperties(); - for (const auto &card : db->cards) { - const auto &properties = card->getPropertiesHash(); - for (auto it = properties.constBegin(); it != properties.constEnd(); ++it) { - if (it.key().startsWith(formatPrefix)) { - formatsCountCache[it.key().mid(formatPrefix.size())]++; + for (const QString &prop : allProps) { + if (prop.startsWith("format-")) { + QString formatName = prop.mid(QStringLiteral("format-").size()); + formatsCountCache[formatName]++; } } } diff --git a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp index d40c1c063..b17eb8b5e 100644 --- a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp @@ -268,7 +268,7 @@ void CockatriceXml3Parser::loadCardsFromXml(QXmlStreamReader &xml) if (attrs.hasAttribute("rarity")) { printingProps.insert("rarity", attrs.value("rarity").toString()); } - PrintingInfo setInfo(set, LazyPropertiesHash::serializedFrom(printingProps)); + PrintingInfo setInfo(set, LazyPropertiesHash(printingProps)); _sets[setName].append(setInfo); } // related cards @@ -325,9 +325,8 @@ void CockatriceXml3Parser::loadCardsFromXml(QXmlStreamReader &xml) .landscapeOrientation = landscapeOrientation, .tableRow = tableRow, .upsideDownArt = upsideDown}; - CardInfoPtr newCard = - CardInfo::newInstance(name, text, isToken, LazyPropertiesHash::serialize(properties), relatedCards, - reverseRelatedCards, _sets, attributes, {}, {}, true); + CardInfoPtr newCard = CardInfo::newInstance(name, text, isToken, properties, relatedCards, + reverseRelatedCards, _sets, attributes); if (targetData) { if (auto existing = targetData->cards.value(name)) { for (const auto &printings : newCard->getSets()) { @@ -402,7 +401,7 @@ static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardInfoPtr &in } // sets - const auto &setMap = info->getSets(); + const SetToPrintingsMap setMap = info->getSets(); for (const auto &printings : setMap) { for (const PrintingInfo &set : printings) { xml.writeStartElement("set"); diff --git a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp index 3ee1e808e..4d975e533 100644 --- a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp @@ -383,7 +383,7 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml) } printingProps.insert(attrName, attr.value().toString()); } - PrintingInfo printingInfo(set, LazyPropertiesHash::serializedFrom(printingProps)); + PrintingInfo printingInfo(set, LazyPropertiesHash(printingProps)); _sets[setName].append(printingInfo); } // related cards @@ -450,9 +450,9 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml) .landscapeOrientation = landscapeOrientation, .tableRow = tableRow, .upsideDownArt = upsideDown}; - CardInfoPtr newCard = CardInfo::newInstance(name, text, isToken, LazyPropertiesHash::serialize(properties), - relatedCards, reverseRelatedCards, _sets, attributes, {}, {}, - true, std::move(localizedNames), std::move(localizedTexts)); + CardInfoPtr newCard = + CardInfo::newInstance(name, text, isToken, properties, relatedCards, reverseRelatedCards, _sets, + attributes, std::move(localizedNames), std::move(localizedTexts)); if (targetData) { // Mirror CardDatabase::addCard: if a card with this name already // exists, merge the new printings into it instead of replacing. diff --git a/libcockatrice_card/libcockatrice/card/lazy_properties_hash.cpp b/libcockatrice_card/libcockatrice/card/lazy_properties_hash.cpp index 3798bcd62..a12de469b 100644 --- a/libcockatrice_card/libcockatrice/card/lazy_properties_hash.cpp +++ b/libcockatrice_card/libcockatrice/card/lazy_properties_hash.cpp @@ -1,168 +1,8 @@ #include "lazy_properties_hash.h" #include -#include #include - -namespace -{ -constexpr quint32 NULL_STRING_LENGTH = 0xFFFFFFFFu; - -/** - * @brief Reads a QDataStream-serialized QString length at @p pos, advancing past the length field. - * - * QDataStream writes a QString as a big-endian quint32 byte length followed by UTF-16 code units (also big-endian), - * or 0xFFFFFFFF for a null string. Parsing at the byte level lets us skip and compare properties without - * constructing a QString for every entry. - */ -bool readBlobLength(const QByteArray &blob, int &pos, quint32 &length) -{ - if (pos < 0 || static_cast(pos) + 4 > blob.size()) { - return false; - } - - const auto *bytes = reinterpret_cast(blob.constData()) + pos; - length = (quint32(bytes[0]) << 24) | (quint32(bytes[1]) << 16) | (quint32(bytes[2]) << 8) | quint32(bytes[3]); - pos += 4; - - if (length == NULL_STRING_LENGTH) { - length = 0; - return true; - } - - if (length > static_cast(blob.size() - pos)) { - return false; - } - - return true; -} - -/** - * @brief Compares the serialized bytes at @p pos with @p key without allocating a QString. - */ -bool blobKeyEquals(const QByteArray &blob, int pos, quint32 length, const QString &key) -{ - if (length != static_cast(key.size()) * 2) { - return false; - } - - const auto *bytes = reinterpret_cast(blob.constData()) + pos; - for (int i = 0; i < key.size(); ++i) { - const ushort codeUnit = static_cast((bytes[2 * i] << 8) | bytes[2 * i + 1]); - if (key.at(i).unicode() != codeUnit) { - return false; - } - } - - return true; -} - -/** - * @brief Builds a QString from the serialized UTF-16BE bytes at @p pos. - */ -QString blobString(const QByteArray &blob, int pos, quint32 length) -{ - const int codeUnits = static_cast(length / 2); - QString result; - result.resize(codeUnits); - - const auto *bytes = reinterpret_cast(blob.constData()) + pos; - for (int i = 0; i < codeUnits; ++i) { - result[i] = QChar(static_cast((bytes[2 * i] << 8) | bytes[2 * i + 1])); - } - - return result; -} -} // namespace - -LazyPropertiesHash::BlobReader::BlobReader(const QByteArray &_blob) : blob(_blob) -{ - if (blob.size() < 4) { - return; - } - - const auto *bytes = reinterpret_cast(blob.constData()); - remaining = (quint32(bytes[0]) << 24) | (quint32(bytes[1]) << 16) | (quint32(bytes[2]) << 8) | quint32(bytes[3]); - valid = true; -} - -bool LazyPropertiesHash::BlobReader::next(int &keyPos, quint32 &keyLength, int &valuePos, quint32 &valueLength) -{ - if (!valid || remaining == 0) { - return false; - } - - if (!readBlobLength(blob, pos, keyLength)) { - valid = false; - return false; - } - keyPos = pos; - pos += static_cast(keyLength); - - if (!readBlobLength(blob, pos, valueLength)) { - valid = false; - return false; - } - valuePos = pos; - pos += static_cast(valueLength); - - --remaining; - return true; -} - -quint32 LazyPropertiesHash::BlobReader::entryCount() const -{ - return valid ? remaining : 0; -} - -QString LazyPropertiesHash::blobValue(const QByteArray &blob, const QString &key) -{ - BlobReader reader(blob); - int keyPos = 0; - int valuePos = 0; - quint32 keyLength = 0; - quint32 valueLength = 0; - while (reader.next(keyPos, keyLength, valuePos, valueLength)) { - if (blobKeyEquals(blob, keyPos, keyLength, key)) { - return blobString(blob, valuePos, valueLength); - } - } - - return {}; -} - -bool LazyPropertiesHash::blobContains(const QByteArray &blob, const QString &key) -{ - BlobReader reader(blob); - int keyPos = 0; - int valuePos = 0; - quint32 keyLength = 0; - quint32 valueLength = 0; - while (reader.next(keyPos, keyLength, valuePos, valueLength)) { - if (blobKeyEquals(blob, keyPos, keyLength, key)) { - return true; - } - } - - return false; -} - -QStringList LazyPropertiesHash::blobKeys(const QByteArray &blob) -{ - BlobReader reader(blob); - QStringList result; - result.reserve(static_cast(reader.entryCount())); - - int keyPos = 0; - int valuePos = 0; - quint32 keyLength = 0; - quint32 valueLength = 0; - while (reader.next(keyPos, keyLength, valuePos, valueLength)) { - result.append(blobString(blob, keyPos, keyLength)); - } - - return result; -} +#include LazyPropertiesHash::LazyPropertiesHash() : isMaterialized(true) { @@ -180,15 +20,15 @@ LazyPropertiesHash::LazyPropertiesHash(const QHash &properties LazyPropertiesHash::LazyPropertiesHash(const LazyPropertiesHash &other) { // since we do not allow dematerialization, we only need to lock if not materialized yet - if (other.isMaterialized.load(std::memory_order_acquire)) { + if (other.isMaterialized) { blob = other.blob; properties = other.properties; - isMaterialized.store(true, std::memory_order_relaxed); + isMaterialized = true; } else { QMutexLocker lock(&other.propertiesMutex); blob = other.blob; properties = other.properties; - isMaterialized.store(other.isMaterialized.load(std::memory_order_relaxed), std::memory_order_relaxed); + isMaterialized = false; } } @@ -199,15 +39,15 @@ LazyPropertiesHash &LazyPropertiesHash::operator=(const LazyPropertiesHash &othe } // since we do not allow dematerialization, we only need to lock if not materialized yet - if (other.isMaterialized.load(std::memory_order_acquire)) { + if (other.isMaterialized) { blob = other.blob; properties = other.properties; - isMaterialized.store(true, std::memory_order_relaxed); + isMaterialized = true; } else { QMutexLocker lock(&other.propertiesMutex); blob = other.blob; properties = other.properties; - isMaterialized.store(false, std::memory_order_relaxed); + isMaterialized = false; } return *this; @@ -215,15 +55,9 @@ LazyPropertiesHash &LazyPropertiesHash::operator=(const LazyPropertiesHash &othe void LazyPropertiesHash::ensureMaterialized() const { - // Materialization happens once and is never undone, so the common case can - // skip the mutex entirely once it has happened. - if (isMaterialized.load(std::memory_order_acquire)) { - return; - } - QMutexLocker lock(&propertiesMutex); - if (isMaterialized.load(std::memory_order_relaxed)) { + if (isMaterialized) { return; } @@ -235,60 +69,13 @@ void LazyPropertiesHash::ensureMaterialized() const blob.clear(); - isMaterialized.store(true, std::memory_order_release); -} - -QByteArray LazyPropertiesHash::serialize(const QHash &properties) -{ - QByteArray result; - QDataStream out(&result, QIODevice::WriteOnly); - out.setVersion(QDataStream::Qt_6_4); - out << properties; - return result; -} - -LazyPropertiesHash LazyPropertiesHash::serializedFrom(const QHash &properties) -{ - return LazyPropertiesHash(serialize(properties)); + isMaterialized = true; } QString LazyPropertiesHash::value(const QString &key) const { - return read([&key](const QHash &materialized) { return materialized.value(key); }, - [&key](const QByteArray &blob) { return LazyPropertiesHash::blobValue(blob, key); }); -} - -bool LazyPropertiesHash::contains(const QString &key) const -{ - return read([&key](const QHash &materialized) { return materialized.contains(key); }, - [&key](const QByteArray &blob) { return LazyPropertiesHash::blobContains(blob, key); }); -} - -int LazyPropertiesHash::size() const -{ - return read([](const QHash &materialized) { return static_cast(materialized.size()); }, - [](const QByteArray &blob) { return static_cast(BlobReader(blob).entryCount()); }); -} - -bool LazyPropertiesHash::isEmpty() const -{ - return size() == 0; -} - -QStringList LazyPropertiesHash::keys() const -{ - return read([](const QHash &materialized) { return QStringList(materialized.keys()); }, - [](const QByteArray &blob) { return blobKeys(blob); }); -} - -QByteArray LazyPropertiesHash::serialized() const -{ - return read([](const QHash &materialized) { return serialize(materialized); }, - [](const QByteArray &blob) { - // An empty blob is the "unknown/absent" form used by failed cache reads; give callers a - // well-formed serialization of the (empty) property map instead. - return blob.isEmpty() ? serialize({}) : blob; - }); + ensureMaterialized(); + return properties.value(key); } bool LazyPropertiesHash::insert(const QString &key, const QString &value) diff --git a/libcockatrice_card/libcockatrice/card/lazy_properties_hash.h b/libcockatrice_card/libcockatrice/card/lazy_properties_hash.h index c8170b7c2..04f5d0251 100644 --- a/libcockatrice_card/libcockatrice/card/lazy_properties_hash.h +++ b/libcockatrice_card/libcockatrice/card/lazy_properties_hash.h @@ -4,67 +4,24 @@ #include #include #include -#include #include -#include -#include /** * @brief A property map that can lazily deserialize blobs to avoid loading overhead. * - * Properties are stored as a pre-serialized blob (cheap to load), and individual values are read straight out of that - * blob, so the database load and ordinary queries avoid constructing thousands of QString per card. + * Properties are stored as a pre-serialized blob (cheap to load), and the QString is materialized on the first query, + * so the database load avoids constructing thousands of QString per card. * - * A full QHash is only built on the first insert() or getProperties() call. Once materialized, it - * cannot be unmaterialized; create a new LazyPropertiesHash to return to the blob form. + * Once the properties are materialized, it cannot be unmaterialized. + * If you want to reset the properties to an unmaterialized state, you should create a new LazyPropertiesHash. */ class LazyPropertiesHash { - /** - * @brief Iterates over the key/value pairs of a serialized QHash blob without materializing it. - * - * The blob layout is the one produced by QDataStream for QHash: a quint32 entry count followed by each entry's - * serialized key and value. - */ - class BlobReader - { - public: - explicit BlobReader(const QByteArray &_blob); - /** - * @brief Reads the next pair's offsets. - * @return False when there are no more pairs or the blob is malformed. - */ - bool next(int &keyPos, quint32 &keyLength, int &valuePos, quint32 &valueLength); - - [[nodiscard]] quint32 entryCount() const; - - private: - const QByteArray &blob; - int pos = 4; - quint32 remaining = 0; - bool valid = false; - }; - - /** - * @brief Looks up @p key in a serialized blob without materializing it. - */ - static QString blobValue(const QByteArray &blob, const QString &key); - - /** - * @brief Checks whether @p key is present in a serialized blob without materializing it. - */ - static bool blobContains(const QByteArray &blob, const QString &key); - - /** - * @brief Lists the keys of a serialized blob without materializing it. - */ - static QStringList blobKeys(const QByteArray &blob); - - mutable QByteArray blob; ///< Serialized properties (load form). - mutable QHash properties; ///< Materialized properties (query form). - mutable QMutex propertiesMutex; ///< Guards lazy materialization. - mutable std::atomic isMaterialized{false}; ///< Whether properties is valid. + mutable QByteArray blob; ///< Serialized properties (load form). + mutable QHash properties; ///< Materialized properties (query form). + mutable QMutex propertiesMutex; ///< Guards lazy materialization. + mutable bool isMaterialized = false; ///< Whether propertiesCache is valid. /** * @brief Materializes properties from blob if not already done. Clears blob afterward. @@ -72,32 +29,6 @@ class LazyPropertiesHash */ void ensureMaterialized() const; - /** - * @brief Runs the accessor for the current storage form. - * - * The common case skips the mutex once the properties are materialized. While they are - * still in blob form, the mutex is taken and @p onBlob reads the values straight out of - * the blob without building the hash, so ordinary queries stay allocation-light. - * - * @param onMaterialized Called with the materialized hash when it is available. - * @param onBlob Called with the blob when it is still the source of truth. - * @return Whatever the invoked accessor returns; both accessors must return the same type. - */ - template - auto read(MaterializedFn &&onMaterialized, BlobFn &&onBlob) const - { - if (isMaterialized.load(std::memory_order_acquire)) { - return onMaterialized(properties); - } - - QMutexLocker lock(&propertiesMutex); - if (isMaterialized.load(std::memory_order_relaxed)) { - return onMaterialized(properties); - } - - return onBlob(blob); - } - public: /** * @brief Default constructor. @@ -121,60 +52,12 @@ public: LazyPropertiesHash &operator=(const LazyPropertiesHash &other); /** - * @brief Serializes a property map into the blob form understood by this class. - * @param properties The properties to serialize - * @return The serialized blob - */ - static QByteArray serialize(const QHash &properties); - - /** - * @brief Creates an unmaterialized LazyPropertiesHash from a property map. - * - * Serializes @p properties once so queries read them from the blob form instead of first materializing a second - * copy of the hash. - * - * @param properties The properties to serialize - * @return An unmaterialized LazyPropertiesHash holding the serialized properties - */ - static LazyPropertiesHash serializedFrom(const QHash &properties); - - /** - * @brief Gets the value for a key, reading it from the blob when possible. + * @brief Gets the value from the materialized properties hash * @param key The key * @return The value, or an empty string if the key is not present */ QString value(const QString &key) const; - /** - * @brief Checks whether a key is present, reading the blob when possible. - * @param key The key - * @return True if the key is present - */ - bool contains(const QString &key) const; - - /** - * @brief Returns the number of properties. - */ - int size() const; - - /** - * @brief Returns whether there are no properties. - */ - bool isEmpty() const; - - /** - * @brief Returns the property names, reading them from the blob when possible. - */ - QStringList keys() const; - - /** - * @brief Returns the properties in serialized blob form. - * - * While the object is unmaterialized this is a cheap copy of the stored blob; after materialization the hash is - * serialized on demand. - */ - QByteArray serialized() const; - /** * @brief Inserts a value into the materialized properties hash * @param key The key @@ -184,7 +67,7 @@ public: bool insert(const QString &key, const QString &value); /** - * @brief Gets a view of the materialized properties hash, materializing it if needed. + * @brief Gets a view of the materialized properties hash. * @return The properties hash */ const QHash &getProperties() const; diff --git a/libcockatrice_card/libcockatrice/card/printing/exact_card.h b/libcockatrice_card/libcockatrice/card/printing/exact_card.h index 83ba97bd0..ea49072fb 100644 --- a/libcockatrice_card/libcockatrice/card/printing/exact_card.h +++ b/libcockatrice_card/libcockatrice/card/printing/exact_card.h @@ -47,7 +47,7 @@ public: * * May be null if the ExactCard is empty. */ - [[nodiscard]] const CardInfoPtr &getCardPtr() const + [[nodiscard]] CardInfoPtr getCardPtr() const { return card; } @@ -57,7 +57,7 @@ public: * * May be empty if no specific printing was assigned. */ - [[nodiscard]] const PrintingInfo &getPrinting() const + [[nodiscard]] PrintingInfo getPrinting() const { return printing; } diff --git a/libcockatrice_card/libcockatrice/card/printing/printing_info.cpp b/libcockatrice_card/libcockatrice/card/printing/printing_info.cpp index 9bc62a07c..654efac5e 100644 --- a/libcockatrice_card/libcockatrice/card/printing/printing_info.cpp +++ b/libcockatrice_card/libcockatrice/card/printing/printing_info.cpp @@ -12,32 +12,15 @@ void PrintingInfo::setProperty(const QString &_name, const QString &_value) properties.insert(_name, _value); } -bool PrintingInfo::operator==(const PrintingInfo &other) const -{ - if (set != other.set || properties.size() != other.properties.size()) { - return false; - } - - // Both property maps are usually still in blob form here, so compare key by key instead of - // materializing two full hashes just to compare them. - for (const QString &key : properties.keys()) { - if (!other.properties.contains(key) || properties.value(key) != other.properties.value(key)) { - return false; - } - } - - return true; -} - /** * Gets the uuid property of the printing, or an empty string if the property isn't present */ QString PrintingInfo::getUuid() const { - return getProperty("uuid"); + return getPropertiesHash().value("uuid"); } QString PrintingInfo::getFlavorName() const { - return getProperty("flavorName"); + return getPropertiesHash().value("flavorName"); } diff --git a/libcockatrice_card/libcockatrice/card/printing/printing_info.h b/libcockatrice_card/libcockatrice/card/printing/printing_info.h index dcfcc846b..038960e86 100644 --- a/libcockatrice_card/libcockatrice/card/printing/printing_info.h +++ b/libcockatrice_card/libcockatrice/card/printing/printing_info.h @@ -54,7 +54,10 @@ public: * @param other Another PrintingInfo to compare against. * @return True if both set and properties are equal, otherwise false. */ - bool operator==(const PrintingInfo &other) const; + bool operator==(const PrintingInfo &other) const + { + return this->set == other.set && this->getPropertiesHash() == other.getPropertiesHash(); + } /** * @brief check if the info is empty, as if default constructed. @@ -63,7 +66,7 @@ public: */ bool isEmpty() const { - return set == nullptr && properties.isEmpty(); + return set == nullptr && getPropertiesHash().isEmpty(); } private: @@ -88,22 +91,9 @@ public: */ [[nodiscard]] QStringList getProperties() const { - return properties.keys(); + return getPropertiesHash().keys(); } - /** - * @brief Returns the printing properties in their serialized blob form. - */ - [[nodiscard]] QByteArray getPropertiesBlob() const - { - return properties.serialized(); - } - - /** - * @brief Returns the materialized property hash, forcing deserialization of the blob. - * - * Prefer getProperty() or getPropertiesBlob(), which avoid building the hash. - */ [[nodiscard]] const QHash &getPropertiesHash() const { return properties.getProperties(); diff --git a/libcockatrice_card/libcockatrice/card/set/card_set.cpp b/libcockatrice_card/libcockatrice/card/set/card_set.cpp index a0f0f4578..e873eeeab 100644 --- a/libcockatrice_card/libcockatrice/card/set/card_set.cpp +++ b/libcockatrice_card/libcockatrice/card/set/card_set.cpp @@ -27,20 +27,17 @@ CardSetPtr CardSet::newInstance(ICardSetPriorityController *_priorityController, const Priority _priority) { CardSetPtr ptr(new CardSet(_priorityController, _shortName, _longName, _setType, _releaseDate, _priority)); + // ptr->setSmartPointer(ptr); return ptr; } QString CardSet::getCorrectedShortName() const { // For Windows machines. - static const QSet invalidFileNames = { - QStringLiteral("CON"), QStringLiteral("PRN"), QStringLiteral("AUX"), QStringLiteral("NUL"), - QStringLiteral("COM1"), QStringLiteral("COM2"), QStringLiteral("COM3"), QStringLiteral("COM4"), - QStringLiteral("COM5"), QStringLiteral("COM6"), QStringLiteral("COM7"), QStringLiteral("COM8"), - QStringLiteral("COM9"), QStringLiteral("LPT1"), QStringLiteral("LPT2"), QStringLiteral("LPT3"), - QStringLiteral("LPT4"), QStringLiteral("LPT5"), QStringLiteral("LPT6"), QStringLiteral("LPT7"), - QStringLiteral("LPT8"), QStringLiteral("LPT9"), - }; + QSet invalidFileNames; + invalidFileNames << "CON" << "PRN" << "AUX" << "NUL" << "COM1" << "COM2" << "COM3" << "COM4" << "COM5" << "COM6" + << "COM7" << "COM8" << "COM9" << "LPT1" << "LPT2" << "LPT3" << "LPT4" << "LPT5" << "LPT6" << "LPT7" + << "LPT8" << "LPT9"; return invalidFileNames.contains(shortName) ? shortName + "_" : shortName; } diff --git a/libcockatrice_card/libcockatrice/card/set/card_set.h b/libcockatrice_card/libcockatrice/card/set/card_set.h index b995ac88e..b9ac74e2d 100644 --- a/libcockatrice_card/libcockatrice/card/set/card_set.h +++ b/libcockatrice_card/libcockatrice/card/set/card_set.h @@ -239,7 +239,7 @@ public: */ [[nodiscard]] bool getIsKnownIgnored() const { - return longName.isEmpty() && setType.isEmpty() && !releaseDate.isValid(); + return longName.length() + setType.length() + releaseDate.toString().length() == 0; } }; diff --git a/libcockatrice_deck_list/CMakeLists.txt b/libcockatrice_deck_list/CMakeLists.txt index 3e7d522c2..e61a5702c 100644 --- a/libcockatrice_deck_list/CMakeLists.txt +++ b/libcockatrice_deck_list/CMakeLists.txt @@ -1,32 +1,34 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + set(HEADERS libcockatrice/deck_list/tree/abstract_deck_list_card_node.h 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 070c04637..2d45b8e5b 100644 --- a/libcockatrice_filters/CMakeLists.txt +++ b/libcockatrice_filters/CMakeLists.txt @@ -1,10 +1,16 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + set(HEADERS libcockatrice/filters/filter_card.h libcockatrice/filters/filter_string.h 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_filters/libcockatrice/filters/filter_card.h b/libcockatrice_filters/libcockatrice/filters/filter_card.h index dbeffd18f..e6a37ee40 100644 --- a/libcockatrice_filters/libcockatrice/filters/filter_card.h +++ b/libcockatrice_filters/libcockatrice/filters/filter_card.h @@ -54,7 +54,7 @@ private: enum Attr a; public: - CardFilter(const QString &term, Type type, Attr attr) : trm(term), t(type), a(attr) + CardFilter(QString &term, Type type, Attr attr) : trm(term), t(type), a(attr) { } diff --git a/libcockatrice_filters/libcockatrice/filters/filter_string.cpp b/libcockatrice_filters/libcockatrice/filters/filter_string.cpp index 081ada824..0acc0eca0 100644 --- a/libcockatrice_filters/libcockatrice/filters/filter_string.cpp +++ b/libcockatrice_filters/libcockatrice/filters/filter_string.cpp @@ -18,7 +18,6 @@ #include #include #include -#include #include #include #include @@ -162,28 +161,30 @@ static void setupParserRules() if (sv.choice() == 0) { auto matcher = std::any_cast(sv[0]); return [=](const CardData &x) -> bool { - const auto &sets = x->getSets(); - return std::any_of(sets.keyValueBegin(), sets.keyValueEnd(), - [&matcher](const auto &set) { return matcher(set.first); }); + QList sets = x->getSets().keys(); + + auto matchesSet = [&matcher](const QString &set) { return matcher(set); }; + return std::any_of(sets.begin(), sets.end(), matchesSet); }; } if (sv.choice() == 1) { auto matcher = std::any_cast(sv[0]); return [=](const CardData &x) -> bool { - const auto &sets = x->getSets(); - return std::none_of(sets.keyValueBegin(), sets.keyValueEnd(), - [&matcher](const auto &set) { return matcher(set.first); }); + QList sets = x->getSets().keys(); + + auto matchesSet = [&matcher](const QString &set) { return matcher(set); }; + return std::none_of(sets.begin(), sets.end(), matchesSet); }; } auto matcher = std::any_cast(sv[0]); return [=](const CardData &x) -> bool { - const auto &sets = x->getSets(); - return std::any_of(sets.constBegin(), sets.constEnd(), [&matcher](const auto &printings) { - return std::any_of(printings.begin(), printings.end(), [&matcher](const PrintingInfo &printing) { - const CardSetPtr set = printing.getSet(); - return set && set->getEnabled() && matcher(set->getReleaseDate().toJulianDay()); - }); + const auto &sets = x->getSets().values(); + auto matchesSet = [&](const PrintingInfo &printing) { + return printing.getSet()->getEnabled() && matcher(printing.getSet()->getReleaseDate().toJulianDay()); + }; + return std::any_of(sets.begin(), sets.end(), [&](const auto &printings) { + return std::any_of(printings.begin(), printings.end(), matchesSet); }); }; }; @@ -206,14 +207,15 @@ static void setupParserRules() search["RarityQuery"] = [](const peg::SemanticValues &sv) -> Filter { const auto rarity = std::any_cast(sv[0]); return [=](const CardData &x) -> bool { + QList infos; for (const auto &printings : x->getSets()) { for (const auto &printing : printings) { - if (rarity == printing.getProperty("rarity")) { - return true; - } + infos.append(printing); } } - return false; + + auto matchesRarity = [&rarity](const PrintingInfo &info) { return rarity == info.getProperty("rarity"); }; + return std::any_of(infos.begin(), infos.end(), matchesRarity); }; }; @@ -409,7 +411,7 @@ static void setupParserRules() const bool identity = sv.tokens[0].empty() || sv.tokens[0][0] != 'i'; if (sv.tokens[1][0] == ':') { return [=](const CardData &x) { - const QString match = identity ? x->getColors() : x->getProperty("coloridentity"); + QString match = identity ? x->getColors() : x->getProperty("coloridentity"); if (parts.contains("m") && match.length() < 2) { return false; } @@ -427,7 +429,7 @@ static void setupParserRules() } return [=](const CardData &x) { - const QString match = identity ? x->getColors() : x->getProperty("colorIdentity"); + QString match = identity ? x->getColors() : x->getProperty("colorIdentity"); if (parts.contains("m") && match.length() < 2) { return false; } @@ -453,37 +455,25 @@ static void setupParserRules() }; search["PowerQuery"] = [](const peg::SemanticValues &sv) -> Filter { const auto matcher = std::any_cast(sv[0]); - return [=](const CardData &x) -> bool { - const QString &powTough = x->getPowTough(); - const qsizetype slash = powTough.indexOf('/'); - return matcher(slash < 0 ? powTough.toInt() : QStringView(powTough).left(slash).toInt()); - }; + return [=](const CardData &x) -> bool { return matcher(x->getPowTough().split("/")[0].toInt()); }; }; search["ToughnessQuery"] = [](const peg::SemanticValues &sv) -> Filter { const auto matcher = std::any_cast(sv[0]); return [=](const CardData &x) -> bool { - const QString &powTough = x->getPowTough(); - const qsizetype slash = powTough.indexOf('/'); - return matcher(slash < 0 ? 0 : QStringView(powTough).mid(slash + 1).toInt()); + auto parts = x->getPowTough().split("/"); + return matcher(parts.length() == 2 ? parts[1].toInt() : 0); }; }; search["FieldQuery"] = [](const peg::SemanticValues &sv) -> Filter { const auto field = std::any_cast(sv[0]); if (sv.choice() == 0) { const auto matcher = std::any_cast(sv[1]); - return [=](const CardData &x) -> bool { - const auto &properties = x->getPropertiesHash(); - const auto it = properties.constFind(field); - return it != properties.constEnd() && matcher(it.value()); - }; + return [=](const CardData &x) -> bool { return x->hasProperty(field) && matcher(x->getProperty(field)); }; } const auto matcher = std::any_cast(sv[1]); - return [=](const CardData &x) -> bool { - const auto &properties = x->getPropertiesHash(); - const auto it = properties.constFind(field); - return it != properties.constEnd() && matcher(it.value().toInt()); - }; + return + [=](const CardData &x) -> bool { return x->hasProperty(field) && matcher(x->getProperty(field).toInt()); }; }; search["GenericQuery"] = [](const peg::SemanticValues &sv) -> Filter { const auto matcher = std::any_cast(sv[0]); diff --git a/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp b/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp index f12f87181..742b44e97 100644 --- a/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp +++ b/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp @@ -9,16 +9,9 @@ #include #include #include -#include #include #include -namespace -{ -/** Separator between the main and sub types of a card type ("Creature — Elf"). */ -const QString MAIN_SUB_TYPE_SEPARATOR = QStringLiteral(" — "); -} // namespace - template FilterTreeNode *FilterTreeBranch::nodeAt(int i) const { return (childNodes.size() > i) ? childNodes.at(i) : nullptr; @@ -115,7 +108,7 @@ FilterTreeNode *FilterItemList::termNode(const QString &term) return childNodes.at(i); } -bool FilterItemList::testTypeAnd(const CardInfoPtr &info, +bool FilterItemList::testTypeAnd(const CardInfoPtr info, CardFilter::Attr attr, const CardSearchLanguage &searchLanguage) const { @@ -132,7 +125,7 @@ bool FilterItemList::testTypeAnd(const CardInfoPtr &info, return true; } -bool FilterItemList::testTypeAndNot(const CardInfoPtr &info, +bool FilterItemList::testTypeAndNot(const CardInfoPtr info, CardFilter::Attr attr, const CardSearchLanguage &searchLanguage) const { @@ -140,7 +133,7 @@ bool FilterItemList::testTypeAndNot(const CardInfoPtr &info, return !testTypeOr(info, attr, searchLanguage); } -bool FilterItemList::testTypeOr(const CardInfoPtr &info, +bool FilterItemList::testTypeOr(const CardInfoPtr info, CardFilter::Attr attr, const CardSearchLanguage &searchLanguage) const { @@ -163,7 +156,7 @@ bool FilterItemList::testTypeOr(const CardInfoPtr &info, return noChildEnabledChild; } -bool FilterItemList::testTypeOrNot(const CardInfoPtr &info, +bool FilterItemList::testTypeOrNot(const CardInfoPtr info, CardFilter::Attr attr, const CardSearchLanguage &searchLanguage) const { @@ -171,118 +164,7 @@ bool FilterItemList::testTypeOrNot(const CardInfoPtr &info, return !testTypeAnd(info, attr, searchLanguage); } -FilterItem::FilterItem(QString trm, FilterItemList *parent) : p(parent), term(std::move(trm)) -{ - lowercaseTerm = term.toLower(); - - switch (attr()) { - case CardFilter::AttrColor: { - QString converted = term.trimmed(); - - converted.replace("green", "g", Qt::CaseInsensitive); - converted.replace("grn", "g", Qt::CaseInsensitive); - converted.replace("blue", "u", Qt::CaseInsensitive); - converted.replace("blu", "u", Qt::CaseInsensitive); - converted.replace("black", "b", Qt::CaseInsensitive); - converted.replace("blk", "b", Qt::CaseInsensitive); - converted.replace("red", "r", Qt::CaseInsensitive); - converted.replace("white", "w", Qt::CaseInsensitive); - converted.replace("wht", "w", Qt::CaseInsensitive); - converted.replace("colorless", "c", Qt::CaseInsensitive); - converted.replace("colourless", "c", Qt::CaseInsensitive); - converted.replace("none", "c", Qt::CaseInsensitive); - - converted.replace(QString(" "), QString(""), Qt::CaseInsensitive); - - convertedColorTerm = converted; - break; - } - case CardFilter::AttrRarity: { - QString converted = term.trimmed(); - - /* - * The purpose of this loop is to only apply one of the replacement - * policies and then escape. If we attempt to layer them on top of - * each other, we will get awkward results (i.e. comythic rare mythic rareon) - * Conditional statement will exit once a case is successful in - * replacement OR we go through all possible cases. - * Will also need to replace just "mythic" - */ - for (int i = 0; converted.length() <= 3 && i <= 6; i++) { - switch (i) { - case 0: - converted.replace("mr", "mythic", Qt::CaseInsensitive); - break; - case 1: - converted.replace("m r", "mythic", Qt::CaseInsensitive); - break; - case 2: - converted.replace("m", "mythic", Qt::CaseInsensitive); - break; - case 3: - converted.replace("c", "common", Qt::CaseInsensitive); - break; - case 4: - converted.replace("u", "uncommon", Qt::CaseInsensitive); - break; - case 5: - converted.replace("r", "rare", Qt::CaseInsensitive); - break; - case 6: - converted.replace("s", "special", Qt::CaseInsensitive); - break; - default: - break; - } - } - - convertedRarityTerm = converted; - break; - } - case CardFilter::AttrManaCost: - sortedManaCostTerm = term.toUpper(); - // Sort the mana cost so it will be easy to find - std::sort(sortedManaCostTerm.begin(), sortedManaCostTerm.end()); - break; - case CardFilter::AttrLoyalty: - uppercaseLoyaltyTerm = term.trimmed().toUpper(); - break; - default: - break; - } - - // Parse the numeric relation used by cmc, power, toughness and loyalty once. - // If the term is not numeric, it probably starts with an operator. - const QString trimmedTerm = term.trimmed(); - bool converted = false; - const int value = trimmedTerm.toInt(&converted); - if (converted) { - numericOperator = NumericOperator::Equal; - numericOperand = value; - } else if (trimmedTerm.size() > 1) { - if (trimmedTerm.at(1) == '=') { - numericOperand = trimmedTerm.mid(2).toInt(); - if (trimmedTerm.startsWith('<')) { - numericOperator = NumericOperator::LessEqual; - } else if (trimmedTerm.startsWith('>')) { - numericOperator = NumericOperator::GreaterEqual; - } else { - numericOperator = NumericOperator::Equal; - } - } else { - numericOperand = trimmedTerm.mid(1).toInt(); - if (trimmedTerm.startsWith('<')) { - numericOperator = NumericOperator::Less; - } else if (trimmedTerm.startsWith('>')) { - numericOperator = NumericOperator::Greater; - } else if (trimmedTerm.startsWith('=')) { - numericOperator = NumericOperator::Equal; - } - } - } -} - -bool FilterItem::acceptName(const CardInfoPtr &info, const CardSearchLanguage &searchLanguage) const +bool FilterItem::acceptName(const CardInfoPtr info, const CardSearchLanguage &searchLanguage) const { const QString &englishName = info->getName(); const QString &localizedName = info->getLocalizedName(searchLanguage.language); @@ -305,7 +187,7 @@ bool FilterItem::acceptName(const CardInfoPtr &info, const CardSearchLanguage &s return false; } -bool FilterItem::acceptNameExact(const CardInfoPtr &info, const CardSearchLanguage &searchLanguage) const +bool FilterItem::acceptNameExact(const CardInfoPtr info, const CardSearchLanguage &searchLanguage) const { const QString &englishName = info->getName(); const QString &localizedName = info->getLocalizedName(searchLanguage.language); @@ -328,42 +210,44 @@ bool FilterItem::acceptNameExact(const CardInfoPtr &info, const CardSearchLangua return false; } -bool FilterItem::acceptType(const CardInfoPtr &info) const +bool FilterItem::acceptType(const CardInfoPtr info) const { return info->getCardType().contains(term, Qt::CaseInsensitive); } -bool FilterItem::acceptMainType(const CardInfoPtr &info) const +bool FilterItem::acceptMainType(const CardInfoPtr info) const { - const QString type = info->getCardType(); - const qsizetype separator = type.indexOf(MAIN_SUB_TYPE_SEPARATOR); - if (separator >= 0) { - return QStringView(type).left(separator).contains(term, Qt::CaseInsensitive); - } - return type.contains(term, Qt::CaseInsensitive); + const QStringList typeParts = info->getCardType().split(" — "); + return !typeParts.isEmpty() && typeParts[0].contains(term, Qt::CaseInsensitive); } -bool FilterItem::acceptSubType(const CardInfoPtr &info) const +bool FilterItem::acceptSubType(const CardInfoPtr info) const { - const QString type = info->getCardType(); - const qsizetype separator = type.indexOf(MAIN_SUB_TYPE_SEPARATOR); - if (separator < 0) { - return false; - } - - const qsizetype subTypeStart = separator + MAIN_SUB_TYPE_SEPARATOR.size(); - const qsizetype nextSeparator = type.indexOf(MAIN_SUB_TYPE_SEPARATOR, subTypeStart); - const QStringView subType = nextSeparator < 0 ? QStringView(type).mid(subTypeStart) - : QStringView(type).mid(subTypeStart, nextSeparator - subTypeStart); - return subType.contains(term, Qt::CaseInsensitive); + const QStringList typeParts = info->getCardType().split(" — "); + return typeParts.size() > 1 && typeParts[1].contains(term, Qt::CaseInsensitive); } -bool FilterItem::acceptColor(const CardInfoPtr &info) const +bool FilterItem::acceptColor(const CardInfoPtr info) const { - const QString colors = info->getColors(); + QString converted_term = term.trimmed(); + + converted_term.replace("green", "g", Qt::CaseInsensitive); + converted_term.replace("grn", "g", Qt::CaseInsensitive); + converted_term.replace("blue", "u", Qt::CaseInsensitive); + converted_term.replace("blu", "u", Qt::CaseInsensitive); + converted_term.replace("black", "b", Qt::CaseInsensitive); + converted_term.replace("blk", "b", Qt::CaseInsensitive); + converted_term.replace("red", "r", Qt::CaseInsensitive); + converted_term.replace("white", "w", Qt::CaseInsensitive); + converted_term.replace("wht", "w", Qt::CaseInsensitive); + converted_term.replace("colorless", "c", Qt::CaseInsensitive); + converted_term.replace("colourless", "c", Qt::CaseInsensitive); + converted_term.replace("none", "c", Qt::CaseInsensitive); + + converted_term.replace(QString(" "), QString(""), Qt::CaseInsensitive); // Colorless card filter - if (convertedColorTerm.toLower() == "c" && colors.length() < 1) { + if (converted_term.toLower() == "c" && info->getColors().length() < 1) { return true; } @@ -372,16 +256,16 @@ bool FilterItem::acceptColor(const CardInfoPtr &info) const * then we should match all of them to the card's colors */ int match_count = 0; - for (const QChar &color : convertedColorTerm) { - if (colors.contains(color, Qt::CaseInsensitive)) { + for (auto &it : converted_term) { + if (info->getColors().contains(it, Qt::CaseInsensitive)) { match_count++; } } - return match_count == convertedColorTerm.length(); + return match_count == converted_term.length(); } -bool FilterItem::acceptText(const CardInfoPtr &info, const CardSearchLanguage &searchLanguage) const +bool FilterItem::acceptText(const CardInfoPtr info, const CardSearchLanguage &searchLanguage) const { const QString &englishText = info->getText(); const QString &localizedText = info->getLocalizedText(searchLanguage.language); @@ -404,47 +288,51 @@ bool FilterItem::acceptText(const CardInfoPtr &info, const CardSearchLanguage &s return false; } -bool FilterItem::acceptSet(const CardInfoPtr &info) const +bool FilterItem::acceptSet(const CardInfoPtr info) const { + bool status = false; for (const auto &printings : info->getSets()) { - for (const auto &printing : printings) { - const CardSetPtr set = printing.getSet(); - if (set && (set->getShortName().compare(term, Qt::CaseInsensitive) == 0 || - set->getLongName().compare(term, Qt::CaseInsensitive) == 0)) { - return true; + for (const auto &set : printings) { + if (set.getSet()->getShortName().compare(term, Qt::CaseInsensitive) == 0 || + set.getSet()->getLongName().compare(term, Qt::CaseInsensitive) == 0) { + status = true; + break; } } } - return false; + return status; } -bool FilterItem::acceptManaCost(const CardInfoPtr &info) const +bool FilterItem::acceptManaCost(const CardInfoPtr info) const { + QString partialCost = term.toUpper(); + + // Sort the mana cost so it will be easy to find + std::sort(partialCost.begin(), partialCost.end()); + // Try to seperate the mana cost in case it's a split card // if it's not a split card the loop will run only once - for (const QString &manaCostPart : info->getManaCost().split("//")) { - QString fullManaCost = manaCostPart; + for (QString fullManaCost : info->getManaCost().split("//")) { std::sort(fullManaCost.begin(), fullManaCost.end()); // If the partial is found in the full, return true - if (fullManaCost.contains(sortedManaCostTerm)) { + if (fullManaCost.contains(partialCost)) { return true; } } return false; } -bool FilterItem::acceptCmc(const CardInfoPtr &info) const +bool FilterItem::acceptCmc(const CardInfoPtr info) const { - const QString cmc = info->getCmc(); bool convertSuccess; - int cmcInt = cmc.toInt(&convertSuccess); + int cmcInt = info->getCmc().toInt(&convertSuccess); // if conversion failed, check for the "//" separator used in split cards if (!convertSuccess) { int cmcSum = 0; - for (const QString &cmcPart : cmc.split("//")) { - cmcInt = cmcPart.toInt(); + for (const QString &cmc : info->getCmc().split("//")) { + cmcInt = cmc.toInt(); cmcSum += cmcInt; if (relationCheck(cmcInt)) { return true; @@ -456,36 +344,39 @@ bool FilterItem::acceptCmc(const CardInfoPtr &info) const } } -bool FilterItem::acceptFormat(const CardInfoPtr &info) const +bool FilterItem::acceptFormat(const CardInfoPtr info) const { - return info->getLegalityProp(lowercaseTerm) == "legal"; + return info->getLegalityProp(term.toLower()) == "legal"; } -bool FilterItem::acceptLoyalty(const CardInfoPtr &info) const +bool FilterItem::acceptLoyalty(const CardInfoPtr info) const { - const QString loyaltyString = info->getLoyalty(); - if (loyaltyString.isEmpty()) { + if (info->getLoyalty().isEmpty()) { return false; + } else { + bool success; + // if loyalty can't be converted to "int" it must be "X" + int loyalty = info->getLoyalty().toInt(&success); + if (success) { + return relationCheck(loyalty); + } else { + return term.trimmed().toUpper() == info->getLoyalty(); + } } - - bool success; - // if loyalty can't be converted to "int" it must be "X" - int loyalty = loyaltyString.toInt(&success); - if (success) { - return relationCheck(loyalty); - } - return uppercaseLoyaltyTerm == loyaltyString; } -bool FilterItem::acceptPowerToughness(const CardInfoPtr &info, CardFilter::Attr attr) const +bool FilterItem::acceptPowerToughness(const CardInfoPtr info, CardFilter::Attr attr) const { - const QString powTough = info->getPowTough(); - const qsizetype slash = powTough.indexOf("/"); + int slash = info->getPowTough().indexOf("/"); if (slash == -1) { return false; } - - QString valueString = attr == CardFilter::AttrPow ? powTough.mid(0, slash) : powTough.mid(slash + 1); + QString valueString; + if (attr == CardFilter::AttrPow) { + valueString = info->getPowTough().mid(0, slash); + } else { + valueString = info->getPowTough().mid(slash + 1); + } if (term == valueString) { return true; } @@ -495,11 +386,49 @@ bool FilterItem::acceptPowerToughness(const CardInfoPtr &info, CardFilter::Attr return conversion ? relationCheck(value) : false; } -bool FilterItem::acceptRarity(const CardInfoPtr &info) const +bool FilterItem::acceptRarity(const CardInfoPtr info) const { + QString converted_term = term.trimmed(); + + /* + * The purpose of this loop is to only apply one of the replacement + * policies and then escape. If we attempt to layer them on top of + * each other, we will get awkward results (i.e. comythic rare mythic rareon) + * Conditional statement will exit once a case is successful in + * replacement OR we go through all possible cases. + * Will also need to replace just "mythic" + */ + for (int i = 0; converted_term.length() <= 3 && i <= 6; i++) { + switch (i) { + case 0: + converted_term.replace("mr", "mythic", Qt::CaseInsensitive); + break; + case 1: + converted_term.replace("m r", "mythic", Qt::CaseInsensitive); + break; + case 2: + converted_term.replace("m", "mythic", Qt::CaseInsensitive); + break; + case 3: + converted_term.replace("c", "common", Qt::CaseInsensitive); + break; + case 4: + converted_term.replace("u", "uncommon", Qt::CaseInsensitive); + break; + case 5: + converted_term.replace("r", "rare", Qt::CaseInsensitive); + break; + case 6: + converted_term.replace("s", "special", Qt::CaseInsensitive); + break; + default: + break; + } + } + for (const auto &printings : info->getSets()) { for (const auto &printing : printings) { - if (printing.getProperty("rarity").compare(convertedRarityTerm, Qt::CaseInsensitive) == 0) { + if (printing.getProperty("rarity").compare(converted_term, Qt::CaseInsensitive) == 0) { return true; } } @@ -509,25 +438,41 @@ bool FilterItem::acceptRarity(const CardInfoPtr &info) const bool FilterItem::relationCheck(int cardInfo) const { - switch (numericOperator) { - case NumericOperator::Equal: - return cardInfo == numericOperand; - case NumericOperator::Less: - return cardInfo < numericOperand; - case NumericOperator::LessEqual: - return cardInfo <= numericOperand; - case NumericOperator::Greater: - return cardInfo > numericOperand; - case NumericOperator::GreaterEqual: - return cardInfo >= numericOperand; - case NumericOperator::Invalid: - return false; - } + bool result, conversion; - return false; + // if int conversion fails, there's probably an operator at the start + result = (cardInfo == term.toInt(&conversion)); + if (!conversion) { + // leading whitespaces could cause indexing to fail + QString trimmedTerm = term.trimmed(); + // check whether it's a 2 char operator (<=, >=, or ==) + if (trimmedTerm[1] == '=') { + int termInt = trimmedTerm.mid(2).toInt(); + if (trimmedTerm.startsWith('<')) { + result = (cardInfo <= termInt); + } else if (trimmedTerm.startsWith('>')) { + result = (cardInfo >= termInt); + } else { + result = (cardInfo == termInt); + } + } else { + int termInt = trimmedTerm.mid(1).toInt(); + if (trimmedTerm.startsWith('<')) { + result = (cardInfo < termInt); + } else if (trimmedTerm.startsWith('>')) { + result = (cardInfo > termInt); + } else if (trimmedTerm.startsWith("=")) { + result = (cardInfo == termInt); + } else { + // the int conversion hasn't failed due to an operator at the start + result = false; + } + } + } + return result; } -bool FilterItem::acceptCardAttr(const CardInfoPtr &info, +bool FilterItem::acceptCardAttr(const CardInfoPtr info, CardFilter::Attr attr, const CardSearchLanguage &searchLanguage) const { @@ -611,7 +556,7 @@ FilterTreeNode *FilterTree::termNode(const CardFilter *f) return termNode(f->attr(), f->type(), f->term()); } -bool FilterTree::testAttr(const CardInfoPtr &info, const LogicMap *lm, const CardSearchLanguage &searchLanguage) const +bool FilterTree::testAttr(const CardInfoPtr info, const LogicMap *lm, const CardSearchLanguage &searchLanguage) const { const FilterItemList *fil; bool status = true; @@ -644,7 +589,7 @@ bool FilterTree::testAttr(const CardInfoPtr &info, const LogicMap *lm, const Car return status; } -bool FilterTree::acceptsCard(const CardInfoPtr &info, const CardSearchLanguage &searchLanguage) const +bool FilterTree::acceptsCard(const CardInfoPtr info, const CardSearchLanguage &searchLanguage) const { for (auto i = childNodes.constBegin(); i != childNodes.constEnd(); i++) { if ((*i)->isEnabled() && !testAttr(info, *i, searchLanguage)) { diff --git a/libcockatrice_filters/libcockatrice/filters/filter_tree.h b/libcockatrice_filters/libcockatrice/filters/filter_tree.h index 85a57304d..0defd466b 100644 --- a/libcockatrice_filters/libcockatrice/filters/filter_tree.h +++ b/libcockatrice_filters/libcockatrice/filters/filter_tree.h @@ -174,13 +174,13 @@ public: } [[nodiscard]] bool - testTypeAnd(const CardInfoPtr &info, CardFilter::Attr attr, const CardSearchLanguage &searchLanguage) const; + testTypeAnd(CardInfoPtr info, CardFilter::Attr attr, const CardSearchLanguage &searchLanguage) const; [[nodiscard]] bool - testTypeAndNot(const CardInfoPtr &info, CardFilter::Attr attr, const CardSearchLanguage &searchLanguage) const; + testTypeAndNot(CardInfoPtr info, CardFilter::Attr attr, const CardSearchLanguage &searchLanguage) const; [[nodiscard]] bool - testTypeOr(const CardInfoPtr &info, CardFilter::Attr attr, const CardSearchLanguage &searchLanguage) const; + testTypeOr(CardInfoPtr info, CardFilter::Attr attr, const CardSearchLanguage &searchLanguage) const; [[nodiscard]] bool - testTypeOrNot(const CardInfoPtr &info, CardFilter::Attr attr, const CardSearchLanguage &searchLanguage) const; + testTypeOrNot(CardInfoPtr info, CardFilter::Attr attr, const CardSearchLanguage &searchLanguage) const; }; class FilterItem : public FilterTreeNode @@ -188,33 +188,12 @@ class FilterItem : public FilterTreeNode private: FilterItemList *const p; - /** - * Values derived from the filter term that never change while this node - * exists. Computing them in the constructor keeps them out of the per-card - * evaluation path. - */ - QString convertedColorTerm; - QString convertedRarityTerm; - QString sortedManaCostTerm; - QString uppercaseLoyaltyTerm; - QString lowercaseTerm; - - enum class NumericOperator - { - Invalid, - Equal, - Less, - LessEqual, - Greater, - GreaterEqual, - }; - NumericOperator numericOperator = NumericOperator::Invalid; - int numericOperand = 0; - public: const QString term; - FilterItem(QString trm, FilterItemList *parent); + FilterItem(QString trm, FilterItemList *parent) : p(parent), term(std::move(trm)) + { + } virtual ~FilterItem() = default; [[nodiscard]] CardFilter::Attr attr() const @@ -238,22 +217,22 @@ public: return true; } - [[nodiscard]] bool acceptName(const CardInfoPtr &info, const CardSearchLanguage &searchLanguage) const; - [[nodiscard]] bool acceptNameExact(const CardInfoPtr &info, const CardSearchLanguage &searchLanguage) const; - [[nodiscard]] bool acceptType(const CardInfoPtr &info) const; - [[nodiscard]] bool acceptMainType(const CardInfoPtr &info) const; - [[nodiscard]] bool acceptSubType(const CardInfoPtr &info) const; - [[nodiscard]] bool acceptColor(const CardInfoPtr &info) const; - [[nodiscard]] bool acceptText(const CardInfoPtr &info, const CardSearchLanguage &searchLanguage) const; - [[nodiscard]] bool acceptSet(const CardInfoPtr &info) const; - [[nodiscard]] bool acceptManaCost(const CardInfoPtr &info) const; - [[nodiscard]] bool acceptCmc(const CardInfoPtr &info) const; - [[nodiscard]] bool acceptPowerToughness(const CardInfoPtr &info, CardFilter::Attr attr) const; - [[nodiscard]] bool acceptLoyalty(const CardInfoPtr &info) const; - [[nodiscard]] bool acceptRarity(const CardInfoPtr &info) const; + [[nodiscard]] bool acceptName(CardInfoPtr info, const CardSearchLanguage &searchLanguage) const; + [[nodiscard]] bool acceptNameExact(CardInfoPtr info, const CardSearchLanguage &searchLanguage) const; + [[nodiscard]] bool acceptType(CardInfoPtr info) const; + [[nodiscard]] bool acceptMainType(CardInfoPtr info) const; + [[nodiscard]] bool acceptSubType(CardInfoPtr info) const; + [[nodiscard]] bool acceptColor(CardInfoPtr info) const; + [[nodiscard]] bool acceptText(CardInfoPtr info, const CardSearchLanguage &searchLanguage) const; + [[nodiscard]] bool acceptSet(CardInfoPtr info) const; + [[nodiscard]] bool acceptManaCost(CardInfoPtr info) const; + [[nodiscard]] bool acceptCmc(CardInfoPtr info) const; + [[nodiscard]] bool acceptPowerToughness(CardInfoPtr info, CardFilter::Attr attr) const; + [[nodiscard]] bool acceptLoyalty(CardInfoPtr info) const; + [[nodiscard]] bool acceptRarity(CardInfoPtr info) const; [[nodiscard]] bool - acceptCardAttr(const CardInfoPtr &info, CardFilter::Attr attr, const CardSearchLanguage &searchLanguage) const; - [[nodiscard]] bool acceptFormat(const CardInfoPtr &info) const; + acceptCardAttr(CardInfoPtr info, CardFilter::Attr attr, const CardSearchLanguage &searchLanguage) const; + [[nodiscard]] bool acceptFormat(CardInfoPtr info) const; [[nodiscard]] bool relationCheck(int cardInfo) const; }; @@ -272,7 +251,7 @@ private: LogicMap *attrLogicMap(CardFilter::Attr attr); FilterItemList *attrTypeList(CardFilter::Attr attr, CardFilter::Type type); - bool testAttr(const CardInfoPtr &info, const LogicMap *lm, const CardSearchLanguage &searchLanguage) const; + bool testAttr(CardInfoPtr info, const LogicMap *lm, const CardSearchLanguage &searchLanguage) const; void nodeChanged() const override { @@ -311,7 +290,7 @@ public: return 0; } - [[nodiscard]] bool acceptsCard(const CardInfoPtr &info, const CardSearchLanguage &searchLanguage) const; + [[nodiscard]] bool acceptsCard(CardInfoPtr info, const CardSearchLanguage &searchLanguage) const; void removeFiltersByAttr(CardFilter::Attr filterType); void removeFilter(const CardFilter *toRemove); void clear(); diff --git a/libcockatrice_game/CMakeLists.txt b/libcockatrice_game/CMakeLists.txt deleted file mode 100644 index ce68c5ea6..000000000 --- a/libcockatrice_game/CMakeLists.txt +++ /dev/null @@ -1,77 +0,0 @@ -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 69ed13b0e..3b17569b1 100644 --- a/libcockatrice_interfaces/CMakeLists.txt +++ b/libcockatrice_interfaces/CMakeLists.txt @@ -1,3 +1,7 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + set(HEADERS libcockatrice/interfaces/interface_cache_storage_settings_provider.h libcockatrice/interfaces/interface_card_database_path_provider.h @@ -20,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/CMakeLists.txt b/libcockatrice_models/CMakeLists.txt index 5186eff13..7af35b2d5 100644 --- a/libcockatrice_models/CMakeLists.txt +++ b/libcockatrice_models/CMakeLists.txt @@ -1,3 +1,7 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + add_subdirectory(libcockatrice/models/database) add_subdirectory(libcockatrice/models/deck_list) diff --git a/libcockatrice_models/libcockatrice/models/database/CMakeLists.txt b/libcockatrice_models/libcockatrice/models/database/CMakeLists.txt index 22a04cd32..a73e36dbb 100644 --- a/libcockatrice_models/libcockatrice/models/database/CMakeLists.txt +++ b/libcockatrice_models/libcockatrice/models/database/CMakeLists.txt @@ -1,7 +1,10 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + set(HEADERS card_database_model.h card_database_display_model.h - card_database_visual_display_model.h card/card_completer_proxy_model.h card/card_search_model.h card_set/card_sets_model.h @@ -9,12 +12,13 @@ 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_database_visual_display_model.cpp card/card_completer_proxy_model.cpp card/card_search_model.cpp card_set/card_sets_model.cpp diff --git a/libcockatrice_models/libcockatrice/models/database/card/card_search_model.cpp b/libcockatrice_models/libcockatrice/models/database/card/card_search_model.cpp index 8a267ed7f..5bde6e3fc 100644 --- a/libcockatrice_models/libcockatrice/models/database/card/card_search_model.cpp +++ b/libcockatrice_models/libcockatrice/models/database/card/card_search_model.cpp @@ -9,7 +9,6 @@ #include #include #include -#include CardSearchModel::CardSearchModel(CardDatabaseDisplayModel *sourceModel, QObject *parent) : QAbstractListModel(parent), sourceModel(sourceModel) @@ -70,28 +69,31 @@ void CardSearchModel::updateSearchResults(const QString &query) continue; } - const std::optional lowerName = matchingLowercaseName(card, lowerQuery); - if (!lowerName) { - continue; - } + // The completer suggestions match against the same languages the card + // search uses, so typing a localized name finds the card. In Both mode + // either language can match. + for (const QString &matchName : searchableNames(card)) { + const QString lowerName = matchName.toLower(); + if (!lowerName.contains(lowerQuery)) { + continue; + } - const int distance = levenshteinDistance(lowerQuery, *lowerName); - // Length of the matched name, used to break distance ties. Stored here - // so the comparator does not look the name up again for every comparison. - const int nameLength = static_cast(lowerName->size()); + const int distance = levenshteinDistance(lowerQuery, lowerName); - if (lowerName->startsWith(lowerQuery)) { - prefixMatches.append({card, distance, nameLength}); - } else { - containsMatches.append({card, distance, nameLength}); + if (lowerName.startsWith(lowerQuery)) { + prefixMatches.append({card, distance}); + } else { + containsMatches.append({card, distance}); + } + break; } } - auto sortByDistanceThenLength = [](const SearchResult &a, const SearchResult &b) { + auto sortByDistanceThenLength = [this](const SearchResult &a, const SearchResult &b) { if (a.distance != b.distance) { return a.distance < b.distance; } - return a.nameLength < b.nameLength; + return sortableName(a.card).size() < sortableName(b.card).size(); }; std::sort(prefixMatches.begin(), prefixMatches.end(), sortByDistanceThenLength); @@ -110,28 +112,24 @@ void CardSearchModel::updateSearchResults(const QString &query) endResetModel(); } -std::optional CardSearchModel::matchingLowercaseName(const CardInfoPtr &card, const QString &lowerQuery) const +QStringList CardSearchModel::searchableNames(const CardInfoPtr &card) const { - if (!searchLanguage.isEnglishOnly() && searchLanguage.mode == SearchLanguageMode::Selected) { - QString localizedName = card->getLocalizedName(searchLanguage.language).toLower(); - if (localizedName.contains(lowerQuery)) { - return std::optional(std::move(localizedName)); - } - return std::nullopt; - } - - QString englishName = card->getName().toLower(); - if (englishName.contains(lowerQuery)) { - return std::optional(std::move(englishName)); - } - if (searchLanguage.isEnglishOnly()) { - return std::nullopt; + return {card->getName()}; } - QString localizedName = card->getLocalizedName(searchLanguage.language).toLower(); - if (localizedName.contains(lowerQuery)) { - return std::optional(std::move(localizedName)); + const QString localizedName = card->getLocalizedName(searchLanguage.language); + if (searchLanguage.mode == SearchLanguageMode::Selected) { + return {localizedName}; } - return std::nullopt; + + return {card->getName(), localizedName}; +} + +QString CardSearchModel::sortableName(const CardInfoPtr &card) const +{ + if (searchLanguage.isEnglishOnly()) { + return card->getName(); + } + return card->getLocalizedName(searchLanguage.language); } diff --git a/libcockatrice_models/libcockatrice/models/database/card/card_search_model.h b/libcockatrice_models/libcockatrice/models/database/card/card_search_model.h index ec7c1195e..08d0e8636 100644 --- a/libcockatrice_models/libcockatrice/models/database/card/card_search_model.h +++ b/libcockatrice_models/libcockatrice/models/database/card/card_search_model.h @@ -16,7 +16,6 @@ #include #include #include -#include #include #include @@ -52,20 +51,13 @@ private: { CardInfoPtr card; int distance; - int nameLength; }; - /** - * @brief Finds the lowercased card name that matches the query. - * - * The completer suggestions match against the same languages the card - * search uses, so typing a localized name finds the card. In Both mode - * either language can match. - * - * @return The matched lowercased name, or std::nullopt when the card does not match. - */ - [[nodiscard]] std::optional matchingLowercaseName(const CardInfoPtr &card, - const QString &lowerQuery) const; + /** @brief The names a card is searched by with the current search language. */ + [[nodiscard]] QStringList searchableNames(const CardInfoPtr &card) const; + + /** @brief The name used to break distance ties when sorting suggestions. */ + [[nodiscard]] QString sortableName(const CardInfoPtr &card) const; CardDatabaseDisplayModel *sourceModel; QList searchResults; diff --git a/libcockatrice_models/libcockatrice/models/database/card_database_display_model.cpp b/libcockatrice_models/libcockatrice/models/database/card_database_display_model.cpp index 9ec0afd9c..39bb8314c 100644 --- a/libcockatrice_models/libcockatrice/models/database/card_database_display_model.cpp +++ b/libcockatrice_models/libcockatrice/models/database/card_database_display_model.cpp @@ -1,6 +1,5 @@ #include "card_database_display_model.h" -#include "../natural_sort_collator.h" #include "card_database_model.h" #include "libcockatrice/card/card_localization.h" #include "libcockatrice/filters/filter_string.h" @@ -21,8 +20,7 @@ class QObject; CardDatabaseDisplayModel::CardDatabaseDisplayModel(QObject *parent) - : QSortFilterProxyModel(parent), isToken(ShowAll), filterString(nullptr), cardModel(nullptr), - collator(NaturalSort::makeNaturalSortCollator()) + : QSortFilterProxyModel(parent), isToken(ShowAll), filterString(nullptr) { filterTree = nullptr; setFilterCaseSensitivity(Qt::CaseInsensitive); @@ -33,37 +31,22 @@ CardDatabaseDisplayModel::CardDatabaseDisplayModel(QObject *parent) invalidate(); emit modelDirty(); }); + + loadedRowCount = 0; } void CardDatabaseDisplayModel::setSourceModel(QAbstractItemModel *model) { - cardModel = qobject_cast(model); QSortFilterProxyModel::setSourceModel(model); connect(model, &QAbstractItemModel::rowsInserted, this, [this]() { dirty(); }); connect(model, &QAbstractItemModel::rowsRemoved, this, [this]() { dirty(); }); - connect(model, &QAbstractItemModel::modelReset, this, [this]() { dirty(); }); -} - -QVariant CardDatabaseDisplayModel::data(const QModelIndex &index, int role) const -{ - if (role == CardInfoRole) { - return QVariant::fromValue(cardInfoAt(index)); - } - - return QSortFilterProxyModel::data(index, role); -} - -CardInfoPtr CardDatabaseDisplayModel::cardInfoAt(const QModelIndex &index) const -{ - if (!index.isValid() || cardModel == nullptr) { - return {}; - } - - const QModelIndex sourceIndex = mapToSource(index); - return sourceIndex.isValid() ? cardModel->getCard(sourceIndex.row()) : CardInfoPtr(); + connect(model, &QAbstractItemModel::modelReset, this, [this]() { + loadedRowCount = 0; + dirty(); + }); } QMap CardDatabaseDisplayModel::characterTranslation = {{L'“', L'\"'}, @@ -71,14 +54,41 @@ QMap CardDatabaseDisplayModel::characterTranslation = {{L'“' {L'‘', L'\''}, {L'’', L'\''}}; +bool CardDatabaseDisplayModel::canFetchMore(const QModelIndex &index) const +{ + return loadedRowCount < sourceModel()->rowCount(index); +} + +void CardDatabaseDisplayModel::fetchMore(const QModelIndex &index) +{ + int remainder = sourceModel()->rowCount(index) - loadedRowCount; + int itemsToFetch = qMin(100, remainder); + + if (itemsToFetch == 0) { + return; + } + + const auto startIndex = qMin(rowCount(QModelIndex()), loadedRowCount); + beginInsertRows(QModelIndex(), startIndex, startIndex + itemsToFetch - 1); + + loadedRowCount += itemsToFetch; + endInsertRows(); +} + +int CardDatabaseDisplayModel::rowCount(const QModelIndex &parent) const +{ + return QSortFilterProxyModel::rowCount(parent); +} + bool CardDatabaseDisplayModel::lessThan(const QModelIndex &left, const QModelIndex &right) const { - const QString leftString = sourceModel()->data(left, CardDatabaseModel::SortRole).toString(); - const QString rightString = sourceModel()->data(right, CardDatabaseModel::SortRole).toString(); + + QString leftString = sourceModel()->data(left, CardDatabaseModel::SortRole).toString(); + QString rightString = sourceModel()->data(right, CardDatabaseModel::SortRole).toString(); if (!cardName.isEmpty() && left.column() == CardDatabaseModel::NameColumn) { - const bool isLeftType = leftString.startsWith(cardName, Qt::CaseInsensitive); - const bool isRightType = rightString.startsWith(cardName, Qt::CaseInsensitive); + bool isLeftType = leftString.startsWith(cardName, Qt::CaseInsensitive); + bool isRightType = rightString.startsWith(cardName, Qt::CaseInsensitive); // test for an exact match: isLeftType && leftString.size() == cardName.size() // or an exclusive start match: isLeftType && !isRightType @@ -91,37 +101,31 @@ bool CardDatabaseDisplayModel::lessThan(const QModelIndex &left, const QModelInd return false; } } else if (right.column() == CardDatabaseModel::PTColumn && left.column() == CardDatabaseModel::PTColumn) { - // The comparator runs O(n log n) times while the view is sorted, so - // splitting every power/toughness string into a QStringList allocates - // a lot for data that has exactly one separator. Work on views and - // only split the two halves out when both strings are well formed. - const QStringView leftPT = leftString; - const QStringView rightPT = rightString; - const qsizetype leftSlash = leftPT.indexOf(u'/'); - const qsizetype rightSlash = rightPT.indexOf(u'/'); - const bool leftIsWellFormed = leftSlash >= 0 && leftPT.indexOf(u'/', leftSlash + 1) < 0; - const bool rightIsWellFormed = rightSlash >= 0 && rightPT.indexOf(u'/', rightSlash + 1) < 0; + QStringList leftList = leftString.split("/"); + QStringList rightList = rightString.split("/"); - if (leftIsWellFormed && rightIsWellFormed) { - const int powerComparison = lessThanNumerically(leftPT.left(leftSlash), rightPT.left(rightSlash)); - if (powerComparison != 0) { - return powerComparison < 0; + if (leftList.size() == 2 && rightList.size() == 2) { + + // cool, have both P/T in list now + int lessThanNum = lessThanNumerically(leftList.at(0), rightList.at(0)); + if (lessThanNum != 0) { + return lessThanNum < 0; + } else { + // power equal, check toughness + return lessThanNumerically(leftList.at(1), rightList.at(1)) < 0; } - // power equal, check toughness - return lessThanNumerically(leftPT.mid(leftSlash + 1), rightPT.mid(rightSlash + 1)) < 0; } } - return collator.compare(leftString, rightString) < 0; + return QString::localeAwareCompare(leftString, rightString) < 0; } -int CardDatabaseDisplayModel::lessThanNumerically(QStringView left, QStringView right) const +int CardDatabaseDisplayModel::lessThanNumerically(const QString &left, const QString &right) { if (left == right) { return 0; } - bool okLeft = false; - bool okRight = false; + bool okLeft, okRight; float leftNum = left.toFloat(&okLeft); float rightNum = right.toFloat(&okRight); @@ -135,11 +139,11 @@ int CardDatabaseDisplayModel::lessThanNumerically(QStringView left, QStringView } } // try and parsing again, for weird ones like "1+*" - QStringView leftAfterNum; - QStringView rightAfterNum; + QString leftAfterNum = ""; + QString rightAfterNum = ""; if (!okLeft) { int leftNumIndex = 0; - for (; leftNumIndex < left.size(); leftNumIndex++) { + for (; leftNumIndex < left.length(); leftNumIndex++) { if (!left.at(leftNumIndex).isDigit()) { break; } @@ -151,7 +155,7 @@ int CardDatabaseDisplayModel::lessThanNumerically(QStringView left, QStringView } if (!okRight) { int rightNumIndex = 0; - for (; rightNumIndex < right.size(); rightNumIndex++) { + for (; rightNumIndex < right.length(); rightNumIndex++) { if (!right.at(rightNumIndex).isDigit()) { break; } @@ -173,7 +177,7 @@ int CardDatabaseDisplayModel::lessThanNumerically(QStringView left, QStringView } else { // both parsed, same number, but at least one has something else // so compare the part after the number - prefer nothing - return collator.compare(leftAfterNum, rightAfterNum); + return QString::localeAwareCompare(leftAfterNum, rightAfterNum); } } else if (okLeft) { return -1; @@ -181,7 +185,7 @@ int CardDatabaseDisplayModel::lessThanNumerically(QStringView left, QStringView return 1; } // couldn't parse it, just return String comparison - return collator.compare(left, right); + return QString::localeAwareCompare(left, right); } bool CardDatabaseDisplayModel::filterAcceptsRow(int sourceRow, const QModelIndex & /*sourceParent*/) const { diff --git a/libcockatrice_models/libcockatrice/models/database/card_database_display_model.h b/libcockatrice_models/libcockatrice/models/database/card_database_display_model.h index 3a9bbc6ab..d12dd1fb4 100644 --- a/libcockatrice_models/libcockatrice/models/database/card_database_display_model.h +++ b/libcockatrice_models/libcockatrice/models/database/card_database_display_model.h @@ -10,18 +10,15 @@ #include "libcockatrice/card/card_info.h" -#include #include #include #include #include -#include #include #include #include #include -class CardDatabaseModel; class FilterTree; class QObject; template class QMap; @@ -37,25 +34,13 @@ public: ShowAll }; - enum Role - { - /** - * @brief The CardInfoPtr behind a proxy row. - * - * Saves views from looking the card up again through its name, and keeps - * them from depending on the source model's column layout. - */ - CardInfoRole = Qt::UserRole + 1 - }; - private: FilterBool isToken; QString cardName, cardText; QSet cardNameSet, cardTypes, cardColors; FilterTree *filterTree; FilterString *filterString; - /// The source model, kept typed so CardInfoRole does not cast per row. - CardDatabaseModel *cardModel; + int loadedRowCount; QTimer dirtyTimer; CardSearchLanguage searchLanguage; QString searchText; @@ -63,9 +48,6 @@ private: /** The translation table that will be used for sanitizeCardName. */ static QMap characterTranslation; - /** Natural, locale-aware comparison shared by every sort in this model. */ - QCollator collator; - public: explicit CardDatabaseDisplayModel(QObject *parent = nullptr); void setSourceModel(QAbstractItemModel *model) override; @@ -98,22 +80,15 @@ public: dirtyTimer.start(20); } void clearFilterAll(); - [[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - - /** - * @brief Returns the card behind a proxy row, without the QVariant wrapper of CardInfoRole. - * - * The visual grid calls this for every row on each rebuild; CardInfoRole stays the - * generic way in for consumers that only see the model through data(). - */ - [[nodiscard]] CardInfoPtr cardInfoAt(const QModelIndex &index) const; - + [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; + [[nodiscard]] bool canFetchMore(const QModelIndex &parent) const override; + void fetchMore(const QModelIndex &parent) override; signals: void modelDirty(); protected: [[nodiscard]] bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; - int lessThanNumerically(QStringView left, QStringView right) const; + static int lessThanNumerically(const QString &left, const QString &right); [[nodiscard]] bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; [[nodiscard]] bool rowMatchesCardName(CardInfoPtr info) const; diff --git a/libcockatrice_models/libcockatrice/models/database/card_database_model.cpp b/libcockatrice_models/libcockatrice/models/database/card_database_model.cpp index a83b7c83d..1ede554f8 100644 --- a/libcockatrice_models/libcockatrice/models/database/card_database_model.cpp +++ b/libcockatrice_models/libcockatrice/models/database/card_database_model.cpp @@ -27,18 +27,14 @@ CardDatabaseModel::CardDatabaseModel(CardDatabase *_db, bool _showOnlyCardsFromE CardDatabaseModel::~CardDatabaseModel() = default; -int CardDatabaseModel::rowCount(const QModelIndex &parent) const +int CardDatabaseModel::rowCount(const QModelIndex & /*parent*/) const { - // A list model's card rows are leaves; returning the full card count for a - // valid parent makes every row claim all cards as children. - return parent.isValid() ? 0 : cardList.size(); + return cardList.size(); } -int CardDatabaseModel::columnCount(const QModelIndex &parent) const +int CardDatabaseModel::columnCount(const QModelIndex & /*parent*/) const { - // Card rows are leaves, so a valid parent holds no columns. Reporting the - // row width for a card index would claim child cells that do not exist. - return parent.isValid() ? 0 : CARDDBMODEL_COLUMNS; + return CARDDBMODEL_COLUMNS; } QVariant CardDatabaseModel::data(const QModelIndex &index, int role) const diff --git a/libcockatrice_models/libcockatrice/models/database/card_database_visual_display_model.cpp b/libcockatrice_models/libcockatrice/models/database/card_database_visual_display_model.cpp deleted file mode 100644 index f6a3a925c..000000000 --- a/libcockatrice_models/libcockatrice/models/database/card_database_visual_display_model.cpp +++ /dev/null @@ -1,123 +0,0 @@ -#include "card_database_visual_display_model.h" - -#include "card_database_display_model.h" -#include "libcockatrice/card/database/card_database_querier.h" - -#include -#include -#include -#include - -/// Coalesces the bursts of proxy signals a filter, sort or database load produces. -static constexpr int REBUILD_COALESCE_MS = 30; - -CardDatabaseVisualDisplayModel::CardDatabaseVisualDisplayModel(CardDatabaseDisplayModel *_displayModel, - CardDatabaseQuerier *_querier, - QObject *parent) - : QAbstractListModel(parent), displayModel(_displayModel), querier(_querier) -{ - rebuildTimer.setSingleShot(true); - rebuildTimer.setInterval(REBUILD_COALESCE_MS); - connect(&rebuildTimer, &QTimer::timeout, this, &CardDatabaseVisualDisplayModel::rebuild); - - if (displayModel == nullptr) { - return; - } - - connect(displayModel, &QAbstractItemModel::modelReset, this, &CardDatabaseVisualDisplayModel::scheduleRebuild); - connect(displayModel, &QAbstractItemModel::layoutChanged, this, &CardDatabaseVisualDisplayModel::scheduleRebuild); - connect(displayModel, &QAbstractItemModel::rowsInserted, this, &CardDatabaseVisualDisplayModel::scheduleRebuild); - connect(displayModel, &QAbstractItemModel::rowsRemoved, this, &CardDatabaseVisualDisplayModel::scheduleRebuild); - connect(displayModel, &QAbstractItemModel::dataChanged, this, &CardDatabaseVisualDisplayModel::scheduleRebuild); -} - -void CardDatabaseVisualDisplayModel::setSetFilterTerms(const QStringList &terms) -{ - if (setFilterTerms == terms) { - return; - } - - setFilterTerms = terms; - refresh(); -} - -void CardDatabaseVisualDisplayModel::refresh() -{ - rebuildTimer.stop(); - rebuild(); -} - -void CardDatabaseVisualDisplayModel::scheduleRebuild() -{ - rebuildTimer.start(); -} - -void CardDatabaseVisualDisplayModel::rebuild() -{ - beginResetModel(); - entries.clear(); - - if (displayModel != nullptr) { - const int proxyRows = displayModel->rowCount(); - entries.reserve(proxyRows); - for (int row = 0; row < proxyRows; ++row) { - const CardInfoPtr info = displayModel->cardInfoAt(displayModel->index(row, 0)); - if (info) { - appendPrintingsFor(info); - } - } - } - - endResetModel(); -} - -void CardDatabaseVisualDisplayModel::appendPrintingsFor(const CardInfoPtr &info) -{ - if (setFilterTerms.isEmpty()) { - entries.append({info, PrintingInfo()}); - return; - } - - const auto &printingsBySet = info->getSets(); - for (const QString &term : setFilterTerms) { - const auto printings = printingsBySet.constFind(term); - if (printings == printingsBySet.constEnd()) { - continue; - } - for (const PrintingInfo &printing : printings.value()) { - entries.append({info, printing}); - } - } -} - -int CardDatabaseVisualDisplayModel::rowCount(const QModelIndex &parent) const -{ - return parent.isValid() ? 0 : static_cast(entries.size()); -} - -QVariant CardDatabaseVisualDisplayModel::data(const QModelIndex &index, int role) const -{ - if (!index.isValid() || index.row() >= entries.size()) { - return {}; - } - - if (role == Qt::DisplayRole || role == Qt::AccessibleTextRole) { - return cardAt(index.row()).getName(); - } - - return {}; -} - -ExactCard CardDatabaseVisualDisplayModel::cardAt(int row) const -{ - if (row < 0 || row >= entries.size()) { - return {}; - } - - const Entry &entry = entries.at(row); - if (entry.printing.isEmpty() && setFilterTerms.isEmpty()) { - return querier != nullptr ? querier->getPreferredCard(entry.info) : ExactCard(entry.info); - } - - return ExactCard(entry.info, entry.printing); -} diff --git a/libcockatrice_models/libcockatrice/models/database/card_database_visual_display_model.h b/libcockatrice_models/libcockatrice/models/database/card_database_visual_display_model.h deleted file mode 100644 index 1303b6464..000000000 --- a/libcockatrice_models/libcockatrice/models/database/card_database_visual_display_model.h +++ /dev/null @@ -1,90 +0,0 @@ -/** - * @file card_database_visual_display_model.h - * @ingroup CardDatabaseModels - * @brief A list model of the exact cards a visual card grid paints. - * - * The filtered and sorted CardDatabaseDisplayModel holds one row per card name. - * A visual grid shows one picture per *printing* instead, so this model flattens - * the proxy rows into ExactCards: the preferred printing per card normally, and - * every printing of the selected sets while set filters are active. - */ - -#ifndef COCKATRICE_CARD_DATABASE_VISUAL_DISPLAY_MODEL_H -#define COCKATRICE_CARD_DATABASE_VISUAL_DISPLAY_MODEL_H - -#include "libcockatrice/card/card_info.h" -#include "libcockatrice/card/printing/exact_card.h" -#include "libcockatrice/card/printing/printing_info.h" - -#include -#include -#include -#include -#include -#include -#include - -class CardDatabaseDisplayModel; -class CardDatabaseQuerier; - -class CardDatabaseVisualDisplayModel : public QAbstractListModel -{ - Q_OBJECT - -public: - /** - * @brief Flattens _displayModel into the cards a visual grid displays. - * @param _displayModel The filtered and sorted proxy model of the card database. - * @param _querier Resolves the preferred printing of a card. - * @param parent Parent object. - */ - explicit CardDatabaseVisualDisplayModel(CardDatabaseDisplayModel *_displayModel, - CardDatabaseQuerier *_querier, - QObject *parent = nullptr); - - /** - * @brief Sets the set shorthand names of the active set filters. - * - * With no terms every card is shown with its preferred printing. With terms - * every printing belonging to one of the named sets is shown instead, in term - * order. Rebuilds immediately when the terms change. - */ - void setSetFilterTerms(const QStringList &terms); - - /** - * @brief Rebuilds the row list from the proxy model right away. - * - * Proxy model changes normally rebuild on a short coalescing timer; call this - * when the caller needs the new rows before returning. - */ - void refresh(); - - [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; - [[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - - /** - * @brief Returns the card painted at @p row, with its preferred printing resolved. - * @param row Row in this model, not in the proxy model. - */ - [[nodiscard]] ExactCard cardAt(int row) const; - -private: - struct Entry - { - CardInfoPtr info; - /// Empty when the entry follows the preferred printing rather than a set filter. - PrintingInfo printing; - }; - - void scheduleRebuild(); - void rebuild(); - void appendPrintingsFor(const CardInfoPtr &info); - - QPointer displayModel; - CardDatabaseQuerier *querier; - QStringList setFilterTerms; - QList entries; - QTimer rebuildTimer; -}; - -#endif // COCKATRICE_CARD_DATABASE_VISUAL_DISPLAY_MODEL_H diff --git a/libcockatrice_models/libcockatrice/models/database/card_set/card_sets_model.cpp b/libcockatrice_models/libcockatrice/models/database/card_set/card_sets_model.cpp index b8bdc3466..a0b9c1061 100644 --- a/libcockatrice_models/libcockatrice/models/database/card_set/card_sets_model.cpp +++ b/libcockatrice_models/libcockatrice/models/database/card_set/card_sets_model.cpp @@ -1,6 +1,5 @@ #include "card_sets_model.h" -#include "../../natural_sort_collator.h" #include "libcockatrice/card/database/card_database.h" #include "libcockatrice/card/set/card_set.h" #include "libcockatrice/interfaces/interface_card_set_priority_controller.h" @@ -297,8 +296,7 @@ QStringList SetsModel::mimeTypes() const return QStringList() << "application/x-cockatricecardset"; } -SetsDisplayModel::SetsDisplayModel(QObject *parent) - : QSortFilterProxyModel(parent), collator(NaturalSort::makeNaturalSortCollator()) +SetsDisplayModel::SetsDisplayModel(QObject *parent) : QSortFilterProxyModel(parent) { setFilterCaseSensitivity(Qt::CaseInsensitive); setSortCaseSensitivity(Qt::CaseInsensitive); @@ -333,5 +331,5 @@ bool SetsDisplayModel::lessThan(const QModelIndex &left, const QModelIndex &righ QString leftString = sourceModel()->data(left, SetsModel::SortRole).toString(); QString rightString = sourceModel()->data(right, SetsModel::SortRole).toString(); - return collator.compare(leftString, rightString) < 0; + return QString::localeAwareCompare(leftString, rightString) < 0; } diff --git a/libcockatrice_models/libcockatrice/models/database/card_set/card_sets_model.h b/libcockatrice_models/libcockatrice/models/database/card_set/card_sets_model.h index 17eab6c53..354414dc8 100644 --- a/libcockatrice_models/libcockatrice/models/database/card_set/card_sets_model.h +++ b/libcockatrice_models/libcockatrice/models/database/card_set/card_sets_model.h @@ -11,7 +11,6 @@ #include "libcockatrice/card/set/card_set_list.h" #include -#include #include #include #include @@ -113,9 +112,6 @@ protected: [[nodiscard]] bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; [[nodiscard]] bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; void fetchMore(const QModelIndex &index) override; - -private: - QCollator collator; }; #endif diff --git a/libcockatrice_models/libcockatrice/models/deck_list/CMakeLists.txt b/libcockatrice_models/libcockatrice/models/deck_list/CMakeLists.txt index ce5c4d54b..324bed102 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/CMakeLists.txt +++ b/libcockatrice_models/libcockatrice/models/deck_list/CMakeLists.txt @@ -1,7 +1,13 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +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_models/libcockatrice/models/deck_list/deck_list_model.cpp b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp index 617324fe3..4b4088972 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp @@ -134,13 +134,6 @@ void DeckListModel::rebuildTree() int DeckListModel::rowCount(const QModelIndex &parent) const { - // Only column 0 carries children. Column 1 and later indexes are siblings - // used by views for the remaining cells of a row, and a model that hangs - // children off them breaks QAbstractItemModelTester and proxy models. - if (parent.isValid() && parent.column() != 0) { - return 0; - } - // debugIndexInfo("rowCount", parent); auto *node = getNode(parent); if (node) { @@ -289,7 +282,7 @@ QVariant DeckListModel::headerData(const int section, const Qt::Orientation orie QModelIndex DeckListModel::index(int row, int column, const QModelIndex &parent) const { // debugIndexInfo("index", parent); - if ((parent.isValid() && parent.column() != 0) || !hasIndex(row, column, parent)) { + if (!hasIndex(row, column, parent)) { return {}; } diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_sort_filter_proxy_model.cpp b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_sort_filter_proxy_model.cpp index cf3410560..dcb294ec8 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_sort_filter_proxy_model.cpp +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_sort_filter_proxy_model.cpp @@ -23,7 +23,7 @@ bool DeckListSortFilterProxyModel::lessThan(const QModelIndex &left, const QMode if (!leftIsCard || !rightIsCard) { QString lName = src->data(left.siblingAtColumn(DeckListModelColumns::CARD_NAME), Qt::EditRole).toString(); QString rName = src->data(right.siblingAtColumn(DeckListModelColumns::CARD_NAME), Qt::EditRole).toString(); - return collator.compare(lName, rName) < 0; + return lName.localeAwareCompare(rName) < 0; } // Both are cards -> apply sort criteria @@ -38,7 +38,7 @@ bool DeckListSortFilterProxyModel::lessThan(const QModelIndex &left, const QMode if (crit == "name") { QString ln = lNode->getName(); QString rn = rNode->getName(); - int cmp = collator.compare(ln, rn); + int cmp = ln.localeAwareCompare(rn); if (cmp != 0) { return cmp < 0; } @@ -51,14 +51,14 @@ bool DeckListSortFilterProxyModel::lessThan(const QModelIndex &left, const QMode } else if (crit == "colors") { QString lr = lInfo ? lInfo->getColors() : QString(); QString rr = rInfo ? rInfo->getColors() : QString(); - int cmp = collator.compare(lr, rr); + int cmp = lr.localeAwareCompare(rr); if (cmp != 0) { return cmp < 0; } } else if (crit == "maintype") { QString lr = lInfo ? lInfo->getMainCardType() : QString(); QString rr = rInfo ? rInfo->getMainCardType() : QString(); - int cmp = collator.compare(lr, rr); + int cmp = lr.localeAwareCompare(rr); if (cmp != 0) { return cmp < 0; } diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_sort_filter_proxy_model.h b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_sort_filter_proxy_model.h index b911b4740..522ed1e23 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_sort_filter_proxy_model.h +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_sort_filter_proxy_model.h @@ -7,9 +7,6 @@ #ifndef COCKATRICE_DECK_LIST_SORT_FILTER_PROXY_MODEL_H #define COCKATRICE_DECK_LIST_SORT_FILTER_PROXY_MODEL_H -#include "../natural_sort_collator.h" - -#include #include #include #include @@ -22,8 +19,7 @@ class DeckListSortFilterProxyModel : public QSortFilterProxyModel { Q_OBJECT public: - explicit DeckListSortFilterProxyModel(QObject *parent = nullptr) - : QSortFilterProxyModel(parent), collator(NaturalSort::makeNaturalSortCollator()) + explicit DeckListSortFilterProxyModel(QObject *parent = nullptr) : QSortFilterProxyModel(parent) { } @@ -38,7 +34,6 @@ protected: private: QStringList sortCriteria; - QCollator collator; }; #endif // COCKATRICE_DECK_LIST_SORT_FILTER_PROXY_MODEL_H diff --git a/libcockatrice_models/libcockatrice/models/natural_sort_collator.h b/libcockatrice_models/libcockatrice/models/natural_sort_collator.h deleted file mode 100644 index 8c026ca06..000000000 --- a/libcockatrice_models/libcockatrice/models/natural_sort_collator.h +++ /dev/null @@ -1,30 +0,0 @@ -#ifndef NATURAL_SORT_COLLATOR_H -#define NATURAL_SORT_COLLATOR_H - -#include -#include - -namespace NaturalSort -{ - -/** - * @brief A collator that compares runs of digits numerically, whatever the process locale is. - * - * Numeric mode only takes effect through a real locale. Under the C locale, which - * headless CI processes run in, QCollator silently falls back to plain string order - * and sorts "Forest 10" before "Forest 2". Pin English for that single degenerate - * locale so sorting is the same everywhere, and keep the user's locale otherwise. - */ -inline QCollator makeNaturalSortCollator() -{ - QCollator collator; - collator.setNumericMode(true); - if (QLocale().language() == QLocale::C) { - collator.setLocale(QLocale(QLocale::English)); - } - return collator; -} - -} // namespace NaturalSort - -#endif // NATURAL_SORT_COLLATOR_H \ No newline at end of file diff --git a/libcockatrice_network/CMakeLists.txt b/libcockatrice_network/CMakeLists.txt index 925e4d445..3069c0db4 100644 --- a/libcockatrice_network/CMakeLists.txt +++ b/libcockatrice_network/CMakeLists.txt @@ -1,3 +1,7 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + add_subdirectory(libcockatrice/network/client) add_subdirectory(libcockatrice/network/server) diff --git a/libcockatrice_network/libcockatrice/network/client/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/client/CMakeLists.txt index cf8d9fc4d..bcf62463c 100644 --- a/libcockatrice_network/libcockatrice/network/client/CMakeLists.txt +++ b/libcockatrice_network/libcockatrice/network/client/CMakeLists.txt @@ -1,3 +1,7 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + add_subdirectory(abstract) add_subdirectory(local) add_subdirectory(remote) diff --git a/libcockatrice_network/libcockatrice/network/client/abstract/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/client/abstract/CMakeLists.txt index 33812e0bf..53ad524b4 100644 --- a/libcockatrice_network/libcockatrice/network/client/abstract/CMakeLists.txt +++ b/libcockatrice_network/libcockatrice/network/client/abstract/CMakeLists.txt @@ -1,8 +1,14 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + 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) @@ -11,7 +17,7 @@ target_include_directories(libcockatrice_network_client_abstract PUBLIC .) target_link_libraries( libcockatrice_network_client_abstract PUBLIC ${COCKATRICE_QT_VERSION_NAME}::Network ${COCKATRICE_QT_VERSION_NAME}::WebSockets libcockatrice_protocol - libcockatrice_network_server_remote libcockatrice_utility + libcockatrice_network_server_remote ) target_precompile_headers(libcockatrice_network_client_abstract PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h") diff --git a/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.cpp b/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.cpp index 04e6beda0..0c523d776 100644 --- a/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.cpp +++ b/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.cpp @@ -10,7 +10,6 @@ #include "libcockatrice/protocol/pb/serverinfo_user.pb.h" #include "libcockatrice/protocol/pb/session_commands.pb.h" #include "libcockatrice/protocol/pb/session_event.pb.h" -#include "libcockatrice/utility/passwordhasher.h" #include #include @@ -81,26 +80,6 @@ AbstractClient::~AbstractClient() { } -Command_AccountPassword AbstractClient::makeAccountPasswordCommand(const QString &oldPassword, - const QString &newPassword) const -{ - Command_AccountPassword cmd; - cmd.set_old_password(oldPassword.toStdString()); - if (getServerSupportsChallengeResponse()) { - // A verifier lets the server upgrade the stored credential without ever seeing the - // password, and it is what a challenge-response server expects for a new password. - cmd.set_hashed_new_password(PasswordHasher::generatePasswordVerifier(newPassword).toStdString()); - } else if (getServerSupportsPasswordHash()) { - // The server hashes the password itself with its own salt; a salted hash is the - // strongest form it accepts, so the plaintext never goes on the wire. - const QString passwordSalt = PasswordHasher::generateRandomSalt(); - cmd.set_hashed_new_password(PasswordHasher::computeHash(newPassword, passwordSalt).toStdString()); - } else { - cmd.set_new_password(newPassword.toStdString()); - } - return cmd; -} - void AbstractClient::processProtocolItem(const ServerMessage &item) { switch (item.message_type()) { diff --git a/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.h b/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.h index 005587ccc..7efd67972 100644 --- a/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.h +++ b/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.h @@ -47,7 +47,6 @@ class Event_UserJoined; class Event_UserLeft; class Event_ServerMessage; class Event_ListRooms; -class Command_AccountPassword; class Event_GameJoined; class Event_UserMessage; class Event_NotifyUser; @@ -68,9 +67,6 @@ enum ClientStatus StatusSubmitForgotPasswordReset, StatusSubmitForgotPasswordChallenge, StatusGettingPasswordSalt, - //! The server accepted the credentials but keeps the session open until the account - //! password is replaced; the connection must survive for `Command_AccountPassword`. - StatusPasswordChangeRequired, }; class AbstractClient : public QObject @@ -175,16 +171,6 @@ public: { return serverSupportsChallengeResponse; } - - /** - * @brief Builds the account-password command in the form this server understands. - * - * Challenge-response servers take a scrypt verifier; servers that only advertise - * SupportsPasswordHash take a salted SHA-512 hash; anything older gets the plaintext, - * which is the only form it understands. Both password-change paths go through here so - * the two cannot drift apart and hand a plaintext password to a server that can hash it. - */ - Command_AccountPassword makeAccountPasswordCommand(const QString &oldPassword, const QString &newPassword) const; const QString &getUserName() const { return userName; diff --git a/libcockatrice_network/libcockatrice/network/client/local/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/client/local/CMakeLists.txt index 3eff7943a..08c12b7f8 100644 --- a/libcockatrice_network/libcockatrice/network/client/local/CMakeLists.txt +++ b/libcockatrice_network/libcockatrice/network/client/local/CMakeLists.txt @@ -1,8 +1,14 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + 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/local/local_client.cpp b/libcockatrice_network/libcockatrice/network/client/local/local_client.cpp index b9a1566f8..c1c1cc976 100644 --- a/libcockatrice_network/libcockatrice/network/client/local/local_client.cpp +++ b/libcockatrice_network/libcockatrice/network/client/local/local_client.cpp @@ -33,26 +33,20 @@ LocalClient::LocalClient(LocalServerInterface *_lsi, sendCommand(prepareSessionCommand(joinCmd)); } +LocalClient::~LocalClient() +{ +} + void LocalClient::sendCommandContainer(const CommandContainer &cont) { - if (LocalClientLog().isDebugEnabled()) { - qCDebug(LocalClientLog).noquote() << userName << "OUT" << getSafeDebugString(cont); - } - - if (lsi.isNull()) { - qCWarning(LocalClientLog).noquote() - << userName << "dropping command, local server interface no longer exists:" << getSafeDebugString(cont); - return; - } + qCDebug(LocalClientLog).noquote() << userName << "OUT" << getSafeDebugString(cont); lsi->itemFromClient(cont); } void LocalClient::itemFromServer(const ServerMessage &item) { - if (LocalClientLog().isDebugEnabled()) { - qCDebug(LocalClientLog).noquote() << userName << "IN" << getSafeDebugString(item); - } + qCDebug(LocalClientLog).noquote() << userName << "IN" << getSafeDebugString(item); processProtocolItem(item); } diff --git a/libcockatrice_network/libcockatrice/network/client/local/local_client.h b/libcockatrice_network/libcockatrice/network/client/local/local_client.h index 15b36d3db..3ae570255 100644 --- a/libcockatrice_network/libcockatrice/network/client/local/local_client.h +++ b/libcockatrice_network/libcockatrice/network/client/local/local_client.h @@ -10,7 +10,6 @@ #include "../abstract/abstract_client.h" #include -#include #include #include @@ -25,15 +24,14 @@ class LocalClient : public AbstractClient { Q_OBJECT private: - // Guarded pointer: the LocalServer (and its interfaces) may be destroyed while this client - // still has pending commands queued on the event loop. - QPointer lsi; + LocalServerInterface *lsi; public: LocalClient(LocalServerInterface *_lsi, const QString &_playerName, const QString &_clientId, QObject *parent = nullptr); + ~LocalClient() override; void sendCommandContainer(const CommandContainer &cont) override; private slots: diff --git a/libcockatrice_network/libcockatrice/network/client/remote/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/client/remote/CMakeLists.txt index 006c18d42..c32ca0aa8 100644 --- a/libcockatrice_network/libcockatrice/network/client/remote/CMakeLists.txt +++ b/libcockatrice_network/libcockatrice/network/client/remote/CMakeLists.txt @@ -1,8 +1,14 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + 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 faacdc9f7..cbf7dbda2 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(), @@ -125,7 +84,6 @@ RemoteClient::RemoteClient(QObject *parent, INetworkSettingsProvider *_networkSe &RemoteClient::doSubmitForgotPasswordResetToServer); connect(this, &RemoteClient::sigSubmitForgotPasswordChallengeToServer, this, &RemoteClient::doSubmitForgotPasswordChallengeToServer); - connect(this, &RemoteClient::sigChangePasswordToServer, this, &RemoteClient::doChangePasswordToServer); } RemoteClient::~RemoteClient() @@ -355,29 +313,6 @@ void RemoteClient::submitPasswordVerifierResponse(const Response &response) } } -void RemoteClient::changePasswordToServer(const QString &oldPassword, const QString &newPassword) -{ - emit sigChangePasswordToServer(oldPassword, newPassword); -} - -void RemoteClient::doChangePasswordToServer(const QString &oldPassword, const QString &newPassword) -{ - const Command_AccountPassword cmd = makeAccountPasswordCommand(oldPassword, newPassword); - - PendingCommand *pend = prepareSessionCommand(cmd); - connect(pend, &PendingCommand::finished, this, &RemoteClient::accountPasswordResponse); - sendCommand(pend); -} - -void RemoteClient::accountPasswordResponse(const Response &response) -{ - if (response.response_code() == Response::RespOk) { - emit sigPasswordChangeSuccess(); - } else { - emit sigPasswordChangeError(response.response_code()); - } -} - void RemoteClient::processConnectionClosedEvent(const Event_ConnectionClosed & /*event*/) { doDisconnectFromServer(); @@ -400,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; @@ -408,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(); @@ -419,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; @@ -428,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 = @@ -436,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; @@ -499,23 +439,6 @@ void RemoteClient::loginResponse(const Response &response) emit notifyUserAboutUpdate(); } - } else if (response.response_code() == Response::RespPasswordChangeRequired) { - // The credentials were accepted: the server keeps this session open and serves - // nothing but ping and Command_AccountPassword until the password is replaced. - // Treating it as a failed login would disconnect before the dialog that collects - // the new password (queued to the UI thread behind this slot) can send anything: - // the command would sit in pendingCommands forever, because the ping timer that - // times it out stops with the connection. Keep the session and let the controller - // report the outcome through accountPasswordResponse(). - QList missingFeatures; - if (resp.missing_features_size() > 0) { - for (int i = 0; i < resp.missing_features_size(); ++i) { - missingFeatures << QString::fromStdString(resp.missing_features(i)); - } - } - setStatus(StatusPasswordChangeRequired); - emit loginError(response.response_code(), QString::fromStdString(resp.denied_reason_str()), - static_cast(resp.denied_end_time()), missingFeatures); } else if (response.response_code() != Response::RespNotConnected) { QList missingFeatures; if (resp.missing_features_size() > 0) { @@ -523,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); } } @@ -584,15 +496,9 @@ void RemoteClient::readData() inputBuffer.append(data); - // Parse messages straight out of inputBuffer at readOffset. Removing each - // consumed message from the front of the buffer would memmove all of the - // remaining bytes once per message; instead the consumed prefix is dropped - // once parsing stops. - qsizetype readOffset = 0; - - while (true) { + do { if (!messageInProgress) { - if (inputBuffer.size() - readOffset >= 4) { + if (inputBuffer.size() >= 4) { // dirty hack to be compatible with v14 server that sends 60 bytes of garbage at the beginning if (!handshakeStarted) { handshakeStarted = true; @@ -602,30 +508,29 @@ void RemoteClient::readData() } } else { // end of hack - const auto *header = reinterpret_cast(inputBuffer.constData() + readOffset); - messageLength = (static_cast(header[0]) << 24) + (static_cast(header[1]) << 16) + - (static_cast(header[2]) << 8) + static_cast(header[3]); - readOffset += 4; + messageLength = (((quint32)(unsigned char)inputBuffer[0]) << 24) + + (((quint32)(unsigned char)inputBuffer[1]) << 16) + + (((quint32)(unsigned char)inputBuffer[2]) << 8) + + ((quint32)(unsigned char)inputBuffer[3]); + inputBuffer.remove(0, 4); messageInProgress = true; } } else { - break; + return; } } - if (inputBuffer.size() - readOffset < messageLength) { - break; + if (inputBuffer.size() < messageLength) { + return; } ServerMessage newServerMessage; - bool ok = newServerMessage.ParseFromArray(inputBuffer.constData() + readOffset, messageLength); + bool ok = newServerMessage.ParseFromArray(inputBuffer.data(), messageLength); - readOffset += messageLength; + inputBuffer.remove(0, messageLength); messageInProgress = false; if (ok) { - if (RemoteClientLog().isDebugEnabled()) { - qCDebug(RemoteClientLog).noquote() << "IN" << getSafeDebugString(newServerMessage); - } + qCDebug(RemoteClientLog).noquote() << "IN" << getSafeDebugString(newServerMessage); processProtocolItem(newServerMessage); } else { @@ -635,12 +540,7 @@ void RemoteClient::readData() if (getStatus() == StatusDisconnecting) { // use thread-safe getter doDisconnectFromServer(); } - } - - // Drop the consumed prefix once, instead of shifting the buffer per message. - if (readOffset > 0) { - inputBuffer.remove(0, readOffset); - } + } while (!inputBuffer.isEmpty()); } void RemoteClient::websocketMessageReceived(const QByteArray &message) @@ -648,9 +548,7 @@ void RemoteClient::websocketMessageReceived(const QByteArray &message) lastDataReceived = timeRunning; ServerMessage newServerMessage; if (newServerMessage.ParseFromArray(message.data(), message.length())) { - if (RemoteClientLog().isDebugEnabled()) { - qCDebug(RemoteClientLog).noquote() << "IN" << getSafeDebugString(newServerMessage); - } + qCDebug(RemoteClientLog).noquote() << "IN" << getSafeDebugString(newServerMessage); processProtocolItem(newServerMessage); } else { @@ -666,9 +564,7 @@ void RemoteClient::sendCommandContainer(const CommandContainer &cont) auto size = static_cast(cont.ByteSize()); #endif - if (RemoteClientLog().isDebugEnabled()) { - qCDebug(RemoteClientLog).noquote() << "OUT" << getSafeDebugString(cont); - } + qCDebug(RemoteClientLog).noquote() << "OUT" << getSafeDebugString(cont); QByteArray buf; bool ok; @@ -925,7 +821,7 @@ void RemoteClient::requestForgotPasswordResponse(const Response &response) emit sigPromptForForgotPasswordReset(); } } else { - emit sigForgotPasswordError(response.response_code()); + emit sigForgotPasswordError(); } doDisconnectFromServer(); @@ -961,7 +857,7 @@ void RemoteClient::submitForgotPasswordResetResponse(const Response &response) derivedVerifier.clear(); } } else { - emit sigForgotPasswordError(response.response_code()); + emit sigForgotPasswordError(); } doDisconnectFromServer(); @@ -996,7 +892,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 1cf40491c..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, @@ -71,11 +71,6 @@ signals: //! should only store it for the connection it is currently negotiating, so //! the hostname and user name are intentionally not part of the signal. void sigPasswordVerifierReady(const QString &verifier); - //! \brief Outcome of the password change the server demanded at login. - void sigPasswordChangeSuccess(); - void sigPasswordChangeError(int responseCode); - //! \brief Request a password change; queued to the client thread like the other requests. - void sigChangePasswordToServer(const QString &oldPassword, const QString &newPassword); private slots: void slotConnected(); void readData(); @@ -104,8 +99,6 @@ private slots: Command_Login generateCommandLogin(); void doSubmitPasswordVerifier(); void submitPasswordVerifierResponse(const Response &response); - void accountPasswordResponse(const Response &response); - void doChangePasswordToServer(const QString &oldPassword, const QString &newPassword); void doDisconnectFromServer(); void doActivateToServer(const QString &_token); void doRequestForgotPasswordToServer(const QString &hostname, unsigned int port, const QString &_userName); @@ -208,10 +201,6 @@ public: unsigned int port, const QString &_userName, const QString &_email); - //! \brief Change the password of the account that just logged in, following the - //! server's forced-password-change reply. Emits sigPasswordChangeSuccess or - //! sigPasswordChangeError when the server answers. - void changePasswordToServer(const QString &oldPassword, const QString &newPassword); }; #endif diff --git a/libcockatrice_network/libcockatrice/network/network_manager.h b/libcockatrice_network/libcockatrice/network/network_manager.h deleted file mode 100644 index bb20e1315..000000000 --- a/libcockatrice_network/libcockatrice/network/network_manager.h +++ /dev/null @@ -1,38 +0,0 @@ -#ifndef COCKATRICE_NETWORK_NETWORK_MANAGER_H -#define COCKATRICE_NETWORK_NETWORK_MANAGER_H - -#include -#include -#include - -/** - * @file network_manager.h - * @ingroup Network - * @brief Creates QNetworkAccessManagers with the application-wide baseline. - * - * Every manager in the client should start from NetworkManagerFactory::create() - * so features that apply to all HTTP traffic (the system proxy, a transfer - * timeout) cannot be forgotten in one of the many call sites. Feature-specific - * setup such as a disk cache or a manual redirect policy stays with the caller. - */ -namespace NetworkManagerFactory -{ -/** - * @brief Returns a QNetworkAccessManager configured for Cockatrice. - * - * The manager is parented to @p parent. The system proxy is enabled once, - * process-wide, the first time this function is called. Transfers use Qt's - * default timeout instead of waiting forever. - */ -inline QNetworkAccessManager *create(QObject *parent = nullptr) -{ - static std::once_flag systemProxyFlag; - std::call_once(systemProxyFlag, [] { QNetworkProxyFactory::setUseSystemConfiguration(true); }); - - auto *manager = new QNetworkAccessManager(parent); - manager->setTransferTimeout(); - return manager; -} -} // namespace NetworkManagerFactory - -#endif // COCKATRICE_NETWORK_NETWORK_MANAGER_H diff --git a/libcockatrice_network/libcockatrice/network/server/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/server/CMakeLists.txt index b59d61572..cbb717ad8 100644 --- a/libcockatrice_network/libcockatrice/network/server/CMakeLists.txt +++ b/libcockatrice_network/libcockatrice/network/server/CMakeLists.txt @@ -1,3 +1,7 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + add_subdirectory(local) add_subdirectory(remote) diff --git a/libcockatrice_network/libcockatrice/network/server/local/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/server/local/CMakeLists.txt index 4055efa64..020753d7f 100644 --- a/libcockatrice_network/libcockatrice/network/server/local/CMakeLists.txt +++ b/libcockatrice_network/libcockatrice/network/server/local/CMakeLists.txt @@ -1,8 +1,14 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + 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 181218e1a..2127487d2 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt +++ b/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt @@ -1,3 +1,7 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + set(HEADERS game/dump_zone_range.h game/server_abstract_participant.h @@ -8,7 +12,6 @@ set(HEADERS game/server_cardzone.h game/server_counter.h game/game_config.h - game/role_info.h game/server_deck_validation_strategy.h game/server_game.h game/server_game_lifecycle_strategy.h @@ -26,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_network/libcockatrice/network/server/remote/game/role_info.h b/libcockatrice_network/libcockatrice/network/server/remote/game/role_info.h deleted file mode 100644 index ef8692361..000000000 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/role_info.h +++ /dev/null @@ -1,13 +0,0 @@ -#ifndef COCKATRICE_ROLE_INFO_H -#define COCKATRICE_ROLE_INFO_H - -/** - * @brief Represents information about a participant's roles. - */ -struct RoleInfo -{ - bool isJudge = false; ///< Whether the participant joined as a judge - bool isSpectator = false; ///< Whether the participant joined as a spectator -}; - -#endif // COCKATRICE_ROLE_INFO_H diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp index ff8ea14f3..5b8d901bb 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp @@ -61,10 +61,10 @@ Server_AbstractParticipant::Server_AbstractParticipant(Server_Game *_game, int _playerId, const ServerInfo_User &_userInfo, - const RoleInfo &_roleInfo, + bool _judge, Server_AbstractUserInterface *_userInterface) : ServerInfo_User_Container(_userInfo), game(_game), userInterface(_userInterface), pingTime(0), - playerId(_playerId), roleInfo(_roleInfo) + playerId(_playerId), judge(_judge) { } @@ -98,8 +98,8 @@ void Server_AbstractParticipant::getProperties(ServerInfo_PlayerProperties &resu if (withUserInfo) { copyUserInfo(*(result.mutable_user_info()), true); } - result.set_spectator(isSpectator()); - result.set_judge(isJudge()); + result.set_spectator(spectator); + result.set_judge(judge); result.set_ping_seconds(pingTime); getPlayerProperties(result); } @@ -176,7 +176,7 @@ Response::ResponseCode Server_AbstractParticipant::cmdUnconcede(const Command_Un Response::ResponseCode Server_AbstractParticipant::cmdJudge(const Command_Judge &cmd, ResponseContainer &rc, GameEventStorage &ges) { - if (!isJudge()) { + if (!judge) { return Response::RespFunctionNotAllowed; } @@ -204,7 +204,7 @@ Response::ResponseCode Server_AbstractParticipant::cmdReadyStart(const Command_R Response::ResponseCode Server_AbstractParticipant::cmdGameSay(const Command_GameSay &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) { - if (isSpectator()) { + if (spectator) { /* Spectators can only talk if: * (a) the game creator allows it * (b) the spectator is a moderator/administrator @@ -365,7 +365,7 @@ Response::ResponseCode Server_AbstractParticipant::cmdNextTurn(const Command_Nex return Response::RespGameNotStarted; } - if (!isJudge()) { + if (!judge) { return Response::RespFunctionNotAllowed; } @@ -381,7 +381,7 @@ Response::ResponseCode Server_AbstractParticipant::cmdSetActivePhase(const Comma return Response::RespGameNotStarted; } - if (!isJudge()) { + if (!judge) { return Response::RespFunctionNotAllowed; } @@ -415,8 +415,8 @@ Response::ResponseCode Server_AbstractParticipant::cmdReverseTurn(const Command_ ResponseContainer & /*rc*/, GameEventStorage &ges) { - if (!isJudge()) { - if (isSpectator()) { + if (!judge) { + if (spectator) { return Response::RespFunctionNotAllowed; } @@ -577,7 +577,7 @@ void Server_AbstractParticipant::setUserInterface(Server_AbstractUserInterface * void Server_AbstractParticipant::disconnectClient() { bool isRegistered = userInfo->user_level() & ServerInfo_User::IsRegistered; - if (!isRegistered || isSpectator()) { + if (!isRegistered || spectator) { game->removeParticipant(this, Event_Leave::USER_DISCONNECTED); } else { setUserInterface(nullptr); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h index b6d4a8025..195da9b7f 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h @@ -2,7 +2,6 @@ #define ABSTRACT_PARTICIPANT_H #include "../serverinfo_user_container.h" -#include "role_info.h" #include "server_arrowtarget.h" #include @@ -62,7 +61,8 @@ protected: Server_AbstractUserInterface *userInterface; int pingTime; int playerId; - RoleInfo roleInfo; + bool spectator; + bool judge; virtual void getPlayerProperties(ServerInfo_PlayerProperties &result); mutable QMutex playerMutex; @@ -70,7 +70,7 @@ public: Server_AbstractParticipant(Server_Game *_game, int _playerId, const ServerInfo_User &_userInfo, - const RoleInfo &_roleInfo, + bool _judge, Server_AbstractUserInterface *_handler); ~Server_AbstractParticipant() override; virtual void prepareDestroy() @@ -91,11 +91,11 @@ public: } bool isSpectator() const { - return roleInfo.isSpectator; + return spectator; } bool isJudge() const { - return roleInfo.isJudge; + return judge; } Server_Game *getGame() const { diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp index b5a620798..c03f1e966 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp @@ -81,9 +81,10 @@ Server_AbstractPlayer::Server_AbstractPlayer(Server_Game *_game, const ServerInfo_User &_userInfo, bool _judge, Server_AbstractUserInterface *_userInterface) - : Server_AbstractParticipant(_game, _playerId, _userInfo, {.isJudge = _judge}, _userInterface), conceded(false), - deck(nullptr), sideboardLocked(true), readyStart(false), nextCardId(0) + : Server_AbstractParticipant(_game, _playerId, _userInfo, _judge, _userInterface), conceded(false), deck(nullptr), + sideboardLocked(true), readyStart(false), nextCardId(0) { + spectator = false; } Server_AbstractPlayer::~Server_AbstractPlayer() = default; @@ -294,7 +295,7 @@ Response::ResponseCode Server_AbstractPlayer::moveCard(GameEventStorage &ges, { // Disallow controller change to other zones than the table. if (((targetzone->getType() != ServerInfo_Zone::PublicZone) || !targetzone->hasCoords()) && - (startzone->getPlayer() != targetzone->getPlayer()) && !isJudge()) { + (startzone->getPlayer() != targetzone->getPlayer()) && !judge) { return Response::RespContextError; } @@ -807,7 +808,7 @@ Server_AbstractPlayer::cmdMoveCard(const Command_MoveCard &cmd, ResponseContaine return Response::RespNameNotFound; } - if ((startPlayer != this) && (!startZone->getPlayersWithWritePermission().contains(playerId)) && !isJudge()) { + if ((startPlayer != this) && (!startZone->getPlayersWithWritePermission().contains(playerId)) && !judge) { return Response::RespContextError; } @@ -820,7 +821,7 @@ Server_AbstractPlayer::cmdMoveCard(const Command_MoveCard &cmd, ResponseContaine return Response::RespNameNotFound; } - if ((startPlayer != this) && (targetPlayer != this) && !isJudge()) { + if ((startPlayer != this) && (targetPlayer != this) && !judge) { return Response::RespContextError; } @@ -1597,7 +1598,7 @@ Server_AbstractPlayer::cmdRevealCards(const Command_RevealCards &cmd, ResponseCo zone->addWritePermission(cmd.player_id()); } - if (isJudge()) { + if (judge) { ges.setOverwriteOwnership(true); } diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp index 49db999f6..b1863c91e 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp @@ -41,7 +41,6 @@ #include "libcockatrice/protocol/pb/serverinfo_replay_match.pb.h" #include "libcockatrice/protocol/pb/serverinfo_user.pb.h" #include "libcockatrice/protocol/pb/session_event.pb.h" -#include "role_info.h" #include "server_abstract_player.h" #include "server_abstractuserinterface.h" #include "server_arrow.h" @@ -480,8 +479,9 @@ void Server_Game::stopGameIfFinished() Response::ResponseCode Server_Game::checkJoin(ServerInfo_User *user, const QString &_password, - const RoleInfo &roleInfo, - bool overrideRestrictions) + bool spectator, + bool overrideRestrictions, + bool asJudge) { Server_DatabaseInterface *databaseInterface = room->getServer()->getDatabaseInterface(); for (auto *participant : participants.values()) { @@ -490,11 +490,11 @@ Response::ResponseCode Server_Game::checkJoin(ServerInfo_User *user, } } - if (roleInfo.isJudge && !(user->user_level() & ServerInfo_User::IsJudge)) { + if (asJudge && !(user->user_level() & ServerInfo_User::IsJudge)) { return Response::RespUserLevelTooLow; } if (!(overrideRestrictions && (user->user_level() & (ServerInfo_User::IsModerator | ServerInfo_User::IsJudge)))) { - if ((_password != password) && !(roleInfo.isSpectator && !spectatorsNeedPassword)) { + if ((_password != password) && !(spectator && !spectatorsNeedPassword)) { return Response::RespWrongPassword; } if (!(user->user_level() & ServerInfo_User::IsRegistered) && onlyRegistered) { @@ -510,13 +510,13 @@ Response::ResponseCode Server_Game::checkJoin(ServerInfo_User *user, QString::fromStdString(user->name()))) { return Response::RespInIgnoreList; } - if (roleInfo.isSpectator) { + if (spectator) { if (!spectatorsAllowed) { return Response::RespSpectatorsNotAllowed; } } } - if (!roleInfo.isSpectator && (gameStarted || (getPlayerCount() >= getMaxPlayers()))) { + if (!spectator && (gameStarted || (getPlayerCount() >= getMaxPlayers()))) { return Response::RespGameFull; } @@ -537,18 +537,19 @@ bool Server_Game::containsUser(const QString &userName) const void Server_Game::addPlayer(Server_AbstractUserInterface *userInterface, ResponseContainer &rc, - const RoleInfo &roleInfo, + bool spectator, + bool judge, bool broadcastUpdate) { QMutexLocker locker(&gameMutex); Server_AbstractParticipant *newParticipant; - if (roleInfo.isSpectator) { + if (spectator) { newParticipant = new Server_Spectator(this, nextPlayerId++, userInterface->copyUserInfo(true, true, true), - roleInfo.isJudge, userInterface); + judge, userInterface); } else { - newParticipant = new Server_Player(this, nextPlayerId++, userInterface->copyUserInfo(true, true, true), - roleInfo.isJudge, userInterface); + newParticipant = new Server_Player(this, nextPlayerId++, userInterface->copyUserInfo(true, true, true), judge, + userInterface); } newParticipant->moveToThread(thread()); @@ -559,7 +560,7 @@ void Server_Game::addPlayer(Server_AbstractUserInterface *userInterface, const QString playerName = QString::fromStdString(newParticipant->getUserInfo()->name()); participants.insert(newParticipant->getPlayerId(), newParticipant); - if (roleInfo.isSpectator) { + if (spectator) { allSpectatorsEver.insert(playerName); } else { allPlayersEver.insert(playerName); @@ -578,7 +579,7 @@ void Server_Game::addPlayer(Server_AbstractUserInterface *userInterface, emit gameInfoChanged(gameInfo); } - if ((newParticipant->getUserInfo()->user_level() & ServerInfo_User::IsRegistered) && !roleInfo.isSpectator) { + if ((newParticipant->getUserInfo()->user_level() & ServerInfo_User::IsRegistered) && !spectator) { room->getServer()->addPersistentPlayer(playerName, room->getId(), gameId, newParticipant->getPlayerId()); } @@ -870,13 +871,6 @@ void Server_Game::createGameJoinedEvent(Server_AbstractParticipant *joiningParti } } -bool Server_Game::canSeePrivateEventsFor(const Server_AbstractParticipant *participant, int privatePlayerId) const -{ - return participant->getPlayerId() == privatePlayerId // same player - || participant->isJudge() // judges see everything - || (participant->isSpectator() && spectatorsSeeEverything); // spectators see everything -} - void Server_Game::sendGameEventContainer(GameEventContainer *cont, GameEventStorageItem::EventRecipients recipients, int privatePlayerId) @@ -885,7 +879,8 @@ void Server_Game::sendGameEventContainer(GameEventContainer *cont, cont->set_game_id(gameId); for (auto *participant : participants.values()) { - const bool playerPrivate = canSeePrivateEventsFor(participant, privatePlayerId); + const bool playerPrivate = (participant->getPlayerId() == privatePlayerId) || participant->isJudge() || + (participant->isSpectator() && spectatorsSeeEverything); if ((recipients.testFlag(GameEventStorageItem::SendToPrivate) && playerPrivate) || (recipients.testFlag(GameEventStorageItem::SendToOthers) && !playerPrivate)) { participant->sendGameEvent(*cont); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h index 363c0ca94..c7f8d0929 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h @@ -62,7 +62,6 @@ class Message; } // namespace protobuf } // namespace google struct GameConfig; -struct RoleInfo; class Server_Game : public QObject { @@ -110,7 +109,6 @@ private: bool omniscient, bool withUserInfo); void storeGameInformation(); - bool canSeePrivateEventsFor(const Server_AbstractParticipant *participant, int privatePlayerId) const; signals: void sigStartGameIfReady(bool override); void gameInfoChanged(ServerInfo_Game gameInfo); @@ -190,11 +188,12 @@ public: return shareDecklistsOnLoad; } Response::ResponseCode - checkJoin(ServerInfo_User *user, const QString &_password, const RoleInfo &roleInfo, bool overrideRestrictions); + checkJoin(ServerInfo_User *user, const QString &_password, bool spectator, bool overrideRestrictions, bool asJudge); bool containsUser(const QString &userName) const; void addPlayer(Server_AbstractUserInterface *userInterface, ResponseContainer &rc, - const RoleInfo &roleInfo, + bool spectator, + bool judge, bool broadcastUpdate = true); void removeParticipant(Server_AbstractParticipant *participant, Event_Leave::LeaveReason reason); void removeArrowsRelatedToPlayer(GameEventStorage &ges, Server_AbstractPlayer *player); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp index b40dae09b..949d04b4b 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp @@ -573,7 +573,7 @@ Server_Player::cmdNextTurn(const Command_NextTurn & /*cmd*/, ResponseContainer & return Response::RespGameNotStarted; } - if (conceded && !isJudge()) { + if (conceded && !judge) { return Response::RespContextError; } @@ -589,7 +589,7 @@ Response::ResponseCode Server_Player::cmdSetActivePhase(const Command_SetActiveP return Response::RespGameNotStarted; } - if (!isJudge()) { + if (!judge) { if (conceded) { return Response::RespContextError; } @@ -623,7 +623,7 @@ Response::ResponseCode Server_Player::cmdReverseTurn(const Command_ReverseTurn &cmd, ResponseContainer &rc, GameEventStorage &ges) { - if (!isJudge() && conceded) { + if (!judge && conceded) { return Response::RespContextError; } return Server_AbstractParticipant::cmdReverseTurn(cmd, rc, ges); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_spectator.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_spectator.cpp index 164a4af5e..d1c369949 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_spectator.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_spectator.cpp @@ -11,6 +11,7 @@ Server_Spectator::Server_Spectator(Server_Game *_game, const ServerInfo_User &_userInfo, bool _judge, Server_AbstractUserInterface *_userInterface) - : Server_AbstractParticipant(_game, _playerId, _userInfo, {.isJudge = _judge, .isSpectator = true}, _userInterface) + : Server_AbstractParticipant(_game, _playerId, _userInfo, _judge, _userInterface) { + spectator = true; } diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server.cpp index 8ea2cd993..63989bbfa 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/server.cpp @@ -131,16 +131,6 @@ AuthenticationResult Server::loginUser(Server_ProtocolHandler *session, return authState; } - if (authState == PasswordChangeRequired) { - // The credentials were right, but the account must choose a new password before it may - // use the server. Give the session an identity so it can complete that change, without - // registering a user session or announcing the user as logged in. - ServerInfo_User pendingData = databaseInterface->getUserData(name, true); - pendingData.set_address(session->getAddress().toStdString()); - session->setUserInfo(pendingData); - return authState; - } - ServerInfo_User data = databaseInterface->getUserData(name, true); data.set_address(session->getAddress().toStdString()); name = QString::fromStdString(data.name()); // Compensate for case indifference @@ -306,9 +296,7 @@ void Server::removeClient(Server_ProtocolHandler *client) QWriteLocker locker(&clientsLock); clients.removeAt(clientIndex); ServerInfo_User *data = client->getUserInfo(); - // A session that never completed login (for example one that still owes a password change) - // has no session id: it was never announced as joined, so it must not be announced as leaving. - if (data && data->has_session_id()) { + if (data) { Event_UserLeft event; event.set_name(data->name()); SessionEvent *se = Server_ProtocolHandler::prepareSessionEvent(event); @@ -323,10 +311,12 @@ void Server::removeClient(Server_ProtocolHandler *client) users.remove(QString::fromStdString(data->name())); qDebug() << "Server::removeClient: name=" << QString::fromStdString(data->name()); - const qint64 sessionId = data->session_id(); - usersBySessionId.remove(sessionId); - emit endSession(sessionId); - qDebug() << "closed session id:" << sessionId; + if (data->has_session_id()) { + const qint64 sessionId = data->session_id(); + usersBySessionId.remove(sessionId); + emit endSession(sessionId); + qDebug() << "closed session id:" << sessionId; + } } qDebug() << "Server::removeClient: removed" << (void *)client << ";" << clients.size() << "clients; " << users.size() << "users left"; @@ -340,9 +330,7 @@ QList Server::getOnlineModeratorList() const ServerInfo_User *data = client->getUserInfo(); //! \todo This line should be updated in the event there is any type of new user level created. - // A session that still owes a forced password change has user info but no session id: - // it never joined, so it must not be reported as an online moderator either. - if (data && data->has_session_id() && + if (data && (data->user_level() & ServerInfo_User::IsModerator || data->user_level() & ServerInfo_User::IsAdmin)) { results << QString::fromStdString(data->name()).simplified(); } diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_abstractuserinterface.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server_abstractuserinterface.cpp index c3524bb93..25aa415fc 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_abstractuserinterface.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_abstractuserinterface.cpp @@ -84,18 +84,6 @@ void Server_AbstractUserInterface::onLogin(ResponseContainer &) { } -bool Server_AbstractUserInterface::getGameInfo(int gameId, int &roomId, int &playerId) const -{ - QMutexLocker locker(&gameListMutex); - const auto it = games.constFind(gameId); - if (it == games.constEnd()) { - return false; - } - roomId = it.value().first; - playerId = it.value().second; - return true; -} - void Server_AbstractUserInterface::playerRemovedFromGame(Server_Game *game) { qDebug() << "Server_AbstractUserInterface::playerRemovedFromGame(): gameId =" << game->getGameId(); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_abstractuserinterface.h b/libcockatrice_network/libcockatrice/network/server/remote/server_abstractuserinterface.h index a2a4ed8c7..a19475e34 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_abstractuserinterface.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_abstractuserinterface.h @@ -58,13 +58,6 @@ public: return games; } - /** - * @brief Looks up the room and player id of one game without copying the game list. - * - * @return True if the game is known; otherwise false and the outputs are untouched. - */ - bool getGameInfo(int gameId, int &roomId, int &playerId) const; - virtual void sendProtocolItem(const Response &item) = 0; virtual void sendProtocolItem(const SessionEvent &item) = 0; virtual void sendProtocolItem(const GameEventContainer &item) = 0; diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp index f9cadc9aa..eb2b2fc1c 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp @@ -64,9 +64,8 @@ Server_ProtocolHandler::Server_ProtocolHandler(Server *_server, Server_DatabaseInterface *_databaseInterface, QObject *parent) : QObject(parent), Server_AbstractUserInterface(_server), deleted(false), databaseInterface(_databaseInterface), - authState(NotLoggedIn), usingRealPassword(false), passwordChangePending(false), acceptsUserListChanges(false), - acceptsRoomListChanges(false), idleClientWarningSent(false), timeRunning(0), lastDataReceived(0), - lastActionReceived(0) + authState(NotLoggedIn), usingRealPassword(false), acceptsUserListChanges(false), acceptsRoomListChanges(false), + idleClientWarningSent(false), timeRunning(0), lastDataReceived(0), lastActionReceived(0) { connect(server, &Server::pingClockTimeout, this, &Server_ProtocolHandler::pingClockTimeout); @@ -209,14 +208,11 @@ Response::ResponseCode Server_ProtocolHandler::processSessionCommandContainer(co Response::ResponseCode resp = Response::RespInvalidCommand; const SessionCommand &sc = cont.session_command(i); const int num = getPbExtension(sc); - if (num != SessionCommand::PING && logsDebugMessages()) { // don't log ping commands + if (num != SessionCommand::PING) { // don't log ping commands logDebugMessage(getSafeDebugString(sc)); } const auto commandType = static_cast(num); - if (passwordChangePending && commandType != SessionCommand::PING && - commandType != SessionCommand::ACCOUNT_PASSWORD) { - resp = Response::RespFunctionNotAllowed; - } else if (authState == NotLoggedIn && !isPreAuthSessionCommand(commandType)) { + if (authState == NotLoggedIn && !isPreAuthSessionCommand(commandType)) { resp = Response::RespLoginNeeded; } else { switch (commandType) { @@ -275,9 +271,7 @@ Response::ResponseCode Server_ProtocolHandler::processRoomCommandContainer(const Response::ResponseCode resp = Response::RespInvalidCommand; const RoomCommand &sc = cont.room_command(i); const int num = getPbExtension(sc); - if (logsDebugMessages()) { - logDebugMessage(getSafeDebugString(sc)); - } + logDebugMessage(getSafeDebugString(sc)); switch ((RoomCommand::RoomCommandType)num) { case RoomCommand::LEAVE_ROOM: resp = cmdLeaveRoom(sc.GetExtension(Command_LeaveRoom::ext), room, rc); @@ -326,14 +320,14 @@ Response::ResponseCode Server_ProtocolHandler::processGameCommandContainer(const return Response::RespLoginNeeded; } - int roomId; - int playerId; - if (!getGameInfo(cont.game_id(), roomId, playerId)) { + QMap> gameMap = getGames(); + if (!gameMap.contains(cont.game_id())) { return Response::RespNotInRoom; } + const QPair roomIdAndPlayerId = gameMap.value(cont.game_id()); QReadLocker roomsLocker(&server->roomsLock); - Server_Room *room = server->getRooms().value(roomId); + Server_Room *room = server->getRooms().value(roomIdAndPlayerId.first); if (!room) { return Response::RespNotInRoom; } @@ -343,14 +337,14 @@ Response::ResponseCode Server_ProtocolHandler::processGameCommandContainer(const if (!game) { if (room->getExternalGames().contains(cont.game_id())) { server->sendIsl_GameCommand(cont, room->getExternalGames().value(cont.game_id()).server_id(), - userInfo->session_id(), roomId, playerId); + userInfo->session_id(), roomIdAndPlayerId.first, roomIdAndPlayerId.second); return Response::RespNothing; } return Response::RespNotInRoom; } QMutexLocker gameLocker(&game->gameMutex); - auto *participant = game->getParticipants().value(playerId); + auto *participant = game->getParticipants().value(roomIdAndPlayerId.second); if (!participant) { return Response::RespNotInRoom; } @@ -363,9 +357,8 @@ Response::ResponseCode Server_ProtocolHandler::processGameCommandContainer(const Response::ResponseCode finalResponseCode = Response::RespOk; for (int i = cont.game_command_size() - 1; i >= 0; --i) { const GameCommand &sc = cont.game_command(i); - if (logsDebugMessages()) { - logDebugMessage(QString("game %1 player %2: ").arg(cont.game_id()).arg(playerId) + getSafeDebugString(sc)); - } + logDebugMessage(QString("game %1 player %2: ").arg(cont.game_id()).arg(roomIdAndPlayerId.second) + + getSafeDebugString(sc)); if (commandCountingInterval > 0) { int totalCount = 0; @@ -414,9 +407,7 @@ Response::ResponseCode Server_ProtocolHandler::processModeratorCommandContainer( Response::ResponseCode resp = Response::RespInvalidCommand; const ModeratorCommand &sc = cont.moderator_command(i); const int num = getPbExtension(sc); - if (logsDebugMessages()) { - logDebugMessage(getSafeDebugString(sc)); - } + logDebugMessage(getSafeDebugString(sc)); resp = processExtendedModeratorCommand(num, sc, rc); if (resp != Response::RespOk) { @@ -443,9 +434,7 @@ Response::ResponseCode Server_ProtocolHandler::processAdminCommandContainer(cons Response::ResponseCode resp = Response::RespInvalidCommand; const AdminCommand &sc = cont.admin_command(i); const int num = getPbExtension(sc); - if (logsDebugMessages()) { - logDebugMessage(getSafeDebugString(sc)); - } + logDebugMessage(getSafeDebugString(sc)); resp = processExtendedAdminCommand(num, sc, rc); if (resp != Response::RespOk) { @@ -472,9 +461,7 @@ Response::ResponseCode Server_ProtocolHandler::processDeveloperCommandContainer( Response::ResponseCode resp = Response::RespInvalidCommand; const DeveloperCommand &sc = cont.developer_command(i); const int num = getPbExtension(sc); - if (logsDebugMessages()) { - logDebugMessage(getSafeDebugString(sc)); - } + logDebugMessage(getSafeDebugString(sc)); resp = processExtendedDeveloperCommand(num, sc, rc); if (resp != Response::RespOk) { @@ -493,19 +480,6 @@ void Server_ProtocolHandler::processCommandContainer(const CommandContainer &con lastDataReceived = timeRunning; - // A session that still owes a forced password change may only reach the session family, - // where processSessionCommandContainer narrows it further to ping and the change itself. - // Every other family is rejected here, including a container that also carries a session - // command: the dispatch below prefers game/room over session, so testing only for the - // absence of a session command would let such a container through. - if (passwordChangePending && - (cont.game_command_size() || cont.room_command_size() || cont.moderator_command_size() || - cont.admin_command_size() || cont.developer_command_size())) { - sendResponseContainer(ResponseContainer(cont.has_cmd_id() ? cont.cmd_id() : -1), - Response::RespFunctionNotAllowed); - return; - } - ResponseContainer responseContainer(cont.has_cmd_id() ? cont.cmd_id() : -1); Response::ResponseCode finalResponseCode; @@ -713,11 +687,6 @@ Response::ResponseCode Server_ProtocolHandler::cmdLogin(const Command_Login &cmd server->recordFailedLogin(getAddress(), userName); return Response::RespAccountNotActivated; case PasswordChangeRequired: - // The credentials were right, but the account must choose a new password first. - // Keep the session authenticated just far enough for the client to do that: - // processCommandContainer refuses everything except the change itself. - authState = PasswordRight; - passwordChangePending = true; return Response::RespPasswordChangeRequired; default: server->clearFailedLogins(getAddress(), userName); @@ -1141,8 +1110,7 @@ Server_ProtocolHandler::cmdCreateGame(const Command_CreateGame &cmd, Server_Room auto *game = new Server_Game(config, room); - RoleInfo roleInfo{.isJudge = asJudge, .isSpectator = asSpectator}; - game->addPlayer(this, rc, roleInfo, false); + game->addPlayer(this, rc, asSpectator, asJudge, false); room->addGame(game); return Response::RespOk; diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.h b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.h index 8ce110354..c0a1ffe9a 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.h @@ -46,12 +46,6 @@ protected: Server_DatabaseInterface *databaseInterface; AuthenticationResult authState; bool usingRealPassword; - /** - * @brief True when the credentials were right but the account must choose a new password - * before it may use the server. Only ping, the password change and disconnecting - * are served while this is set. - */ - bool passwordChangePending; bool acceptsUserListChanges; bool acceptsRoomListChanges; bool idleClientWarningSent; @@ -61,17 +55,6 @@ protected: virtual void logDebugMessage(const QString & /* message */) { } - /** - * @brief Whether debug messages produced by this handler are actually written anywhere. - * - * Formatting a protocol message for the log is expensive, so callers only - * build the debug string when this returns true. The base handler has no - * log destination and therefore never needs one. - */ - [[nodiscard]] virtual bool logsDebugMessages() const - { - return false; - } private: QList messageSizeOverTime, messageCountOverTime, commandCountOverTime; diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_room.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server_room.cpp index 78ef2b289..68b137cd4 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_room.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_room.cpp @@ -1,6 +1,5 @@ #include "server_room.h" -#include "game/role_info.h" #include "game/server_game.h" #include "libcockatrice/protocol/pb/response.pb.h" #include "libcockatrice/protocol/pb/room_event.pb.h" @@ -373,11 +372,11 @@ Response::ResponseCode Server_Room::processJoinGameCommand(const Command_JoinGam QMutexLocker gameLocker(&game->gameMutex); - RoleInfo roleInfo{.isJudge = cmd.join_as_judge(), .isSpectator = cmd.spectator()}; - Response::ResponseCode result = game->checkJoin( - userInterface->getUserInfo(), QString::fromStdString(cmd.password()), roleInfo, cmd.override_restrictions()); + Response::ResponseCode result = + game->checkJoin(userInterface->getUserInfo(), QString::fromStdString(cmd.password()), cmd.spectator(), + cmd.override_restrictions(), cmd.join_as_judge()); if (result == Response::RespOk) { - game->addPlayer(userInterface, rc, roleInfo); + game->addPlayer(userInterface, rc, cmd.spectator(), cmd.join_as_judge()); } return result; diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt b/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt index 059e83096..bd0b5bda2 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt @@ -174,8 +174,6 @@ set(PROTO_FILES response_report_user_info.proto response_moderator_last_logins.proto response_remove_user_avatar.proto - response_remove_user_banner.proto - response_clear_blacklisted_banners.proto response_reset_user_password.proto response_user_alts.proto response_user_sessions.proto diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto index 04cb31d98..5298d3dad 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto @@ -20,8 +20,6 @@ message ModeratorCommand { GET_MODERATOR_LAST_LOGINS = 1015; RESET_USER_PASSWORD = 1016; REMOVE_USER_AVATAR = 1017; - REMOVE_USER_BANNER = 1018; - CLEAR_BLACKLISTED_BANNERS = 1019; REPORT_LIST = 1200; REPORT_ASSIGN = 1201; REPORT_RESOLVE = 1202; @@ -87,16 +85,14 @@ message Command_ViewLogHistory { extend DeveloperCommand { optional Command_ViewLogHistory dev_ext = 1001; } - optional string user_name = 1; // user that created message - optional string ip_address = 2; // ip address of user that created message - optional string game_name = 3; // client id of user that created the message - optional string game_id = 4; // game number the message was sent to - optional string message = 5; // raw message that was sent - repeated string log_location = 6; // destination of message (ex: main room, game room, private chat) - required uint32 date_range = 7; // the length of time (in minutes) to look back for - optional uint32 maximum_results = 8; // the maximum number of query results (page size when paginating) - optional uint32 offset = 9 [default = 0]; // rows to skip for pagination, ORDER BY log_time DESC - optional string before_time = 10; // page those rows against this `log_time` snapshot (newest first page row) + optional string user_name = 1; // user that created message + optional string ip_address = 2; // ip address of user that created message + optional string game_name = 3; // client id of user that created the message + optional string game_id = 4; // game number the message was sent to + optional string message = 5; // raw message that was sent + repeated string log_location = 6; // destination of message (ex: main room, game room, private chat) + required uint32 date_range = 7; // the length of time (in minutes) to look back for + optional uint32 maximum_results = 8; // the maximum number of query results } message Command_GrantReplayAccess { @@ -183,16 +179,3 @@ message Command_RemoveUserAvatar { } optional string user_name = 1; } - -message Command_RemoveUserBanner { - extend ModeratorCommand { - optional Command_RemoveUserBanner ext = 1018; - } - optional string user_name = 1; -} - -message Command_ClearBlacklistedBanners { - extend ModeratorCommand { - optional Command_ClearBlacklistedBanners ext = 1019; - } -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_clear_blacklisted_banners.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_clear_blacklisted_banners.proto deleted file mode 100644 index 86cbc92f3..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/response_clear_blacklisted_banners.proto +++ /dev/null @@ -1,9 +0,0 @@ -syntax = "proto2"; -import "response.proto"; - -message Response_ClearBlacklistedBanners { - extend Response { - optional Response_ClearBlacklistedBanners ext = 1222; - } - optional uint32 cleared_count = 1 [default = 0]; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_remove_user_banner.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_remove_user_banner.proto deleted file mode 100644 index bfad7333c..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/response_remove_user_banner.proto +++ /dev/null @@ -1,11 +0,0 @@ -syntax = "proto2"; -import "response.proto"; - -message Response_RemoveUserBanner { - extend Response { - optional Response_RemoveUserBanner ext = 1221; - } - optional string user_name = 1; - // false when the user exists but had no banner card set, so nothing was cleared - optional bool had_banner = 2 [default = true]; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_viewlog_history.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_viewlog_history.proto index 1041bca74..2ec0aff28 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/response_viewlog_history.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response_viewlog_history.proto @@ -7,5 +7,4 @@ message Response_ViewLogHistory { optional Response_ViewLogHistory ext = 1015; } repeated ServerInfo_ChatMessage log_message = 1; - optional bool has_more = 2 [default = false]; // true when more rows exist past this page } diff --git a/libcockatrice_rng/CMakeLists.txt b/libcockatrice_rng/CMakeLists.txt index 77165122c..6128e3f6e 100644 --- a/libcockatrice_rng/CMakeLists.txt +++ b/libcockatrice_rng/CMakeLists.txt @@ -1,7 +1,13 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +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 a3b9a68db..5a2fa42a9 100644 --- a/libcockatrice_settings/CMakeLists.txt +++ b/libcockatrice_settings/CMakeLists.txt @@ -1,3 +1,7 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + set(HEADERS libcockatrice/settings/appearance_settings.h libcockatrice/settings/cache_storage_settings.h @@ -27,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_settings/libcockatrice/settings/servers_settings.cpp b/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp index 38b87b97c..4595da2a2 100644 --- a/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp @@ -5,118 +5,16 @@ #include #include #include -#include -#include +#include #include -#include -#include +#include #include -namespace -{ -constexpr auto SERVER_GROUP = "server"; -constexpr auto SERVER_DETAILS_GROUP = "server_details"; -constexpr auto SERVERS_ARRAY = "servers"; - -QString legacyKey(const char *field, int index) -{ - return QStringLiteral("%1/%2%3") - .arg(QString::fromLatin1(SERVER_DETAILS_GROUP), QString::fromLatin1(field)) - .arg(index); -} - -//! Expects \a settings to be positioned on the server group. -QList readServerArray(QSettings &settings) -{ - QList servers; - - settings.beginGroup(SERVER_DETAILS_GROUP); - const int size = settings.beginReadArray(SERVERS_ARRAY); - servers.reserve(size); - for (int i = 0; i < size; ++i) { - settings.setArrayIndex(i); - ServersSettings::ServerEntry entry; - entry.saveName = settings.value("saveName").toString(); - entry.host = settings.value("server").toString(); - entry.port = settings.value("port").toString(); - entry.username = settings.value("username").toString(); - entry.password = settings.value("password").toString(); - entry.savePassword = settings.value("savePassword").toBool(); - entry.site = settings.value("site").toString(); - servers.append(entry); - } - settings.endArray(); - settings.endGroup(); - - return servers; -} - -//! Expects \a settings to be positioned on the server group. -QList readLegacyServers(QSettings &settings, int lastIndex) -{ - QList servers; - - for (int i = 0; i <= lastIndex; ++i) { - ServersSettings::ServerEntry entry; - entry.saveName = settings.value(legacyKey("saveName", i)).toString(); - entry.host = settings.value(legacyKey("server", i)).toString(); - entry.port = settings.value(legacyKey("port", i)).toString(); - entry.username = settings.value(legacyKey("username", i)).toString(); - entry.password = settings.value(legacyKey("password", i)).toString(); - entry.savePassword = settings.value(legacyKey("savePassword", i)).toBool(); - entry.site = settings.value(legacyKey("site", i)).toString(); - - // Older builds could leave the unused index 0 behind. - if (!entry.saveName.isEmpty() || !entry.host.isEmpty()) { - servers.append(entry); - } - } - - return servers; -} - -void writeServerArray(QSettings &settings, const QList &servers) -{ - settings.beginGroup(SERVER_DETAILS_GROUP); - // beginWriteArray() never drops entries past the new size, so without this - // a removed profile would keep its slot - including its saved password - - // in servers.ini forever. The legacy keys live beside the array, not in it. - settings.remove(SERVERS_ARRAY); - settings.beginWriteArray(SERVERS_ARRAY, servers.size()); - for (int i = 0; i < servers.size(); ++i) { - settings.setArrayIndex(i); - const ServersSettings::ServerEntry &entry = servers.at(i); - settings.setValue("saveName", entry.saveName); - settings.setValue("server", entry.host); - settings.setValue("port", entry.port); - settings.setValue("username", entry.username); - settings.setValue("password", entry.password); - settings.setValue("savePassword", entry.savePassword); - settings.setValue("site", entry.site); - } - settings.endArray(); - settings.endGroup(); -} -} // namespace +class QObject; ServersSettings::ServersSettings(const QString &settingPath, QObject *parent) - : SettingsManager(settingPath + "servers.ini", SERVER_GROUP, QString(), parent) + : SettingsManager(settingPath + "servers.ini", "server", QString(), parent) { - migrateLegacyServers(); -} - -QList ServersSettings::getServers() const -{ - return loadServers(); -} - -std::optional ServersSettings::getServer(int index) const -{ - const QList servers = loadServers(); - if (index < 0 || index >= servers.size()) { - return std::nullopt; - } - return servers.at(index); } void ServersSettings::setPreviousHostLogin(int previous) @@ -147,14 +45,16 @@ void ServersSettings::setPrevioushostName(const QString &name) QString ServersSettings::getSaveName(QString defaultname) { - const auto server = getServer(getPrevioushostindex(getPrevioushostName())); - return server ? server->saveName : std::move(defaultname); + int index = getPrevioushostindex(getPrevioushostName()); + QVariant saveName = getValue(QString("saveName%1").arg(index), "server", "server_details"); + return saveName == QVariant() ? std::move(defaultname) : saveName.toString(); } QString ServersSettings::getSite(QString defaultSite) { - const auto server = getServer(getPrevioushostindex(getPrevioushostName())); - return server ? server->site : std::move(defaultSite); + int index = getPrevioushostindex(getPrevioushostName()); + QVariant site = getValue(QString("site%1").arg(index), "server", "server_details"); + return site == QVariant() ? std::move(defaultSite) : site.toString(); } QString ServersSettings::getPrevioushostName() const @@ -165,9 +65,10 @@ QString ServersSettings::getPrevioushostName() const int ServersSettings::getPrevioushostindex(const QString &saveName) const { - const QList servers = loadServers(); - for (int i = 0; i < servers.size(); ++i) { - if (servers.at(i).saveName == saveName) { + int size = getValue("totalServers", "server", "server_details").toInt(); + + for (int i = 0; i <= size; ++i) { + if (saveName == getValue(QString("saveName%1").arg(i), "server", "server_details").toString()) { return i; } } @@ -177,38 +78,43 @@ int ServersSettings::getPrevioushostindex(const QString &saveName) const QString ServersSettings::getHostname(QString defaultHost) const { - const auto server = getServer(getPrevioushostindex(getPrevioushostName())); - return server ? server->host : std::move(defaultHost); + int index = getPrevioushostindex(getPrevioushostName()); + QVariant hostname = getValue(QString("server%1").arg(index), "server", "server_details"); + return hostname == QVariant() ? std::move(defaultHost) : hostname.toString(); } QString ServersSettings::getPort(QString defaultPort) const { - const auto server = getServer(getPrevioushostindex(getPrevioushostName())); - qCDebug(ServersSettingsLog) << "getPort() found server = " << server.has_value(); - return server ? server->port : std::move(defaultPort); + int index = getPrevioushostindex(getPrevioushostName()); + QVariant port = getValue(QString("port%1").arg(index), "server", "server_details"); + qCDebug(ServersSettingsLog) << "getPort() index = " << index << " port.val = " << port.toString(); + return port == QVariant() ? std::move(defaultPort) : port.toString(); } QString ServersSettings::getPlayerName(QString defaultName) const { - const auto server = getServer(getPrevioushostindex(getPrevioushostName())); - qCDebug(ServersSettingsLog) << "getPlayerName() found server = " << server.has_value(); - return server ? server->username : std::move(defaultName); + int index = getPrevioushostindex(getPrevioushostName()); + QVariant name = getValue(QString("username%1").arg(index), "server", "server_details"); + qCDebug(ServersSettingsLog) << "getPlayerName() index = " << index << " name.val = " << name.toString(); + return name == QVariant() ? std::move(defaultName) : name.toString(); } QString ServersSettings::getPassword() { - const auto server = getServer(getPrevioushostindex(getPrevioushostName())); - if (!server || !server->savePassword) { - return QString(); + int index = getPrevioushostindex(getPrevioushostName()); + + if (getSavePassword()) { + return getValue(QString("password%1").arg(index), "server", "server_details").toString(); } - return server->password; + return QString(); } bool ServersSettings::getSavePassword() const { - const auto server = getServer(getPrevioushostindex(getPrevioushostName())); - return server && server->savePassword; + int index = getPrevioushostindex(getPrevioushostName()); + bool save = getValue(QString("savePassword%1").arg(index), "server", "server_details").toBool(); + return save; } void ServersSettings::setAutoConnect(int autoconnect) @@ -251,19 +157,15 @@ void ServersSettings::setFPPlayerName(QString playerName) void ServersSettings::setServerPassword(const QString &saveName, const QString &password) { - QList servers = loadServers(); - for (ServerEntry &server : servers) { - if (server.saveName == saveName) { - server.password = password; - storeServers(servers); - return; - } + const int index = getPrevioushostindex(saveName); + if (index >= 0) { + setValue(password, QString("password%1").arg(index), "server", "server_details"); + } else { + // A credential write that silently no-ops is hard to diagnose from a + // bug report; surface a mismatched profile name instead. + qCWarning(ServersSettingsLog) << "setServerPassword() could not find profile:" << saveName + << "- password not saved"; } - - // A credential write that silently no-ops is hard to diagnose from a bug - // report; surface a mismatched profile name instead. - qCWarning(ServersSettingsLog) << "setServerPassword() could not find profile:" << saveName - << "- password not saved"; } QString ServersSettings::getFPPlayerName(QString defaultName) const @@ -300,29 +202,62 @@ void ServersSettings::addNewServer(const QString &saveName, return; } - QList servers = loadServers(); - ServerEntry entry; - entry.saveName = saveName; - entry.host = serv; - entry.port = port; - entry.username = username; - entry.password = password; - entry.savePassword = savePassword; - entry.site = site; - servers.append(entry); - storeServers(servers); + int index = getValue("totalServers", "server", "server_details").toInt() + 1; + + setValue(saveName, QString("saveName%1").arg(index), "server", "server_details"); + setValue(serv, QString("server%1").arg(index), "server", "server_details"); + setValue(port, QString("port%1").arg(index), "server", "server_details"); + setValue(username, QString("username%1").arg(index), "server", "server_details"); + setValue(savePassword, QString("savePassword%1").arg(index), "server", "server_details"); + setValue(index, "totalServers", "server", "server_details"); + setValue(password, QString("password%1").arg(index), "server", "server_details"); + setValue(site, QString("site%1").arg(index), "server", "server_details"); } void ServersSettings::removeServer(QString servAddr) { - QList servers = loadServers(); - for (int i = 0; i < servers.size(); ++i) { - if (servers.at(i).host == servAddr) { - servers.removeAt(i); - storeServers(servers); - return; + int size = getValue("totalServers", "server", "server_details").toInt(); + + bool found = false; + for (int i = 0; i <= size; ++i) { + if (!found) { + // find entry and overwrite it + if (servAddr == getValue(QString("server%1").arg(i), "server", "server_details").toString()) { + found = true; + } + } else { + // move all other entries after it one back, overwriting the previous one + int previous = i - 1; // we delete only one entry + setValue(getValue(QString("server%1").arg(i), "server", "server_details"), + QString("server%1").arg(previous), "server", "server_details"); + setValue(getValue(QString("port%1").arg(i), "server", "server_details"), QString("port%1").arg(previous), + "server", "server_details"); + setValue(getValue(QString("username%1").arg(i), "server", "server_details"), + QString("username%1").arg(previous), "server", "server_details"); + setValue(getValue(QString("savePassword%1").arg(i), "server", "server_details"), + QString("savePassword%1").arg(previous), "server", "server_details"); + setValue(getValue(QString("password%1").arg(i), "server", "server_details"), + QString("password%1").arg(previous), "server", "server_details"); + setValue(getValue(QString("saveName%1").arg(i), "server", "server_details"), + QString("saveName%1").arg(previous), "server", "server_details"); + setValue(getValue(QString("site%1").arg(i), "server", "server_details"), QString("site%1").arg(previous), + "server", "server_details"); } } + + // if we have deleted an entry, adjust the total + if (found) { + setValue(size - 1, "totalServers", "server", "server_details"); + + // delete last value + deleteValue(QString("server%1").arg(size), "server", "server_details"); + deleteValue(QString("port%1").arg(size), "server", "server_details"); + deleteValue(QString("username%1").arg(size), "server", "server_details"); + deleteValue(QString("savePassword%1").arg(size), "server", "server_details"); + deleteValue(QString("password%1").arg(size), "server", "server_details"); + deleteValue(QString("saveName%1").arg(size), "server", "server_details"); + deleteValue(QString("site%1").arg(size), "server", "server_details"); + } } /** @@ -330,23 +265,23 @@ void ServersSettings::removeServer(QString servAddr) */ bool ServersSettings::updateExistingServerWithoutLoss(QString saveName, QString serv, QString port, QString site) { - QList servers = loadServers(); - for (ServerEntry &server : servers) { - if (server.host != serv) { - continue; - } + int size = getValue("totalServers", "server", "server_details").toInt(); - if (!port.isEmpty()) { - server.port = port; + for (int i = 0; i <= size; ++i) { + if (serv == getValue(QString("server%1").arg(i), "server", "server_details").toString()) { + if (!port.isEmpty()) { + setValue(port, QString("port%1").arg(i), "server", "server_details"); + } + + if (!site.isEmpty()) { + setValue(site, QString("site%1").arg(i), "server", "server_details"); + } + + setValue(saveName, QString("saveName%1").arg(i), "server", "server_details"); + + return true; } - if (!site.isEmpty()) { - server.site = site; - } - server.saveName = saveName; - storeServers(servers); - return true; } - return false; } @@ -358,13 +293,17 @@ bool ServersSettings::updateExistingServer(QString saveName, bool savePassword, QString site) { - const int index = findServerIndex(serv, port); - if (index < 0) { - return false; - } + int size = getValue("totalServers", "server", "server_details").toInt(); - updateServerFields(index, saveName, username, password, savePassword, site); - return true; + for (int i = 0; i <= size; ++i) { + if (serv == getValue(QString("server%1").arg(i), "server", "server_details").toString()) { + setValue(port, QString("port%1").arg(i), "server", "server_details"); + updateServerFields(i, saveName, username, password, savePassword, site); + + return true; + } + } + return false; } void ServersSettings::updateServerFields(int index, @@ -374,33 +313,33 @@ void ServersSettings::updateServerFields(int index, bool savePassword, const QString &site) { - QList servers = loadServers(); - if (index < 0 || index >= servers.size()) { - return; + if (!username.isEmpty()) { + setValue(username, QString("username%1").arg(index), "server", "server_details"); } - ServerEntry &server = servers[index]; - if (!username.isEmpty()) { - server.username = username; - } if (savePassword && !password.isEmpty()) { - server.password = password; + setValue(password, QString("password%1").arg(index), "server", "server_details"); } else { - server.password.clear(); + setValue(QString(), QString("password%1").arg(index), "server", "server_details"); } + if (!site.isEmpty()) { - server.site = site; + setValue(site, QString("site%1").arg(index), "server", "server_details"); } - server.savePassword = savePassword; - server.saveName = saveName; - storeServers(servers); + + setValue(savePassword, QString("savePassword%1").arg(index), "server", "server_details"); + setValue(saveName, QString("saveName%1").arg(index), "server", "server_details"); } int ServersSettings::findServerIndex(const QString &host, const QString &port) const { - const QList servers = loadServers(); - for (int i = 0; i < servers.size(); ++i) { - if (servers.at(i).host == host && servers.at(i).port == port) { + int size = getValue("totalServers", "server", "server_details").toInt(); + + for (int i = 0; i <= size; ++i) { + QString storedHost = getValue(QString("server%1").arg(i), "server", "server_details").toString(); + QString storedPort = getValue(QString("port%1").arg(i), "server", "server_details").toString(); + + if (storedHost == host && storedPort == port) { return i; } } @@ -410,9 +349,12 @@ int ServersSettings::findServerIndex(const QString &host, const QString &port) c int ServersSettings::findHostIndex(const QString &host) const { - const QList servers = loadServers(); - for (int i = 0; i < servers.size(); ++i) { - if (servers.at(i).host.compare(host, Qt::CaseInsensitive) == 0) { + int size = getValue("totalServers", "server", "server_details").toInt(); + + for (int i = 0; i <= size; ++i) { + QString storedHost = getValue(QString("server%1").arg(i), "server", "server_details").toString(); + + if (storedHost.compare(host, Qt::CaseInsensitive) == 0) { return i; } } @@ -422,100 +364,29 @@ int ServersSettings::findHostIndex(const QString &host) const bool ServersSettings::hasUsername(const QString &host, const QString &port) const { - const auto server = getServer(findServerIndex(host, port)); - return server && !server->username.isEmpty(); + int index = findServerIndex(host, port); + if (index < 0) { + return false; + } + + QString user = getValue(QString("username%1").arg(index), "server", "server_details").toString(); + return !user.isEmpty(); } bool ServersSettings::hasCredentials(const QString &host, const QString &port) const { - const auto server = getServer(findServerIndex(host, port)); - return server && server->savePassword && !server->password.isEmpty(); + int index = findServerIndex(host, port); + if (index < 0) { + return false; + } + + bool save = getValue(QString("savePassword%1").arg(index), "server", "server_details").toBool(); + QString password = getValue(QString("password%1").arg(index), "server", "server_details").toString(); + + return save && !password.isEmpty(); } bool ServersSettings::hasLoginData(const QString &host, const QString &port) const { return hasUsername(host, port) && hasCredentials(host, port); } - -QList ServersSettings::loadServers() const -{ - QSettings settings = getSettings(); - settings.beginGroup(SERVER_GROUP); - const QList servers = readServerArray(settings); - settings.endGroup(); - - return servers; -} - -void ServersSettings::storeServers(const QList &servers) -{ - batchWrite([&servers](QSettings &settings) { - settings.beginGroup(SERVER_GROUP); - writeServerArray(settings, servers); - settings.endGroup(); - }); -} - -void ServersSettings::migrateLegacyServers() -{ - { - QSettings settings = getSettings(); - settings.beginGroup(SERVER_GROUP); - // Only builds that predate the array format write this marker. Once it - // is gone there is nothing left to import and no reason to touch the - // file (a sync on every start is a needless open-and-write). - const bool hasLegacy = settings.contains(QStringLiteral("%1/totalServers").arg(SERVER_DETAILS_GROUP)); - settings.endGroup(); - - if (!hasLegacy) { - return; - } - } - - const QSettings::Status status = batchWrite([](QSettings &settings) { - settings.beginGroup(SERVER_GROUP); - - const int lastLegacyIndex = - settings.value(QStringLiteral("%1/totalServers").arg(SERVER_DETAILS_GROUP), -1).toInt(); - - QList servers = readServerArray(settings); - - // An older build can write the legacy keys again after the array - // migration, so the array is not automatically complete: append every - // legacy profile the array does not already hold instead of dropping it. - int imported = 0; - const QList legacyServers = readLegacyServers(settings, lastLegacyIndex); - for (const ServersSettings::ServerEntry &entry : legacyServers) { - const bool known = - std::any_of(servers.cbegin(), servers.cend(), [&entry](const ServersSettings::ServerEntry &existing) { - return existing.host == entry.host && existing.port == entry.port; - }); - if (!known) { - servers.append(entry); - ++imported; - } - } - - writeServerArray(settings, servers); - if (imported > 0) { - qCDebug(ServersSettingsLog) << "Migrated" << imported << "server profiles to the array format"; - } - - // The array (freshly written or already present) is authoritative now. - for (int i = 0; i <= lastLegacyIndex; ++i) { - settings.remove(legacyKey("saveName", i)); - settings.remove(legacyKey("server", i)); - settings.remove(legacyKey("port", i)); - settings.remove(legacyKey("username", i)); - settings.remove(legacyKey("password", i)); - settings.remove(legacyKey("savePassword", i)); - settings.remove(legacyKey("site", i)); - } - settings.remove(QStringLiteral("%1/totalServers").arg(SERVER_DETAILS_GROUP)); - settings.endGroup(); - }); - - if (status != QSettings::NoError) { - qCWarning(ServersSettingsLog) << "Failed to migrate server profiles:" << status; - } -} diff --git a/libcockatrice_settings/libcockatrice/settings/servers_settings.h b/libcockatrice_settings/libcockatrice/settings/servers_settings.h index b403b1cea..38bb02876 100644 --- a/libcockatrice_settings/libcockatrice/settings/servers_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/servers_settings.h @@ -9,11 +9,9 @@ #include "settings_manager.h" -#include #include #include #include -#include #include class QObject; @@ -29,22 +27,6 @@ class ServersSettings : public SettingsManager friend class SettingsCache; public: - struct ServerEntry - { - QString saveName; - QString host; - QString port; - QString username; - QString password; - bool savePassword = false; - QString site; - }; - - explicit ServersSettings(const QString &settingPath, QObject *parent = nullptr); - - QList getServers() const; - std::optional getServer(int index) const; - int getPreviousHostLogin() const; int getPrevioushostindex(const QString &) const; QStringList getPreviousHostList() const; @@ -106,11 +88,8 @@ public: bool getClearDebugLogStatus(bool abDefaultValue) const; private: + explicit ServersSettings(const QString &settingPath, QObject *parent = nullptr); ServersSettings(const ServersSettings & /*other*/); - - QList loadServers() const; - void storeServers(const QList &servers); - void migrateLegacyServers(); }; #endif // SERVERSSETTINGS_H diff --git a/libcockatrice_settings/libcockatrice/settings/settings_manager.cpp b/libcockatrice_settings/libcockatrice/settings/settings_manager.cpp index 7cc1fd7be..f98778ca0 100644 --- a/libcockatrice_settings/libcockatrice/settings/settings_manager.cpp +++ b/libcockatrice_settings/libcockatrice/settings/settings_manager.cpp @@ -193,12 +193,13 @@ QVariant SettingsManager::getValue(const QString &name, return value; } -QSettings::Status SettingsManager::batchWrite(std::function batchWriteFunction) +void SettingsManager::batchWrite(std::function batchWriteFunction) { auto settings = getSettings(); + settings.setAtomicSyncRequired(false); batchWriteFunction(settings); settings.sync(); // single flush - return settings.status(); + settings.setAtomicSyncRequired(true); } /** diff --git a/libcockatrice_settings/libcockatrice/settings/settings_manager.h b/libcockatrice_settings/libcockatrice/settings/settings_manager.h index 760d095e6..8d3110106 100644 --- a/libcockatrice_settings/libcockatrice/settings/settings_manager.h +++ b/libcockatrice_settings/libcockatrice/settings/settings_manager.h @@ -28,9 +28,7 @@ public: QVariant getValue(const QString &name, const QString &group, const QString &subGroup = QString()) const; QVariant getValue(const QString &name, const QString &group, const QString &subGroup, const QVariant &defaultValue) const; - //! \brief Runs \a batchWriteFunction on a temporary QSettings and syncs once. - //! \return the status of the underlying file after the final sync. - QSettings::Status batchWrite(std::function batchWriteFunction); + void batchWrite(std::function batchWriteFunction); void sync(); diff --git a/libcockatrice_utility/CMakeLists.txt b/libcockatrice_utility/CMakeLists.txt index 98ad40e69..a98318945 100644 --- a/libcockatrice_utility/CMakeLists.txt +++ b/libcockatrice_utility/CMakeLists.txt @@ -1,6 +1,10 @@ cmake_minimum_required(VERSION 3.16) project(Utility VERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}") +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + set(UTILITY_SOURCES libcockatrice/utility/cryptoutil.cpp libcockatrice/utility/expression.cpp @@ -18,15 +22,12 @@ set(UTILITY_HEADERS libcockatrice/utility/card_ref.h libcockatrice/utility/color.h 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 libcockatrice/utility/passwordhasher.h libcockatrice/utility/playmat_params.h - libcockatrice/utility/qt_utils.h libcockatrice/utility/report_categories.h libcockatrice/utility/string_limits.h libcockatrice/utility/dice_limits.h diff --git a/libcockatrice_utility/libcockatrice/utility/debouncer.h b/libcockatrice_utility/libcockatrice/utility/debouncer.h deleted file mode 100644 index 54635db4f..000000000 --- a/libcockatrice_utility/libcockatrice/utility/debouncer.h +++ /dev/null @@ -1,62 +0,0 @@ -#ifndef COCKATRICE_DEBOUNCER_H -#define COCKATRICE_DEBOUNCER_H - -#include -#include -#include -#include - -namespace QtUtils -{ -/** - * @brief Runs the most recent call() after a period without further calls. - * - * Typical use is coalescing rapid model or editor updates into one refresh: - * - * debouncer.call([this] { refresh(); }); - * - * The pending callback is owned by the debouncer and replaced when call() is invoked - * again. The debouncer must outlive any callback it schedules; parenting it to the - * owning widget is usually enough. - */ -class Debouncer : public QObject -{ -public: - explicit Debouncer(int intervalMs, QObject *parent = nullptr) : QObject(parent) - { - timer.setSingleShot(true); - timer.setInterval(intervalMs); - connect(&timer, &QTimer::timeout, this, [this] { - if (callback) { - auto pending = std::move(callback); - callback = nullptr; - pending(); - } - }); - } - - /** - * @brief Schedules @p newCallback, replacing any callback not run yet. - */ - void call(std::function newCallback) - { - callback = std::move(newCallback); - timer.start(); - } - - /** - * @brief Drops the pending callback and stops the timer. - */ - void cancel() - { - timer.stop(); - callback = nullptr; - } - -private: - QTimer timer; - std::function callback; -}; -} // namespace QtUtils - -#endif // COCKATRICE_DEBOUNCER_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/levenshtein.cpp b/libcockatrice_utility/libcockatrice/utility/levenshtein.cpp index 11eeef19b..f7d8459cb 100644 --- a/libcockatrice_utility/libcockatrice/utility/levenshtein.cpp +++ b/libcockatrice_utility/libcockatrice/utility/levenshtein.cpp @@ -7,40 +7,23 @@ int levenshteinDistance(const QString &s1, const QString &s2) { - const qsizetype len1 = s1.size(); - const qsizetype len2 = s2.size(); + int len1 = s1.size(); + int len2 = s2.size(); + std::vector> dp(len1 + 1, std::vector(len2 + 1)); - if (len1 == 0) { - return static_cast(len2); + for (int i = 0; i <= len1; i++) { + dp[i][0] = i; } - if (len2 == 0) { - return static_cast(len1); - } - if (s1 == s2) { - return 0; + for (int j = 0; j <= len2; j++) { + dp[0][j] = j; } - // Only the previous row of the distance matrix is needed to compute the - // next one, so two rows replace the full len1 x len2 matrix. The card - // search runs this for every matching card on each keystroke. - std::vector previous(len2 + 1); - std::vector current(len2 + 1); - - for (qsizetype j = 0; j <= len2; j++) { - previous[j] = static_cast(j); - } - - for (qsizetype i = 1; i <= len1; i++) { - current[0] = static_cast(i); - const QChar c1 = s1.at(i - 1); - - for (qsizetype j = 1; j <= len2; j++) { - const int cost = (c1 == s2.at(j - 1)) ? 0 : 1; - current[j] = std::min(previous[j] + 1, std::min(current[j - 1] + 1, previous[j - 1] + cost)); + for (int i = 1; i <= len1; i++) { + for (int j = 1; j <= len2; j++) { + int cost = (s1[i - 1] == s2[j - 1]) ? 0 : 1; + dp[i][j] = std::min({dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost}); } - - std::swap(previous, current); } - return previous[len2]; + return dp[len1][len2]; } diff --git a/libcockatrice_utility/libcockatrice/utility/passwordhasher.cpp b/libcockatrice_utility/libcockatrice/utility/passwordhasher.cpp index 8e876d5bc..a7106aadc 100644 --- a/libcockatrice_utility/libcockatrice/utility/passwordhasher.cpp +++ b/libcockatrice_utility/libcockatrice/utility/passwordhasher.cpp @@ -62,11 +62,9 @@ QByteArray PasswordHasher::deriveKey(const QString &password, const QByteArray & { QByteArray key(SCRYPT_VERIFIER_LENGTH, '\0'); const QByteArray passwordUtf8 = password.toUtf8(); - // EVP_PBE_scrypt aborts unless maxmem covers the required working memory: V is 128 * n * - // r bytes, B is 128 * r * p, and XY is 256 * r. Under-budgeting XY makes OpenSSL fail - // for r >= 17, which costParamsAreSane still accepts. - const auto maxmem = - static_cast(128) * n * r + static_cast(128) * r * p + static_cast(256) * r + 4096; + // EVP_PBE_scrypt aborts unless maxmem covers the required working memory, + // which is roughly 128 * n * r bytes (plus the small Salsa20/8 block array). + const auto maxmem = static_cast(128) * n * r + static_cast(128) * r * p + 4096; if (EVP_PBE_scrypt(passwordUtf8.constData(), passwordUtf8.size(), reinterpret_cast(salt.constData()), salt.size(), n, r, p, maxmem, reinterpret_cast(key.data()), key.size()) != 1) { diff --git a/libcockatrice_utility/libcockatrice/utility/qt_utils.h b/libcockatrice_utility/libcockatrice/utility/qt_utils.h index 1d06c4d72..8e5212031 100644 --- a/libcockatrice_utility/libcockatrice/utility/qt_utils.h +++ b/libcockatrice_utility/libcockatrice/utility/qt_utils.h @@ -1,33 +1,13 @@ #ifndef COCKATRICE_QT_UTILS_H #define COCKATRICE_QT_UTILS_H -#include -#include -#include #include -#include #include -#include -#include -#include -#include -#include namespace QtUtils { -/** - * @brief Returns the nearest ancestor of @p obj that casts to @c T. - * - * Walks the QObject parent chain. QWidget::parentWidget() is the same pointer as - * QObject::parent() for widgets, so this also covers the common "find the parent tab, - * splitter or scroll area" lookups. - * - * @param obj Object to start from. May be null. - * @param includeSelf Also test @p obj itself before walking upwards. - * @return The first matching ancestor, or nullptr if there is none. - */ -template T *findParentOfType(const QObject *obj, bool includeSelf = false) +template T *findParentOfType(const QObject *obj) { - const QObject *p = includeSelf ? obj : (obj ? obj->parent() : nullptr); + const QObject *p = obj ? obj->parent() : nullptr; while (p) { if (auto casted = qobject_cast(const_cast(p))) { return casted; @@ -37,172 +17,22 @@ template T *findParentOfType(const QObject *obj, bool includeSelf = return nullptr; } -/** - * @brief How clearLayoutRec() disposes of the widgets it removes. - * - * Deferred is the safe default: deleteLater() lets a widget finish handling the event that - * triggered the rebuild. Use Immediate when the caller rebuilds the same layout right away - * and a lingering widget would be visible or otherwise in the way until the next event loop - * turn. - */ -enum class DeletePolicy -{ - Deferred, - Immediate -}; - -/** - * @brief Removes and deletes every item in @p l, recursing into nested layouts. - * - * Widgets are disposed of according to @p policy; spacers and nested layouts are simply - * deleted. - */ -static inline void clearLayoutRec(QLayout *l, DeletePolicy policy = DeletePolicy::Deferred) +static inline void clearLayoutRec(QLayout *l) { if (!l) { return; } - while (QLayoutItem *it = l->takeAt(0)) { + QLayoutItem *it; + while ((it = l->takeAt(0)) != nullptr) { if (QWidget *w = it->widget()) { - if (policy == DeletePolicy::Immediate) { - delete w; - } else { - w->deleteLater(); - } + w->deleteLater(); } if (QLayout *sub = it->layout()) { - clearLayoutRec(sub, policy); + clearLayoutRec(sub); } delete it; } } - -/** - * @brief Creates a layout of type @c LayoutT with zero margins. - * - * @param parent Parent widget, or nullptr when the layout is added later. - * @param spacing Optional spacing. Values below zero leave the style's default spacing - * untouched; pass a non-negative value to call setSpacing(). - * @param margin Uniform margin. Defaults to zero. - */ -template LayoutT *makeLayout(QWidget *parent = nullptr, int spacing = -1, int margin = 0) -{ - auto *layout = new LayoutT(parent); - layout->setContentsMargins(margin, margin, margin, margin); - if (spacing >= 0) { - layout->setSpacing(spacing); - } - return layout; -} - -/** - * @brief Forces @p layout to recompute and repaint immediately. - * - * Convenience for the invalidate/activate/update sequence used after changing a layout's - * contents outside of a resize event. - */ -inline void relayout(QLayout *layout) -{ - if (!layout) { - return; - } - layout->invalidate(); - layout->activate(); - layout->update(); -} - -/** - * @brief Creates a QScrollArea using the conventions the client applies nearly everywhere: - * the inner widget resizes with the viewport and the horizontal bar is disabled. - * - * @param parent Parent widget, or nullptr for a parentless scroll area. - * @param verticalPolicy Vertical scroll bar policy. - * @param horizontalPolicy Horizontal scroll bar policy. - */ -static inline QScrollArea *makeScrollArea(QWidget *parent = nullptr, - Qt::ScrollBarPolicy verticalPolicy = Qt::ScrollBarAsNeeded, - Qt::ScrollBarPolicy horizontalPolicy = Qt::ScrollBarAlwaysOff) -{ - auto *scrollArea = new QScrollArea(parent); - scrollArea->setWidgetResizable(true); - scrollArea->setVerticalScrollBarPolicy(verticalPolicy); - scrollArea->setHorizontalScrollBarPolicy(horizontalPolicy); - return scrollArea; -} - -/** - * @brief A monospace font at the system UI font's point size. - * - * Used for log dumps, deck codes and other text that has to line up. - */ -static inline QFont monospaceFont() -{ - QFont font(QStringLiteral("monospace")); - font.setStyleHint(QFont::Monospace); - const int systemPointSize = QFontDatabase::systemFont(QFontDatabase::GeneralFont).pointSize(); - if (systemPointSize > 0) { - font.setPointSize(systemPointSize); - } - return font; -} - -/** - * @brief Fades @p window between two opacity values. - * - * @param window Window to animate. - * @param startOpacity Starting opacity. - * @param endOpacity Ending opacity. - * @param durationMs Duration in milliseconds. - * @param hideWhenFinished Hide the window when the animation finishes. - * @return The running animation. It is owned by @p window and deletes itself when stopped. - */ -static inline QPropertyAnimation * -fadeWindow(QWidget *window, qreal startOpacity, qreal endOpacity, int durationMs, bool hideWhenFinished = false) -{ - auto *animation = new QPropertyAnimation(window, "windowOpacity", window); - animation->setDuration(durationMs); - animation->setStartValue(startOpacity); - animation->setEndValue(endOpacity); - if (hideWhenFinished) { - QObject::connect(animation, &QPropertyAnimation::finished, window, &QWidget::hide); - } - animation->start(QAbstractAnimation::DeleteWhenStopped); - return animation; -} - -/** - * @brief Opens @p path with the platform's file manager or default handler. - */ -static inline bool openPathInFileManager(const QString &path) -{ - return QDesktopServices::openUrl(QUrl::fromLocalFile(path)); -} - -/** - * @brief Returns @p font with bold styling enabled. - */ -static inline QFont boldFont(QFont font) -{ - font.setBold(true); - return font; -} - -/** - * @brief Shows a Yes/No confirmation dialog and returns true when the user confirms. - * - * @param parent Parent widget. - * @param title Dialog title. - * @param text Dialog text. - * @param defaultButton Button focused by default. NoButton lets Qt choose. - */ -static inline bool confirm(QWidget *parent, - const QString &title, - const QString &text, - QMessageBox::StandardButton defaultButton = QMessageBox::NoButton) -{ - return QMessageBox::question(parent, title, text, QMessageBox::Yes | QMessageBox::No, defaultButton) == - QMessageBox::Yes; -} } // namespace QtUtils #endif // COCKATRICE_QT_UTILS_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 202b7c75f..38147fe2b 100644 --- a/oracle/CMakeLists.txt +++ b/oracle/CMakeLists.txt @@ -64,7 +64,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 @@ -109,7 +109,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 @@ -284,7 +284,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} @@ -294,7 +294,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) @@ -320,4 +320,4 @@ if(Qt6LinguistTools_FOUND) endforeach() endif() -qt_finalize_target(oracle) +qt6_finalize_target(oracle) diff --git a/oracle/src/oracleimporter.cpp b/oracle/src/oracleimporter.cpp index 477c54d21..3382e36d6 100644 --- a/oracle/src/oracleimporter.cpp +++ b/oracle/src/oracleimporter.cpp @@ -486,7 +486,7 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJson } } - PrintingInfo printingInfo(currentSet, LazyPropertiesHash::serializedFrom(printingProps)); + PrintingInfo printingInfo(currentSet, LazyPropertiesHash(printingProps)); QString numComponent; const QString numProperty = printingInfo.getProperty("num"); diff --git a/oracle/src/oraclewizard.cpp b/oracle/src/oraclewizard.cpp index c33d821a5..d2b3a71a4 100644 --- a/oracle/src/oraclewizard.cpp +++ b/oracle/src/oraclewizard.cpp @@ -22,7 +22,6 @@ #include #include #include -#include #include #include #include @@ -32,6 +31,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 @@ -51,7 +53,7 @@ OracleWizard::OracleWizard(QWidget *parent) : QWizard(parent) // foreignData for any other language is never imported. importer->setCardLang(SettingsCache::instance().cardsDisplay().getCardLang()); - nam = NetworkManagerFactory::create(this); + nam = new QNetworkAccessManager(this); QList pages; 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/servatrice.ini.example b/servatrice/servatrice.ini.example index ac291fd7f..021f5157f 100644 --- a/servatrice/servatrice.ini.example +++ b/servatrice/servatrice.ini.example @@ -455,11 +455,9 @@ authentication_strictness=mixed ; the scrypt verifier, so old clients can still log in during the window below without ; the database holding a fast-crackable hash in the clear. ; Generate with: openssl rand -hex 32 -; If unset there is no downgrade path at all: a migrated account keeps only a scrypt -; verifier, and an administrator's password reset stores a verifier too, so pre-challenge -; clients cannot log in with the temporary password either. Set the key if you still serve -; them. Rotating the key invalidates existing backups (they become unusable immediately; -; expired ones are still purged). +; If unset, migrated accounts have no downgrade window: old clients must use a password +; reset to get back in. Rotating the key invalidates existing backups (they become +; unusable immediately; expired ones are still purged). legacy_backup_key= ; Days a legacy password backup may go unused before it is purged from the database. diff --git a/servatrice/src/isl_interface.cpp b/servatrice/src/isl_interface.cpp index 1898cd5fa..8733c9a40 100644 --- a/servatrice/src/isl_interface.cpp +++ b/servatrice/src/isl_interface.cpp @@ -58,16 +58,17 @@ void IslInterface::sharedCtor(const QSslCertificate &cert, const QSslKey &privat socket->setLocalCertificate(cert); socket->setPrivateKey(privateKey); - connect(socket, &QSslSocket::readyRead, this, &IslInterface::readClient, Qt::QueuedConnection); - connect(socket, &QAbstractSocket::errorOccurred, this, &IslInterface::catchSocketError); - connect(this, &IslInterface::outputBufferChanged, this, &IslInterface::flushOutputBuffer, Qt::QueuedConnection); + connect(socket, SIGNAL(readyRead()), this, SLOT(readClient()), Qt::QueuedConnection); + connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), this, + SLOT(catchSocketError(QAbstractSocket::SocketError))); + connect(this, SIGNAL(outputBufferChanged()), this, SLOT(flushOutputBuffer()), Qt::QueuedConnection); } 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); } @@ -263,17 +264,13 @@ void IslInterface::readClient() server->incRxBytes(data.size()); inputBuffer.append(data); - // Parse messages straight out of inputBuffer at readOffset. Removing each - // consumed message from the front would memmove the remaining bytes once - // per message; the consumed prefix is dropped once parsing stops. - qsizetype readOffset = 0; - - while (true) { + do { if (!messageInProgress) { - if (inputBuffer.size() - readOffset >= 4) { - const auto *header = reinterpret_cast(inputBuffer.constData() + readOffset); - messageLength = (static_cast(header[0]) << 24) + (static_cast(header[1]) << 16) + - (static_cast(header[2]) << 8) + static_cast(header[3]); + if (inputBuffer.size() >= 4) { + messageLength = (((quint32)(unsigned char)inputBuffer[0]) << 24) + + (((quint32)(unsigned char)inputBuffer[1]) << 16) + + (((quint32)(unsigned char)inputBuffer[2]) << 8) + + ((quint32)(unsigned char)inputBuffer[3]); // Reject implausible lengths instead of buffering until they arrive. // This also handles lengths that overflow into negative int values. if (messageLength < 0 || messageLength > MAX_ISL_MESSAGE_SIZE) { @@ -281,19 +278,19 @@ void IslInterface::readClient() socket->disconnectFromHost(); return; } - readOffset += 4; + inputBuffer.remove(0, 4); messageInProgress = true; } else { - break; + return; } } - if (inputBuffer.size() - readOffset < messageLength) { - break; + if (inputBuffer.size() < messageLength) { + return; } IslMessage newMessage; - bool ok = newMessage.ParseFromArray(inputBuffer.constData() + readOffset, messageLength); - readOffset += messageLength; + bool ok = newMessage.ParseFromArray(inputBuffer.data(), messageLength); + inputBuffer.remove(0, messageLength); messageInProgress = false; if (ok) { @@ -301,12 +298,7 @@ void IslInterface::readClient() } else { qCWarning(IslInterfaceLog) << "parsing error!"; } - } - - // Drop the consumed prefix once, instead of shifting the buffer per message. - if (readOffset > 0) { - inputBuffer.remove(0, readOffset); - } + } while (!inputBuffer.isEmpty()); } void IslInterface::catchSocketError(QAbstractSocket::SocketError socketError) @@ -503,9 +495,7 @@ void IslInterface::processRoomCommand(const CommandContainer &cont, qint64 sessi void IslInterface::processMessage(const IslMessage &item) { - if (IslInterfaceLog().isDebugEnabled()) { - qCDebug(IslInterfaceLog) << getSafeDebugString(item); - } + qCDebug(IslInterfaceLog) << getSafeDebugString(item); switch (item.message_type()) { case IslMessage::ROOM_COMMAND_CONTAINER: { 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 afe028f32..b6eaf11f4 100644 --- a/servatrice/src/servatrice.cpp +++ b/servatrice/src/servatrice.cpp @@ -100,10 +100,8 @@ Servatrice_GameServer::Servatrice_GameServer(Servatrice *_server, newThread->start(); server->watchWorkerThread(newThread); - QMetaObject::invokeMethod( - newDatabaseInterface, - [newDatabaseInterface, _sqlDatabase] { newDatabaseInterface->initDatabase(_sqlDatabase); }, - Qt::BlockingQueuedConnection); + QMetaObject::invokeMethod(newDatabaseInterface, "initDatabase", Qt::BlockingQueuedConnection, + Q_ARG(QSqlDatabase, _sqlDatabase)); connectionPools.append(newPool); } @@ -127,10 +125,9 @@ void Servatrice_GameServer::incomingConnection(qintptr socketDescriptor) auto ssi = new TcpServerSocketInterface(server, pool->getDatabaseInterface()); ssi->moveToThread(pool->thread()); pool->addClient(); - connect(ssi, &QObject::destroyed, pool, &Servatrice_ConnectionPool::removeClient); + connect(ssi, SIGNAL(destroyed()), pool, SLOT(removeClient())); - QMetaObject::invokeMethod( - ssi, [ssi, socketDescriptor] { ssi->initConnection(socketDescriptor); }, Qt::QueuedConnection); + QMetaObject::invokeMethod(ssi, "initConnection", Qt::QueuedConnection, Q_ARG(int, socketDescriptor)); } Servatrice_ConnectionPool *Servatrice_GameServer::findLeastUsedConnectionPool() @@ -171,15 +168,13 @@ Servatrice_WebsocketGameServer::Servatrice_WebsocketGameServer(Servatrice *_serv newThread->start(); server->watchWorkerThread(newThread); - QMetaObject::invokeMethod( - newDatabaseInterface, - [newDatabaseInterface, _sqlDatabase] { newDatabaseInterface->initDatabase(_sqlDatabase); }, - Qt::BlockingQueuedConnection); + QMetaObject::invokeMethod(newDatabaseInterface, "initDatabase", Qt::BlockingQueuedConnection, + Q_ARG(QSqlDatabase, _sqlDatabase)); connectionPools.append(newPool); - } - connect(this, &QWebSocketServer::newConnection, this, &Servatrice_WebsocketGameServer::onNewConnection); + connect(this, SIGNAL(newConnection()), this, SLOT(onNewConnection())); + } } Servatrice_WebsocketGameServer::~Servatrice_WebsocketGameServer() @@ -204,10 +199,9 @@ void Servatrice_WebsocketGameServer::onNewConnection() */ // ssi->moveToThread(pool->thread()); pool->addClient(); - connect(ssi, &QObject::destroyed, pool, &Servatrice_ConnectionPool::removeClient); + connect(ssi, SIGNAL(destroyed()), pool, SLOT(removeClient())); - void *socket = nextPendingConnection(); - QMetaObject::invokeMethod(ssi, [ssi, socket] { ssi->initConnection(socket); }, Qt::QueuedConnection); + QMetaObject::invokeMethod(ssi, "initConnection", Qt::QueuedConnection, Q_ARG(void *, nextPendingConnection())); } Servatrice_ConnectionPool *Servatrice_WebsocketGameServer::findLeastUsedConnectionPool() @@ -230,14 +224,14 @@ Servatrice_ConnectionPool *Servatrice_WebsocketGameServer::findLeastUsedConnecti void Servatrice_IslServer::incomingConnection(qintptr socketDescriptor) { auto thread = new QThread; - connect(thread, &QThread::finished, thread, &QObject::deleteLater); + connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater())); auto interface = new IslInterface(static_cast(socketDescriptor), cert, privateKey, server); interface->moveToThread(thread); - connect(interface, &QObject::destroyed, thread, &QThread::quit); + connect(interface, SIGNAL(destroyed()), thread, SLOT(quit())); thread->start(); - QMetaObject::invokeMethod(interface, [interface] { interface->initServer(); }, Qt::QueuedConnection); + QMetaObject::invokeMethod(interface, "initServer", Qt::QueuedConnection); } Servatrice::Servatrice(QObject *parent) @@ -314,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"; @@ -386,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(); @@ -591,16 +584,15 @@ bool Servatrice::initServer() auto *thread = new QThread; thread->setObjectName("isl_" + QString::number(prop.id)); - connect(thread, &QThread::finished, thread, &QObject::deleteLater); + connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater())); IslInterface *interface = new IslInterface(prop.id, prop.hostname, prop.address.toString(), prop.controlPort, prop.cert, cert, key, this); interface->moveToThread(thread); - connect(interface, &QObject::destroyed, thread, &QThread::quit); + connect(interface, SIGNAL(destroyed()), thread, SLOT(quit())); thread->start(); - QMetaObject::invokeMethod( - interface, [interface] { interface->initClient(); }, Qt::BlockingQueuedConnection); + QMetaObject::invokeMethod(interface, "initClient", Qt::BlockingQueuedConnection); } qDebug() << "Starting ISL server on port" << getISLNetworkPort(); @@ -608,27 +600,27 @@ 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; } pingClock = new QTimer(this); - connect(pingClock, &QTimer::timeout, this, &Servatrice::pingClockTimeout); + connect(pingClock, SIGNAL(timeout()), this, SIGNAL(pingClockTimeout())); pingClock->start(getClientKeepAlive() * 1000); statusUpdateClock = new QTimer(this); - connect(statusUpdateClock, &QTimer::timeout, this, &Servatrice::statusUpdate); + connect(statusUpdateClock, SIGNAL(timeout()), this, SLOT(statusUpdate())); if (getServerStatusUpdateTime() != 0) { qDebug() << "Starting status update clock, interval" << getServerStatusUpdateTime() << "ms"; statusUpdateClock->start(getServerStatusUpdateTime()); } deckShareCleanupClock = new QTimer(this); - connect(deckShareCleanupClock, &QTimer::timeout, this, &Servatrice::cleanupExpiredDeckShares); + connect(deckShareCleanupClock, SIGNAL(timeout()), this, SLOT(cleanupExpiredDeckShares())); const int deckShareCleanupInterval = getDeckShareCleanupInterval(); if (deckShareCleanupInterval > 0) { qDebug() << "Starting deck share cleanup clock, interval" << deckShareCleanupInterval << "ms"; @@ -649,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; } } @@ -665,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; } } @@ -1045,7 +1036,7 @@ void Servatrice::scheduleShutdown(const QString &reason, int minutes) nextShutdownMessageMinutes = shutdownMinutes; if (minutes > 0) { shutdownTimer = new QTimer; - connect(shutdownTimer, &QTimer::timeout, this, &Servatrice::shutdownTimeout); + connect(shutdownTimer, SIGNAL(timeout()), this, SLOT(shutdownTimeout())); shutdownTimer->start(60000); } shutdownStateMutex.unlock(); @@ -1108,19 +1099,25 @@ void Servatrice::addIslInterface(int _serverId, IslInterface *interface) { // Only call with islLock locked for writing islInterfaces.insert(_serverId, interface); - connect(interface, &IslInterface::externalUserJoined, this, &Servatrice::externalUserJoined); - connect(interface, &IslInterface::externalUserLeft, this, &Servatrice::externalUserLeft); - connect(interface, &IslInterface::externalRoomUserJoined, this, &Servatrice::externalRoomUserJoined); - connect(interface, &IslInterface::externalRoomUserLeft, this, &Servatrice::externalRoomUserLeft); - connect(interface, &IslInterface::externalRoomSay, this, &Servatrice::externalRoomSay); - connect(interface, &IslInterface::externalRoomRemoveMessages, this, &Servatrice::externalRoomRemoveMessages); - connect(interface, &IslInterface::externalRoomGameListChanged, this, &Servatrice::externalRoomGameListChanged); - connect(interface, &IslInterface::joinGameCommandReceived, this, &Servatrice::externalJoinGameCommandReceived); - connect(interface, &IslInterface::gameCommandContainerReceived, this, - &Servatrice::externalGameCommandContainerReceived); - connect(interface, &IslInterface::responseReceived, this, &Servatrice::externalResponseReceived); - connect(interface, &IslInterface::gameEventContainerReceived, this, - &Servatrice::externalGameEventContainerReceived); + connect(interface, SIGNAL(externalUserJoined(ServerInfo_User)), this, SLOT(externalUserJoined(ServerInfo_User))); + connect(interface, SIGNAL(externalUserLeft(QString)), this, SLOT(externalUserLeft(QString))); + connect(interface, SIGNAL(externalRoomUserJoined(int, ServerInfo_User)), this, + SLOT(externalRoomUserJoined(int, ServerInfo_User))); + connect(interface, SIGNAL(externalRoomUserLeft(int, QString)), this, SLOT(externalRoomUserLeft(int, QString))); + connect(interface, SIGNAL(externalRoomSay(int, QString, QString, QString)), this, + SLOT(externalRoomSay(int, QString, QString, QString))); + connect(interface, SIGNAL(externalRoomRemoveMessages(int, QString, int)), this, + SLOT(externalRoomRemoveMessages(int, QString, int))); + connect(interface, SIGNAL(externalRoomGameListChanged(int, ServerInfo_Game)), this, + SLOT(externalRoomGameListChanged(int, ServerInfo_Game))); + connect(interface, SIGNAL(joinGameCommandReceived(Command_JoinGame, int, int, int, qint64)), this, + SLOT(externalJoinGameCommandReceived(Command_JoinGame, int, int, int, qint64))); + connect(interface, SIGNAL(gameCommandContainerReceived(CommandContainer, int, int, qint64)), this, + SLOT(externalGameCommandContainerReceived(CommandContainer, int, int, qint64))); + connect(interface, SIGNAL(responseReceived(Response, qint64)), this, + SLOT(externalResponseReceived(Response, qint64))); + connect(interface, SIGNAL(gameEventContainerReceived(GameEventContainer, qint64)), this, + SLOT(externalGameEventContainerReceived(GameEventContainer, qint64))); } void Servatrice::removeIslInterface(int _serverId) diff --git a/servatrice/src/servatrice_database_interface.cpp b/servatrice/src/servatrice_database_interface.cpp index a4f184e38..04f6047fa 100644 --- a/servatrice/src/servatrice_database_interface.cpp +++ b/servatrice/src/servatrice_database_interface.cpp @@ -33,7 +33,6 @@ #include #include #include -#include #include #include #include @@ -483,10 +482,6 @@ AuthenticationResult Servatrice_DatabaseInterface::checkUserPassword(Server_Prot const QByteArray expected = PasswordHasher::computeResponse(key, nonce); handler->clearAuthNonce(); if (PasswordHasher::constantTimeEquals(expected, response)) { - if (forceChange) { - qCDebug(DatabaseInterfaceLog) << "Login accepted but password change required"; - return PasswordChangeRequired; - } qCDebug(DatabaseInterfaceLog) << "Login accepted: challenge-response password right"; return PasswordRight; } @@ -760,20 +755,6 @@ QString Servatrice_DatabaseInterface::getUserPasswordData(const QString &user) return query->value(0).toString(); } -QString Servatrice_DatabaseInterface::sealLegacyBackup(const QString &storedName, const QString &credential) -{ - const QByteArray key = server->getLegacyBackupKey(); - if (key.isEmpty()) { - return {}; - } - const QByteArray aad = storedName.simplified().toUtf8(); - const QString sealed = CryptoUtil::encryptSecret(credential.toUtf8(), key, aad); - if (sealed.isEmpty() || sealed.length() > MAX_LEGACY_BACKUP_LENGTH) { - return {}; - } - return sealed; -} - bool Servatrice_DatabaseInterface::submitPasswordVerifier(const QString &user, const QString &passwordVerifier) { if (server->getAuthenticationMethod() != Servatrice::AuthenticationSql) { @@ -798,13 +779,18 @@ bool Servatrice_DatabaseInterface::submitPasswordVerifier(const QString &user, c QString sealedBackup; QDateTime backupUsedAt; - if (PasswordHasher::isLegacyFormat(currentCredential) && !server->getLegacyBackupKey().isEmpty()) { - sealedBackup = sealLegacyBackup(storedName, currentCredential); - if (sealedBackup.isEmpty()) { - qCWarning(DatabaseInterfaceLog) - << "Failed to seal legacy backup for user" << user << "- account migrates without a downgrade path"; - } else { - backupUsedAt = QDateTime::currentDateTimeUtc(); + if (PasswordHasher::isLegacyFormat(currentCredential)) { + const QByteArray key = server->getLegacyBackupKey(); + if (!key.isEmpty()) { + const QByteArray aad = storedName.simplified().toUtf8(); + sealedBackup = CryptoUtil::encryptSecret(currentCredential.toUtf8(), key, aad); + if (sealedBackup.isEmpty() || sealedBackup.length() > MAX_LEGACY_BACKUP_LENGTH) { + qCWarning(DatabaseInterfaceLog) + << "Failed to seal legacy backup for user" << user << "- account migrates without a downgrade path"; + sealedBackup.clear(); + } else { + backupUsedAt = QDateTime::currentDateTimeUtc(); + } } } @@ -1658,54 +1644,6 @@ bool Servatrice_DatabaseInterface::changeUserPassword(const QString &user, return passwordQuery->numRowsAffected() > 0; } -bool Servatrice_DatabaseInterface::changeUserPasswordWithLegacyBackup(const QString &user, - const QString &passwordVerifier, - const QString &legacyCredential) -{ - if (server->getAuthenticationMethod() != Servatrice::AuthenticationSql) { - return false; - } - - if (!checkSql()) { - return false; - } - - // Seal against the stored row name, not the caller-supplied one: the name column is - // case-insensitive, so a differently-cased name would fail the tag check on open. - QSqlQuery *nameQuery = prepareQuery("SELECT name FROM {prefix}_users WHERE name = :name"); - nameQuery->bindValue(":name", user); - if (!execSqlQuery(nameQuery) || !nameQuery->next()) { - return false; - } - const QString storedName = nameQuery->value(0).toString(); - - const QString sealedBackup = sealLegacyBackup(storedName, legacyCredential); - const QDateTime backupUsedAt = sealedBackup.isEmpty() ? QDateTime() : QDateTime::currentDateTimeUtc(); - if (sealedBackup.isEmpty()) { - // Without a backup a pre-challenge client has nothing to authenticate against: the - // row holds a verifier and the legacy hash it would send cannot match it. Say so, - // because the symptom - the temporary password appearing not to work - shows up on - // a different machine from the one that reset it. - qCWarning(DatabaseInterfaceLog) << "Stored a verifier without a legacy backup for user" << user - << "- pre-challenge clients cannot use this password" - << "- check security/legacy_backup_key"; - } - - QSqlQuery *query = prepareQuery("update {prefix}_users set password_sha512=:password, " - "password_legacy_backup=:backup, password_legacy_backup_used_at=:used_at, " - "passwordLastChangedDate = NOW() where name = :name"); - query->bindValue(":password", passwordVerifier); - query->bindValue(":backup", sealedBackup); - query->bindValue(":used_at", backupUsedAt.isValid() - ? QVariant(backupUsedAt.toString(QStringLiteral("yyyy-MM-dd HH:mm:ss"))) - : QVariant()); - query->bindValue(":name", storedName); - if (!execSqlQuery(query)) { - return false; - } - return query->numRowsAffected() > 0; -} - void Servatrice_DatabaseInterface::setForcePasswordChange(const QString &user, bool force) { if (!checkSql()) { @@ -2139,108 +2077,6 @@ bool Servatrice_DatabaseInterface::removeUserAvatar(const QString &userName) return query->numRowsAffected() > 0; } -Servatrice_DatabaseInterface::RemoveUserBannerResult -Servatrice_DatabaseInterface::removeUserBanner(const QString &userName) -{ - if (!checkSql()) { - return RemoveUserBannerResult::DatabaseError; - } - - // The UPDATE alone cannot tell a missing user from a user without a banner, - // so read the row first and let the moderator know which case they hit. - QSqlQuery *lookupQuery = prepareQuery("SELECT card_art_params FROM {prefix}_users WHERE name = :user_name"); - lookupQuery->bindValue(":user_name", userName); - if (!execSqlQuery(lookupQuery)) { - return RemoveUserBannerResult::DatabaseError; - } - if (!lookupQuery->next()) { - return RemoveUserBannerResult::NoSuchUser; - } - if (lookupQuery->value(0).toString().isEmpty()) { - return RemoveUserBannerResult::NothingToRemove; - } - - QSqlQuery *query = prepareQuery("UPDATE {prefix}_users SET card_art_params = NULL WHERE name = :user_name"); - query->bindValue(":user_name", userName); - if (!execSqlQuery(query)) { - return RemoveUserBannerResult::DatabaseError; - } - return RemoveUserBannerResult::Removed; -} - -// isCardArtAllowed() compares against the rule table with the database's -// case-insensitive collation, so the sweep has to fold case the same way or it -// leaves behind banners the server already treats as blacklisted. -static QPair bannerKey(const QString &cardName, const QString &cardProviderId) -{ - return {cardName.toCaseFolded(), cardProviderId.toCaseFolded()}; -} - -int Servatrice_DatabaseInterface::clearBlacklistedBanners() -{ - if (!checkSql()) { - return -1; - } - - QSqlQuery *ruleQuery = prepareQuery("SELECT card_name, card_provider_id FROM {prefix}_card_art_name_rules " - "WHERE mode = 'DENY'"); - if (!execSqlQuery(ruleQuery)) { - return -1; - } - - QSet> denied; - while (ruleQuery->next()) { - denied.insert(bannerKey(ruleQuery->value(0).toString(), ruleQuery->value(1).toString())); - } - if (denied.isEmpty()) { - return 0; - } - - QSqlQuery *userQuery = - prepareQuery("SELECT id, card_art_params FROM {prefix}_users WHERE card_art_params IS NOT NULL"); - if (!execSqlQuery(userQuery)) { - return -1; - } - - QList matchingIds; - while (userQuery->next()) { - const QJsonDocument doc = QJsonDocument::fromJson(userQuery->value(1).toString().toUtf8()); - if (!doc.isObject()) { - continue; - } - const QJsonObject obj = doc.object(); - if (denied.contains(bannerKey(obj.value("card_name").toString(), obj.value("card_provider_id").toString()))) { - matchingIds.append(userQuery->value(0)); - } - } - - if (matchingIds.isEmpty()) { - return 0; - } - - // Chunk the UPDATE so very large user tables do not exceed driver placeholder limits. - int cleared = 0; - constexpr int BATCH_SIZE = 500; - for (int i = 0; i < matchingIds.size(); i += BATCH_SIZE) { - const QList batch = matchingIds.mid(i, BATCH_SIZE); - QStringList placeholders; - placeholders.reserve(batch.size()); - for (int j = 0; j < batch.size(); ++j) { - placeholders.append(QString(":id%1").arg(j)); - } - QSqlQuery *clearQuery = prepareQuery( - QString("UPDATE {prefix}_users SET card_art_params = NULL WHERE id IN (%1)").arg(placeholders.join(","))); - for (int j = 0; j < batch.size(); ++j) { - clearQuery->bindValue(placeholders.at(j), batch.at(j)); - } - if (!execSqlQuery(clearQuery)) { - return -1; - } - cleared += clearQuery->numRowsAffected(); - } - return cleared; -} - QList Servatrice_DatabaseInterface::getMessageLogHistory(const QString &user, const QString &ipaddress, const QString &gamename, @@ -2250,14 +2086,8 @@ QList Servatrice_DatabaseInterface::getMessageLogHistory bool &game, bool &room, int &range, - int &maxresults, - int offset, - const QString &beforeTime, - bool *hasMore) + int &maxresults) { - if (hasMore != nullptr) { - *hasMore = false; - } QList results; ServerInfo_ChatMessage chatMessage; @@ -2266,31 +2096,8 @@ QList Servatrice_DatabaseInterface::getMessageLogHistory return results; } - // Page size guard: always bound the scan so a filter-less browse of room - // history cannot become a 50M-row fetch. Zero means "client did not set a - // size", so fall back to one smooth-scroll page. - const int pageSize = maxresults <= 0 ? 200 : qBound(1, maxresults, 1000); - maxresults = pageSize; - const int safeOffset = qMax(0, offset); - - const bool hasIndexedFilter = !user.isEmpty() || !ipaddress.isEmpty() || !gameid.isEmpty() || !gamename.isEmpty(); - const bool hasLocationFilter = chat || game || room; - if (!hasLocationFilter) { - // No target_type restriction would mix private chat into a room/game - // browse, so refuse rather than leak. - return results; - } - // A browse narrowed only by a message substring has no index to serve it, and `range` - // has no upper bound on the wire: the client offers 20 days, but a hand-built command - // can ask for any value and every LIKE candidate in it is a row the server examines. - // Clamping here keeps the guard independent of what the client happens to send. - constexpr int maxFilterlessRangeHours = 20 * 24; - if (!hasIndexedFilter && range > maxFilterlessRangeHours) { - range = maxFilterlessRangeHours; - } - if (!hasIndexedFilter && range <= 0) { - // Filter-less browse must stay time-bounded (idx_log_time) so the - // query stays an index range scan instead of a full table scan. + if (user.isEmpty() && ipaddress.isEmpty() && gameid.isEmpty() && gamename.isEmpty()) { + // To ensure quick results and minimal lag, require an indexed field return results; } @@ -2345,23 +2152,10 @@ QList Servatrice_DatabaseInterface::getMessageLogHistory queryString.append(" AND log_time >= DATE_SUB(now(), INTERVAL :range_time HOUR)"); } - if (!beforeTime.isEmpty()) { - // Pages after the first run against the snapshot fixed by the first one, so rows - // logged while the moderator scrolls can no longer push the OFFSET window down and - // repeat a row already on screen. - queryString.append(" AND log_time <= :before_time"); + if (maxresults) { + queryString.append(" LIMIT :limit_size"); } - // Newest-first keeps "going back from today" stable across pages and lets - // the client append older rows at the bottom for smooth infinite scroll. - queryString.append(" ORDER BY log_time DESC"); - - // Fetch one extra row when the caller wants a has_more cursor so a full - // page does not need a second empty round-trip to prove exhaustion. - const bool detectMore = (hasMore != nullptr); - const int fetchSize = detectMore ? pageSize + 1 : pageSize; - queryString.append(" LIMIT :limit_size OFFSET :offset_size"); - QSqlQuery *query = prepareQuery(queryString); if (!user.isEmpty()) { query->bindValue(":user_name", user); @@ -2381,11 +2175,9 @@ QList Servatrice_DatabaseInterface::getMessageLogHistory if (range) { query->bindValue(":range_time", range); } - if (!beforeTime.isEmpty()) { - query->bindValue(":before_time", beforeTime); + if (maxresults) { + query->bindValue(":limit_size", maxresults); } - query->bindValue(":limit_size", fetchSize); - query->bindValue(":offset_size", safeOffset); if (!execSqlQuery(query)) { qCWarning(DatabaseInterfaceLog) << "Failed to collect log history information: SQL Error"; @@ -2405,11 +2197,6 @@ QList Servatrice_DatabaseInterface::getMessageLogHistory results << chatMessage; } - if (detectMore && results.size() > pageSize) { - results.removeLast(); - *hasMore = true; - } - return results; } diff --git a/servatrice/src/servatrice_database_interface.h b/servatrice/src/servatrice_database_interface.h index eb74063ac..822accada 100644 --- a/servatrice/src/servatrice_database_interface.h +++ b/servatrice/src/servatrice_database_interface.h @@ -62,9 +62,6 @@ private: bool checkUserIsIpBanned(const QString &ipAddress, QString &banReason, int &banSecondsRemaining); /** Must be called after checkSql and server is known to be in auth mode. */ bool checkUserIsNameBanned(QString const &userName, QString &banReason, int &banSecondsRemaining); - /** Seal a legacy credential for the account's downgrade backup; empty when the - * backup key is unset or the sealed value does not fit the column. */ - QString sealLegacyBackup(const QString &storedName, const QString &credential); bool isStrictModeEnabled(bool &ok) const; protected: @@ -195,11 +192,6 @@ public: const QString &targetName, const QString &channel = QString()) override; bool changeUserPassword(const QString &user, const QString &password, bool passwordNeedsHash) override; - /** Replace a credential with an already-derived verifier, keeping a sealed legacy hash of the - * same password as the downgrade backup for pre-challenge clients. */ - bool changeUserPasswordWithLegacyBackup(const QString &user, - const QString &passwordVerifier, - const QString &legacyCredential); bool changeUserPassword(const QString &user, const QString &oldPassword, bool oldPasswordNeedsHash, @@ -219,10 +211,7 @@ public: bool &game, bool &room, int &range, - int &maxresults, - int offset = 0, - const QString &beforeTime = QString(), - bool *hasMore = nullptr); + int &maxresults); QList getUserSessions(const QString &userName, int limit); QList getUserAlts(const QString &userName); QList getModeratorLastLogins(); @@ -242,18 +231,6 @@ public: }; UptimeSnapshot getLatestUptimeSnapshot(int serverId); bool removeUserAvatar(const QString &userName); - - // Outcome of removeUserBanner(), so the caller can tell a missing user apart - // from a user who simply has no banner card set. - enum class RemoveUserBannerResult - { - Removed, - NothingToRemove, - NoSuchUser, - DatabaseError, - }; - RemoveUserBannerResult removeUserBanner(const QString &userName); - int clearBlacklistedBanners(); bool isCardArtAllowed(const QString &cardName, const QString &cardProviderId) override; bool addForgotPassword(const QString &user); bool removeForgotPassword(const QString &user) override; diff --git a/servatrice/src/server_logger.cpp b/servatrice/src/server_logger.cpp index 09453b7ee..1972444f1 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,55 @@ 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(); -} - -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); + connect(this, SIGNAL(sigFlushBuffer()), this, SLOT(flushBuffer()), Qt::QueuedConnection); } 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 +102,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 +136,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..f2875a6a3 100644 --- a/servatrice/src/server_logger.h +++ b/servatrice/src/server_logger.h @@ -14,20 +14,10 @@ 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(); - - /** - * @brief Whether messages passed to logMessage() would be written anywhere. - * - * Callers use this to avoid formatting expensive debug payloads that - * logMessage() would discard. - */ - [[nodiscard]] bool isLoggingEnabled() const; - 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 +27,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 c38d790b1..890a93fbc 100644 --- a/servatrice/src/serversocketinterface.cpp +++ b/servatrice/src/serversocketinterface.cpp @@ -126,7 +126,6 @@ #include #include #include -#include #include #include #include @@ -141,7 +140,6 @@ #include #include #include -#include #include #include #include @@ -199,8 +197,7 @@ AbstractServerSocketInterface::AbstractServerSocketInterface(Servatrice *_server // Never call flushOutputQueue directly from outputQueueChanged. In case of a socket error, // it could lead to this object being destroyed while another function is still on the call stack. -> mutex // deadlocks etc. - connect(this, &AbstractServerSocketInterface::outputQueueChanged, this, - &AbstractServerSocketInterface::flushOutputQueue, Qt::QueuedConnection); + connect(this, SIGNAL(outputQueueChanged()), this, SLOT(flushOutputQueue()), Qt::QueuedConnection); } bool AbstractServerSocketInterface::initSession() @@ -268,11 +265,6 @@ void AbstractServerSocketInterface::logDebugMessage(const QString &message) logger->logMessage(message, this); } -bool AbstractServerSocketInterface::logsDebugMessages() const -{ - return logger != nullptr && logger->isLoggingEnabled(); -} - void AbstractServerSocketInterface::processCommandContainer(const CommandContainer &cont) { QElapsedTimer timer; @@ -487,10 +479,6 @@ Response::ResponseCode AbstractServerSocketInterface::processExtendedModeratorCo return cmdGetModeratorLastLogins(cmd.GetExtension(Command_GetModeratorLastLogins::ext), rc); case ModeratorCommand::REMOVE_USER_AVATAR: return cmdRemoveUserAvatar(cmd.GetExtension(Command_RemoveUserAvatar::ext), rc); - case ModeratorCommand::REMOVE_USER_BANNER: - return cmdRemoveUserBanner(cmd.GetExtension(Command_RemoveUserBanner::ext), rc); - case ModeratorCommand::CLEAR_BLACKLISTED_BANNERS: - return cmdClearBlacklistedBanners(cmd.GetExtension(Command_ClearBlacklistedBanners::ext), rc); default: return Response::RespFunctionNotAllowed; } @@ -1743,17 +1731,12 @@ Response::ResponseCode AbstractServerSocketInterface::cmdGetLogHistory(const Com int dateRange = cmd.date_range(); int maximumResults = cmd.maximum_results(); - const int offset = cmd.has_offset() ? static_cast(cmd.offset()) : 0; - const QString beforeTime = cmd.has_before_time() ? QString::fromStdString(cmd.before_time()) : QString(); Response_ViewLogHistory *re = new Response_ViewLogHistory; if (servatrice->getEnableLogQuery()) { - bool hasMore = false; - QList history = - sqlInterface->getMessageLogHistory(userName, ipAddress, gameName, gameID, message, chatType, gameType, - roomType, dateRange, maximumResults, offset, beforeTime, &hasMore); - QListIterator messageIterator(history); + QListIterator messageIterator(sqlInterface->getMessageLogHistory( + userName, ipAddress, gameName, gameID, message, chatType, gameType, roomType, dateRange, maximumResults)); while (messageIterator.hasNext()) { ServerInfo_ChatMessage chatMessage = messageIterator.next(); if (!allowPrivateChat) { @@ -1761,7 +1744,6 @@ Response::ResponseCode AbstractServerSocketInterface::cmdGetLogHistory(const Com } re->add_log_message()->CopyFrom(chatMessage); } - re->set_has_more(hasMore); } else { ServerInfo_ChatMessage chatMessage; @@ -1989,8 +1971,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdBanFromServer(const Com SessionEvent *se = userList[i]->prepareSessionEvent(event); userList[i]->sendProtocolItem(*se); delete se; - QMetaObject::invokeMethod( - userList[i], [user = userList[i]] { user->prepareDestroy(); }, Qt::QueuedConnection); + QMetaObject::invokeMethod(userList[i], "prepareDestroy", Qt::QueuedConnection); } } @@ -2649,15 +2630,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdResetUserPassword(const } const QString tempPassword = PasswordHasher::generateRandomSalt(); - // Store a scrypt verifier rather than a legacy salt+hash: a legacy row makes the next - // login look like an authentication downgrade, which modern clients refuse when they - // still hold a verifier for the account. Where security/legacy_backup_key is configured, - // keep a sealed legacy hash of the same temporary password as the downgrade backup so - // pre-challenge clients keep the path the reset used to provide; without the key they - // have none, which changeUserPasswordWithLegacyBackup warns about. - const QString verifier = PasswordHasher::generatePasswordVerifier(tempPassword); - const QString legacyCredential = PasswordHasher::computeHash(tempPassword, PasswordHasher::generateRandomSalt()); - if (!sqlInterface->changeUserPasswordWithLegacyBackup(userName, verifier, legacyCredential)) { + if (!sqlInterface->changeUserPassword(userName, tempPassword, true)) { return Response::RespInternalError; } sqlInterface->setForcePasswordChange(userName, true); @@ -2713,67 +2686,6 @@ Response::ResponseCode AbstractServerSocketInterface::cmdRemoveUserAvatar(const return Response::RespOk; } -Response::ResponseCode AbstractServerSocketInterface::cmdRemoveUserBanner(const Command_RemoveUserBanner &cmd, - ResponseContainer &rc) -{ - if (!sqlInterface->checkSql()) { - return Response::RespInternalError; - } - - const QString userName = nameFromStdString(cmd.user_name()).simplified(); - if (userName.isEmpty()) { - return Response::RespContextError; - } - - bool hadBanner = true; - switch (sqlInterface->removeUserBanner(userName)) { - case Servatrice_DatabaseInterface::RemoveUserBannerResult::Removed: - break; - case Servatrice_DatabaseInterface::RemoveUserBannerResult::NothingToRemove: - hadBanner = false; - break; - case Servatrice_DatabaseInterface::RemoveUserBannerResult::NoSuchUser: - return Response::RespNameNotFound; - case Servatrice_DatabaseInterface::RemoveUserBannerResult::DatabaseError: - default: - return Response::RespInternalError; - } - - if (hadBanner) { - sqlInterface->addAuditRecord(userName, this->getAddress(), QString::fromStdString(userInfo->clientid()), - "REMOVE_USER_BANNER", "Moderator removed user banner card", true); - } - - Response_RemoveUserBanner *re = new Response_RemoveUserBanner; - re->set_user_name(userName.toStdString()); - re->set_had_banner(hadBanner); - rc.setResponseExtension(re); - return Response::RespOk; -} - -Response::ResponseCode -AbstractServerSocketInterface::cmdClearBlacklistedBanners(const Command_ClearBlacklistedBanners & /* cmd */, - ResponseContainer &rc) -{ - if (!sqlInterface->checkSql()) { - return Response::RespInternalError; - } - - const int cleared = sqlInterface->clearBlacklistedBanners(); - if (cleared < 0) { - return Response::RespInternalError; - } - - sqlInterface->addAuditRecord(QString::fromStdString(userInfo->name()), this->getAddress(), - QString::fromStdString(userInfo->clientid()), "CLEAR_BLACKLISTED_BANNERS", - QString("Moderator cleared %1 blacklisted banner(s)").arg(cleared), true); - - Response_ClearBlacklistedBanners *re = new Response_ClearBlacklistedBanners; - re->set_cleared_count(static_cast(cleared)); - rc.setResponseExtension(re); - return Response::RespOk; -} - void AbstractServerSocketInterface::sendPendingReportNotifications(ResponseContainer &rc) { if (!sqlInterface->checkSql()) { @@ -3795,9 +3707,6 @@ Response::ResponseCode AbstractServerSocketInterface::cmdAccountPassword(const C } databaseInterface->setForcePasswordChange(userName, false); - // The forced change is done, but this session never completed login (see - // Server_ProtocolHandler::cmdLogin). It stays restricted to ping and another password - // change; the client reconnects with the new password to actually enter the server. return Response::RespOk; } @@ -4199,15 +4108,15 @@ Response::ResponseCode AbstractServerSocketInterface::cmdUpdateServerMessage(const Command_UpdateServerMessage & /*cmd*/, ResponseContainer & /*rc*/) { - QMetaObject::invokeMethod(servatrice, [srv = servatrice] { srv->updateLoginMessage(); }); + QMetaObject::invokeMethod(server, "updateLoginMessage"); return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdShutdownServer(const Command_ShutdownServer &cmd, ResponseContainer & /*rc*/) { - QMetaObject::invokeMethod(servatrice, [srv = servatrice, reason = textFromStdString(cmd.reason()), - minutes = cmd.minutes()] { srv->scheduleShutdown(reason, minutes); }); + QMetaObject::invokeMethod(server, "scheduleShutdown", Q_ARG(QString, textFromStdString(cmd.reason())), + Q_ARG(int, cmd.minutes())); return Response::RespOk; } @@ -4216,9 +4125,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReloadConfig(const Comm { logDebugMessage("Received admin command: reloading configuration"); settingsCache->sync(); - QMetaObject::invokeMethod(servatrice, [srv = servatrice, features = servatrice->getRequiredFeatures()] { - srv->setRequiredFeatures(features); - }); + QMetaObject::invokeMethod(server, "setRequiredFeatures", Q_ARG(QString, server->getRequiredFeatures())); return Response::RespOk; } @@ -4270,7 +4177,7 @@ bool AbstractServerSocketInterface::removeAdminFlagFromUser(const QString &userN delete se; } - QMetaObject::invokeMethod(user, [user] { user->prepareDestroy(); }, Qt::QueuedConnection); + QMetaObject::invokeMethod(user, "prepareDestroy", Qt::QueuedConnection); return true; } @@ -4445,10 +4352,10 @@ TcpServerSocketInterface::TcpServerSocketInterface(Servatrice *_server, { socket = new QTcpSocket(this); socket->setSocketOption(QAbstractSocket::LowDelayOption, 1); - connect(socket, &QTcpSocket::readyRead, this, [this] { readClient(); }); - connect(socket, &QTcpSocket::disconnected, this, [this] { catchSocketDisconnected(); }); - connect(socket, &QTcpSocket::errorOccurred, this, - [this](QAbstractSocket::SocketError error) { catchSocketError(error); }); + connect(socket, SIGNAL(readyRead()), this, SLOT(readClient())); + connect(socket, SIGNAL(disconnected()), this, SLOT(catchSocketDisconnected())); + connect(socket, SIGNAL(errorOccurred(QAbstractSocket::SocketError)), this, + SLOT(catchSocketError(QAbstractSocket::SocketError))); } TcpServerSocketInterface::~TcpServerSocketInterface() @@ -4524,17 +4431,13 @@ void TcpServerSocketInterface::readClient() servatrice->incRxBytes(data.size()); inputBuffer.append(data); - // Parse messages straight out of inputBuffer at readOffset. Removing each - // consumed message from the front would memmove the remaining bytes once - // per message; the consumed prefix is dropped once parsing stops. - qsizetype readOffset = 0; - - while (true) { + do { if (!messageInProgress) { - if (inputBuffer.size() - readOffset >= 4) { - const auto *header = reinterpret_cast(inputBuffer.constData() + readOffset); - messageLength = (static_cast(header[0]) << 24) + (static_cast(header[1]) << 16) + - (static_cast(header[2]) << 8) + static_cast(header[3]); + if (inputBuffer.size() >= 4) { + messageLength = (((quint32)(unsigned char)inputBuffer[0]) << 24) + + (((quint32)(unsigned char)inputBuffer[1]) << 16) + + (((quint32)(unsigned char)inputBuffer[2]) << 8) + + ((quint32)(unsigned char)inputBuffer[3]); // Reject implausible lengths instead of buffering until they arrive. // This also handles lengths that overflow into negative int values. if (messageLength < 0 || messageLength > MAX_TCP_MESSAGE_SIZE) { @@ -4543,33 +4446,32 @@ void TcpServerSocketInterface::readClient() prepareDestroy(); return; } - readOffset += 4; + inputBuffer.remove(0, 4); messageInProgress = true; } else { - break; + return; } } - if (inputBuffer.size() - readOffset < messageLength) { - break; + if (inputBuffer.size() < messageLength) { + return; } CommandContainer newCommandContainer; - bool ok = false; + bool ok; try { - ok = newCommandContainer.ParseFromArray(inputBuffer.constData() + readOffset, messageLength); + ok = newCommandContainer.ParseFromArray(inputBuffer.data(), messageLength); } catch (std::exception &e) { - qCWarning(TcpServerSocketInterfaceLog) - << "Caught std::exception in" << __FILE__ << __LINE__ << + qCWarning(TcpServerSocketInterfaceLog) << "Caught std::exception in" << __FILE__ << __LINE__ << #ifdef _MSC_VER // Visual Studio __FUNCTION__ #else __PRETTY_FUNCTION__ #endif - << Qt::endl - << "Exception:" << e.what() << Qt::endl - << "Message coming from:" << getAddress() << Qt::endl - << "Message length:" << messageLength << Qt::endl - << "Message content:" << inputBuffer.mid(readOffset, messageLength).toHex(); + << Qt::endl + << "Exception:" << e.what() << Qt::endl + << "Message coming from:" << getAddress() << Qt::endl + << "Message length:" << messageLength << Qt::endl + << "Message content:" << inputBuffer.toHex(); } catch (...) { qCWarning(TcpServerSocketInterfaceLog) << "Unhandled exception in" << __FILE__ << __LINE__ << #ifdef _MSC_VER // Visual Studio @@ -4580,7 +4482,7 @@ void TcpServerSocketInterface::readClient() << Qt::endl << "Message coming from:" << getAddress(); } - readOffset += messageLength; + inputBuffer.remove(0, messageLength); messageInProgress = false; if (ok) { @@ -4597,12 +4499,8 @@ void TcpServerSocketInterface::readClient() } else { qCWarning(TcpServerSocketInterfaceLog) << "parsing error!"; } - } - // Drop the consumed prefix once, instead of shifting the buffer per message. - if (readOffset > 0) { - inputBuffer.remove(0, readOffset); - } + } while (!inputBuffer.isEmpty()); } bool TcpServerSocketInterface::initTcpSession() @@ -4670,17 +4568,11 @@ void WebsocketServerSocketInterface::initConnection(void *_socket) } } - connect(socket, &QWebSocket::binaryMessageReceived, this, &WebsocketServerSocketInterface::binaryMessageReceived); - // 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(binaryMessageReceived(const QByteArray &)), this, + SLOT(binaryMessageReceived(const QByteArray &))); connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(catchSocketError(QAbstractSocket::SocketError))); -#endif - connect(socket, &QWebSocket::disconnected, this, [this] { catchSocketDisconnected(); }); + connect(socket, SIGNAL(disconnected()), this, SLOT(catchSocketDisconnected())); // Add this object to the server's list of connections before it can receive socket events. // Otherwise, in case of a socket error, it could be removed from the list before it is added. diff --git a/servatrice/src/serversocketinterface.h b/servatrice/src/serversocketinterface.h index cd37286c6..dd9d3f2bb 100644 --- a/servatrice/src/serversocketinterface.h +++ b/servatrice/src/serversocketinterface.h @@ -73,7 +73,6 @@ class Command_AccountPassword; class Command_Activate; class Command_AddCardArtRule; class Command_AdjustMod; -class Command_ClearBlacklistedBanners; class Command_ForceActivateUser; class Command_ForgotPasswordChallenge; class Command_ForgotPasswordRequest; @@ -91,7 +90,6 @@ class Command_ListCardArtRules; class Command_Register; class Command_RemoveCardArtRule; class Command_RemoveUserAvatar; -class Command_RemoveUserBanner; class Command_Report; class Command_ReportAddComment; class Command_ReportAssign; @@ -125,7 +123,6 @@ signals: protected: void logDebugMessage(const QString &message) override; - [[nodiscard]] bool logsDebugMessages() const override; bool tooManyRegistrationAttempts(const QString &ipAddress); void processCommandContainer(const CommandContainer &cont) override; bool acceptsCredentialFormat(bool passwordNeedsHash, const QString &password) const; @@ -238,9 +235,6 @@ private: Response::ResponseCode cmdGetModeratorLastLogins(const Command_GetModeratorLastLogins &cmd, ResponseContainer &rc); Response::ResponseCode cmdResetUserPassword(const Command_ResetUserPassword &cmd, ResponseContainer &rc); Response::ResponseCode cmdRemoveUserAvatar(const Command_RemoveUserAvatar &cmd, ResponseContainer &rc); - Response::ResponseCode cmdRemoveUserBanner(const Command_RemoveUserBanner &cmd, ResponseContainer &rc); - Response::ResponseCode cmdClearBlacklistedBanners(const Command_ClearBlacklistedBanners &cmd, - ResponseContainer &rc); Response::ResponseCode cmdGetServerStats(const Command_GetServerStats &cmd, ResponseContainer &rc); 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/servatrice/src/smtpclient.cpp b/servatrice/src/smtpclient.cpp index bfdfae905..82824e496 100644 --- a/servatrice/src/smtpclient.cpp +++ b/servatrice/src/smtpclient.cpp @@ -25,7 +25,7 @@ static const qint64 IDLE_TIMEOUT_MS = 60000; // 60 seconds // MAX_RETRY_DELAY cap is applied, preventing unbounded intermediate values. static const int MAX_BACKOFF_SHIFT = 10; -inline Q_LOGGING_CATEGORY(SmtpClientLog, "smtp_client"); +inline Q_LOGGING_CATEGORY(SmtpClientLog, "smtp_client") static QString pendingKey(const QString &userName, EmailType type, const QString &token) { @@ -55,23 +55,24 @@ SmtpClient::SmtpClient(QObject *parent) : QObject(parent) idleTimer = new QTimer(this); idleTimer->setSingleShot(true); - connect(idleTimer, &QTimer::timeout, this, &SmtpClient::idleTimeout); + connect(idleTimer, SIGNAL(timeout()), this, SLOT(idleTimeout())); - connect(smtp, &QxtSmtp::authenticated, this, &SmtpClient::authenticated); - connect(smtp, qOverload(&QxtSmtp::authenticationFailed), this, - &SmtpClient::authenticationFailed); - connect(smtp, &QxtSmtp::connected, this, &SmtpClient::connected); - connect(smtp, qOverload(&QxtSmtp::connectionFailed), this, &SmtpClient::connectionFailed); - connect(smtp, &QxtSmtp::disconnected, this, &SmtpClient::disconnected); - connect(smtp, &QxtSmtp::encrypted, this, &SmtpClient::encrypted); - connect(smtp, qOverload(&QxtSmtp::encryptionFailed), this, &SmtpClient::encryptionFailed); - connect(smtp, &QxtSmtp::finished, this, &SmtpClient::finished); - connect(smtp, qOverload(&QxtSmtp::mailFailed), this, &SmtpClient::mailFailed); - connect(smtp, &QxtSmtp::mailSent, this, &SmtpClient::mailSent); - connect(smtp, qOverload(&QxtSmtp::recipientRejected), this, - &SmtpClient::recipientRejected); - connect(smtp, qOverload(&QxtSmtp::senderRejected), this, - &SmtpClient::senderRejected); + connect(smtp, SIGNAL(authenticated()), this, SLOT(authenticated())); + connect(smtp, SIGNAL(authenticationFailed(const QByteArray &)), this, + SLOT(authenticationFailed(const QByteArray &))); + connect(smtp, SIGNAL(connected()), this, SLOT(connected())); + connect(smtp, SIGNAL(connectionFailed(const QByteArray &)), this, SLOT(connectionFailed(const QByteArray &))); + connect(smtp, SIGNAL(disconnected()), this, SLOT(disconnected())); + connect(smtp, SIGNAL(encrypted()), this, SLOT(encrypted())); + connect(smtp, SIGNAL(encryptionFailed(const QByteArray &)), this, SLOT(encryptionFailed(const QByteArray &))); + connect(smtp, SIGNAL(finished()), this, SLOT(finished())); + connect(smtp, SIGNAL(mailFailed(int, int, const QByteArray &)), this, + SLOT(mailFailed(int, int, const QByteArray &))); + connect(smtp, SIGNAL(mailSent(int)), this, SLOT(mailSent(int))); + connect(smtp, SIGNAL(recipientRejected(int, const QString &, const QByteArray &)), this, + SLOT(recipientRejected(int, const QString &, const QByteArray &))); + connect(smtp, SIGNAL(senderRejected(int, const QString &, const QByteArray &)), this, + SLOT(senderRejected(int, const QString &, const QByteArray &))); } SmtpClient::~SmtpClient() diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 59a7376a3..d80c541d2 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -179,27 +179,6 @@ cockatrice_add_test(NAME password_hash_test SOURCES password_hash_test.cpp LIBS cockatrice_add_test(NAME report_categories_test SOURCES report_categories_test.cpp LIBS libcockatrice_utility) cockatrice_add_test(NAME svg_recolor_test SOURCES svg_recolor_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. -cockatrice_add_test( - NAME - holder_line_test - SOURCES - ${CMAKE_SOURCE_DIR}/tools/installer-holder-probe/holder_line.cpp - holder_line_test.cpp - 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 @@ -252,17 +231,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 @@ -329,40 +297,6 @@ cockatrice_add_test( LABELS server ) -cockatrice_add_test( - NAME - server_forced_password_change_test - SOURCES - server_forced_password_change_test.cpp - LIBS - libcockatrice_network_server_remote - libcockatrice_rng - LABELS - server -) -cockatrice_add_test( - NAME - client_login_status_test - SOURCES - ${VERSION_STRING_CPP} - client_login_status_test.cpp - LIBS - libcockatrice_network_client_remote - LABELS - ci -) -# The local game runs one client and one server in the same process. -# Either of them can be destroyed first; the survivor has to stay usable. -cockatrice_add_test( - NAME - local_client_lifetime_test - SOURCES - local_client_lifetime_test.cpp - LIBS - libcockatrice_network - LABELS - ci -) cockatrice_add_test( NAME server_room_channels_test @@ -425,7 +359,6 @@ cockatrice_add_test( ${VERSION_STRING_CPP} loader_local_matching_test.cpp LIBS - libcockatrice_network libcockatrice_settings INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/cockatrice/src @@ -462,7 +395,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) @@ -491,22 +423,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( @@ -530,16 +446,10 @@ add_subdirectory(deck_list_model) add_subdirectory(deck_list_zones) add_subdirectory(icon_assets) add_subdirectory(loading_from_clipboard) -add_subdirectory(model_contract) 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/CMakeLists.txt b/tests/carddatabase/CMakeLists.txt index 7d83640a1..22171a9e8 100644 --- a/tests/carddatabase/CMakeLists.txt +++ b/tests/carddatabase/CMakeLists.txt @@ -35,21 +35,6 @@ if(WITH_ORACLE OR WITH_CLIENT) LIBS libcockatrice_filters ) - - # ------------------------ - # Visual Display Model Test - # ------------------------ - cockatrice_add_test( - NAME - visual_display_model_test - SOURCES - ${VERSION_STRING_CPP} - visual_display_model_test.cpp - mocks.cpp - LIBS - libcockatrice_card - libcockatrice_models - ) endif() # ------------------------ diff --git a/tests/carddatabase/carddatabase_test.cpp b/tests/carddatabase/carddatabase_test.cpp index 22cbe9074..2b216d5b6 100644 --- a/tests/carddatabase/carddatabase_test.cpp +++ b/tests/carddatabase/carddatabase_test.cpp @@ -17,18 +17,15 @@ #include #include #include -#include #include #include #include #include -#include #include #include #include #include #include -#include #include #include #include @@ -65,43 +62,6 @@ TEST(CardDatabaseTest, LoadXml) ASSERT_EQ(NotLoaded, db->getLoadStatus()) << "Incorrect status after clear"; } -TEST(CardDatabaseTest, LazyPropertiesHashStreamsBlobAccessors) -{ - QHash props; - props.insert("manacost", "1R"); - props.insert("colors", "R"); - props.insert("format-standard", "legal"); - - LazyPropertiesHash lazy = LazyPropertiesHash::serializedFrom(props); - const QByteArray blob = lazy.serialized(); - - EXPECT_EQ(3, lazy.size()); - EXPECT_FALSE(lazy.isEmpty()); - EXPECT_TRUE(lazy.contains("colors")); - EXPECT_FALSE(lazy.contains("missing")); - EXPECT_EQ("1R", lazy.value("manacost")); - EXPECT_EQ(QString(), lazy.value("missing")); - EXPECT_EQ(blob, lazy.serialized()) << "an untouched blob must be passed through unchanged"; - - QStringList keys = lazy.keys(); - keys.sort(); - QStringList expectedKeys = props.keys(); - expectedKeys.sort(); - EXPECT_EQ(expectedKeys, keys); - - // Mutating materializes the hash, after which every accessor keeps working. - EXPECT_TRUE(lazy.insert("loyalty", "3")); - EXPECT_FALSE(lazy.insert("loyalty", "3")); - EXPECT_EQ(4, lazy.size()); - EXPECT_EQ("3", lazy.value("loyalty")); - EXPECT_TRUE(lazy.contains("loyalty")); - - LazyPropertiesHash roundTrip(lazy.serialized()); - EXPECT_EQ(4, roundTrip.size()); - EXPECT_EQ("3", roundTrip.value("loyalty")); - EXPECT_EQ("legal", roundTrip.value("format-standard")); -} - TEST(CardDatabaseTest, GuessCardWithProviderIdOnUnknownCardReturnsEmpty) { // An unknown name plus a providerId used to dereference the missing card @@ -127,32 +87,6 @@ TEST(CardDatabaseTest, FindPrintingWithIdOnNullCardInfoReturnsEmpty) EXPECT_TRUE(db.query()->findPrintingWithId(nullptr, "card_Not a Card_fixture").isEmpty()); } -TEST(CardInfoTest, ClearDirectRelationsDropsRawPointers) -{ - auto *forward = new CardRelation("Child"); - auto *reverse = new CardRelation("Parent"); - auto *toMe = new CardRelation("Child"); - - auto card = CardInfo::newInstance("Parent", "", false, QHash(), {forward}, {reverse}, - SetToPrintingsMap(), CardInfo::UiAttributes()); - card->addReverseRelatedCards2Me(toMe); - ASSERT_EQ(1, card->getRelatedCards().size()); - ASSERT_EQ(1, card->getReverseRelatedCards().size()); - ASSERT_EQ(1, card->getReverseRelatedCards2Me().size()); - ASSERT_EQ(2, card->getAllRelatedCards().size()); - - card->clearDirectRelations(); - - EXPECT_TRUE(card->getRelatedCards().isEmpty()); - EXPECT_TRUE(card->getReverseRelatedCards().isEmpty()); - EXPECT_TRUE(card->getReverseRelatedCards2Me().isEmpty()); - EXPECT_TRUE(card->getAllRelatedCards().isEmpty()); - - delete forward; - delete reverse; - delete toMe; -} - TEST(CardDatabaseTest, Xml4LocalizedDataRoundTrip) { NoopCardSetPriorityController controller; @@ -170,8 +104,6 @@ TEST(CardDatabaseTest, Xml4LocalizedDataRoundTrip) CardInfo::newInstance("Lightning Bolt", "Deal 3 damage.", false, {}, {}, {}, setsInfo, attributes); card->setLocalizedName("de", "Blitzschlag"); card->setLocalizedText("de", "Blitzschlag fügt 3 Schadenspunkte zu."); - card->setProperty("format-standard", "legal"); - card->setProperty("colors", "R"); SetNameMap sets; sets.insert("TST", set); @@ -195,13 +127,6 @@ TEST(CardDatabaseTest, Xml4LocalizedDataRoundTrip) ASSERT_EQ(loaded->getLocalizedName("de"), "Blitzschlag"); ASSERT_EQ(loaded->getLocalizedText("de"), "Blitzschlag fügt 3 Schadenspunkte zu."); ASSERT_EQ(loaded->getLocalizedText("fr"), "Deal 3 damage."); - ASSERT_EQ(loaded->getProperty("format-standard"), "legal"); - ASSERT_TRUE(loaded->hasProperty("colors")); - ASSERT_EQ(loaded->getProperties().size(), 2); - - const QList printings = loaded->getSets().value("TST"); - ASSERT_FALSE(printings.isEmpty()); - ASSERT_EQ(printings.first().getProperty("manacost"), "1R"); } /** @@ -610,110 +535,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/carddatabase/load_benchmark.cpp b/tests/carddatabase/load_benchmark.cpp index da077a50f..5492b9531 100644 --- a/tests/carddatabase/load_benchmark.cpp +++ b/tests/carddatabase/load_benchmark.cpp @@ -125,7 +125,7 @@ RunResult runLoad(CardDatabase *db, ChurnCounters &counters) result.churn.modelResets = counters.modelResets - before.modelResets; for (const CardInfoPtr &card : db->getCardList()) { - result.propTotal += card->getProperties().size(); + result.propTotal += card->getPropertiesHash().size(); if (!card->getProperty("colors").isEmpty()) { ++result.propsQueried; } diff --git a/tests/carddatabase/visual_display_model_test.cpp b/tests/carddatabase/visual_display_model_test.cpp deleted file mode 100644 index 5fa554bb3..000000000 --- a/tests/carddatabase/visual_display_model_test.cpp +++ /dev/null @@ -1,188 +0,0 @@ -/** - * @file visual_display_model_test.cpp - * @brief Tests for the flattened model behind the visual card database grid. - */ - -#include "card_database_display_model.h" -#include "card_database_model.h" -#include "card_database_visual_display_model.h" -#include "libcockatrice/card/card_info.h" -#include "libcockatrice/card/database/card_database.h" -#include "libcockatrice/card/database/card_database_querier.h" -#include "libcockatrice/card/printing/exact_card.h" -#include "libcockatrice/card/printing/printing_info.h" -#include "libcockatrice/card/set/card_set.h" -#include "libcockatrice/interfaces/noop_card_preference_provider.h" -#include "libcockatrice/interfaces/noop_card_set_priority_controller.h" -#include "test_card_database_path_provider.h" - -#include "gtest/gtest.h" -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ -// GTest's main() creates no QCoreApplication, which QTimer needs for its event loop. -const QCoreApplication &application() -{ - static int argc = 1; - static char executable[] = "visual_display_model_test"; - static char *argv[] = {executable, nullptr}; - static const QCoreApplication app(argc, argv); - return app; -} - -/// Lets the rebuild timer fire, as it would in the running client. -void waitForRebuild() -{ - QEventLoop loop; - QTimer::singleShot(100, &loop, &QEventLoop::quit); - loop.exec(); -} - -class CardDatabaseVisualDisplayModelTest : public ::testing::Test -{ -protected: - void SetUp() override - { - (void)application(); - - db = std::make_unique(nullptr, new NoopCardPreferenceProvider(), - new TestCardDatabasePathProvider(), new NoopCardSetPriorityController()); - db->loadCardDatabases(); - - cardModel = std::make_unique(db.get(), false); - displayModel = std::make_unique(); - displayModel->setSourceModel(cardModel.get()); - visualModel = std::make_unique(displayModel.get(), db->query()); - visualModel->refresh(); - } - - /** - * @brief Builds a card with @p printingCount printings in each named set. - * @param printingCountPerSet Set short name to the number of printings in that set. - */ - [[nodiscard]] CardInfoPtr makeCard(const QString &name, const QMap &printingCountPerSet) - { - SetToPrintingsMap setsInfo; - for (auto it = printingCountPerSet.constBegin(); it != printingCountPerSet.constEnd(); ++it) { - CardSetPtr set = CardSet::newInstance(&setController, it.key(), it.key() + " test set", "expansion", - QDate(), CardSet::PriorityPrimary); - for (int printing = 0; printing < it.value(); ++printing) { - QHash properties; - properties["uuid"] = QString("%1-%2-%3").arg(it.key(), name).arg(printing); - setsInfo[it.key()].append(PrintingInfo(set, LazyPropertiesHash(properties))); - } - } - - CardInfo::UiAttributes attributes = {.tableRow = 1}; - return CardInfo::newInstance(name, "", false, {}, {}, {}, setsInfo, attributes); - } - - NoopCardSetPriorityController setController; - std::unique_ptr db; - std::unique_ptr cardModel; - std::unique_ptr displayModel; - std::unique_ptr visualModel; -}; - -TEST_F(CardDatabaseVisualDisplayModelTest, ExposesTheCardInfoBehindAProxyRow) -{ - ASSERT_GT(displayModel->rowCount(), 0); - - const QVariant info = displayModel->data(displayModel->index(0, CardDatabaseModel::NameColumn), - CardDatabaseDisplayModel::CardInfoRole); - ASSERT_TRUE(info.isValid()); - EXPECT_FALSE(info.value().isNull()); -} - -TEST_F(CardDatabaseVisualDisplayModelTest, FlattensOneRowPerProxyCardWithoutSetFilters) -{ - ASSERT_GT(displayModel->rowCount(), 0); - EXPECT_EQ(displayModel->rowCount(), visualModel->rowCount()); - - for (int row = 0; row < visualModel->rowCount(); ++row) { - const ExactCard card = visualModel->cardAt(row); - ASSERT_TRUE(card) << "row " << row; - EXPECT_EQ( - displayModel->data(displayModel->index(row, CardDatabaseModel::NameColumn), Qt::DisplayRole).toString(), - card.getName()) - << "row " << row; - } -} - -TEST_F(CardDatabaseVisualDisplayModelTest, ShowsOneRowPerCardWhenNoSetIsFiltered) -{ - const int rowsBefore = visualModel->rowCount(); - - db->addCard(makeCard("Multi", {{"TST", 2}, {"OTH", 1}})); - visualModel->refresh(); - - EXPECT_EQ(rowsBefore + 1, visualModel->rowCount()); - EXPECT_EQ("Multi", visualModel->cardAt(visualModel->rowCount() - 1).getName()); -} - -TEST_F(CardDatabaseVisualDisplayModelTest, ShowsEveryPrintingOfAFilteredSet) -{ - db->addCard(makeCard("Multi", {{"TST", 2}, {"OTH", 1}})); - visualModel->refresh(); - - visualModel->setSetFilterTerms({"TST"}); - - ASSERT_EQ(2, visualModel->rowCount()); - for (int row = 0; row < visualModel->rowCount(); ++row) { - const ExactCard card = visualModel->cardAt(row); - ASSERT_TRUE(card) << "row " << row; - EXPECT_EQ("Multi", card.getName()); - ASSERT_FALSE(card.getPrinting().isEmpty()) << "row " << row; - EXPECT_EQ("TST", card.getPrinting().getSet()->getShortName()) << "row " << row; - } - EXPECT_NE(visualModel->cardAt(0).getPrinting(), visualModel->cardAt(1).getPrinting()); -} - -TEST_F(CardDatabaseVisualDisplayModelTest, HidesCardsWithoutAPrintingInTheFilteredSets) -{ - visualModel->setSetFilterTerms({"NO SUCH SET"}); - - EXPECT_EQ(0, visualModel->rowCount()); -} - -TEST_F(CardDatabaseVisualDisplayModelTest, RestoresOneRowPerCardWhenTheSetTermsClear) -{ - db->addCard(makeCard("Multi", {{"TST", 2}, {"OTH", 1}})); - visualModel->refresh(); - const int rowsBefore = visualModel->rowCount(); - - visualModel->setSetFilterTerms({"TST"}); - ASSERT_EQ(2, visualModel->rowCount()); - - visualModel->setSetFilterTerms({}); - EXPECT_EQ(rowsBefore, visualModel->rowCount()); -} - -TEST_F(CardDatabaseVisualDisplayModelTest, RebuildsByItselfWhenTheProxyGainsARow) -{ - const int rowsBefore = visualModel->rowCount(); - - db->addCard(makeCard("Late", {{"TST", 1}})); - waitForRebuild(); - - EXPECT_EQ(rowsBefore + 1, visualModel->rowCount()); -} - -TEST_F(CardDatabaseVisualDisplayModelTest, ReportsTheCardNameForDisplayAndAccessibleText) -{ - ASSERT_GT(visualModel->rowCount(), 0); - const QString expected = visualModel->cardAt(0).getName(); - - EXPECT_EQ(expected, visualModel->data(visualModel->index(0), Qt::DisplayRole).toString()); - EXPECT_EQ(expected, visualModel->data(visualModel->index(0), Qt::AccessibleTextRole).toString()); - EXPECT_FALSE(visualModel->data(QModelIndex(), Qt::DisplayRole).isValid()); -} -} // namespace diff --git a/tests/client_login_status_test.cpp b/tests/client_login_status_test.cpp deleted file mode 100644 index d8ea63550..000000000 --- a/tests/client_login_status_test.cpp +++ /dev/null @@ -1,116 +0,0 @@ -/** @file client_login_status_test.cpp - * @brief The login response state machine must keep the session a forced password change needs. - * @ingroup Tests - */ - -#include "abstract_client.h" -#include "libcockatrice/interfaces/interface_network_settings_provider.h" -#include "libcockatrice/protocol/pb/response.pb.h" -#include "libcockatrice/protocol/pb/response_login.pb.h" -#include "remote_client.h" - -#include -#include -#include -#include - -namespace -{ - -class StubNetworkSettingsProvider : public INetworkSettingsProvider -{ -public: - QString getClientID() override - { - return {}; - } - [[nodiscard]] int getTimeOut() const override - { - return 1000; - } - [[nodiscard]] int getKeepAlive() const override - { - return 60; - } - [[nodiscard]] bool getNotifyAboutUpdates() const override - { - return false; - } - void setKnownMissingFeatures(const QString &) override - { - } - QString getKnownMissingFeatures() override - { - return {}; - } -}; - -/** - * @brief Feeds a login response to RemoteClient without a socket. - * - * `loginResponse` is the slot the socket thread runs for the login command's answer, so - * invoking it directly exercises the status transition that decides whether `readData()` - * tears the connection down right after it. - */ -class ClientLoginStatusTest : public ::testing::Test -{ -protected: - void SetUp() override - { - client = new RemoteClient(nullptr, &provider); - } - - void TearDown() override - { - delete client; - client = nullptr; - } - - void deliverLoginResponse(Response::ResponseCode code) - { - Response response; - response.set_response_code(code); - response.MutableExtension(Response_Login::ext); - ASSERT_TRUE( - QMetaObject::invokeMethod(client, "loginResponse", Qt::DirectConnection, Q_ARG(Response, response))); - } - - StubNetworkSettingsProvider provider; - RemoteClient *client = nullptr; -}; - -/** - * @brief A required password change is not a failed login. - * - * The server keeps the session open and accepts only ping and Command_AccountPassword until - * the password is replaced. StatusDisconnecting would make readData() close the socket right - * after this slot returns - before the dialog that collects the new password (queued to the - * UI thread) can send anything - and the pending command would never be answered because the - * ping timer that times it out dies with the connection. StatusLoggedIn would start the - * normal post-login flow for a session that cannot serve it. - */ -TEST_F(ClientLoginStatusTest, PasswordChangeRequiredKeepsTheSessionOpen) -{ - deliverLoginResponse(Response::RespPasswordChangeRequired); - - EXPECT_EQ(client->getStatus(), StatusPasswordChangeRequired); - EXPECT_NE(client->getStatus(), StatusDisconnecting); - EXPECT_NE(client->getStatus(), StatusLoggedIn); -} - -/** @brief Every other login error still ends the connection, as it did before. */ -TEST_F(ClientLoginStatusTest, OtherLoginErrorsDisconnect) -{ - deliverLoginResponse(Response::RespWrongPassword); - - EXPECT_EQ(client->getStatus(), StatusDisconnecting); -} - -} // namespace - -int main(int argc, char **argv) -{ - QCoreApplication app(argc, argv); - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/tests/deck_list_model/CMakeLists.txt b/tests/deck_list_model/CMakeLists.txt index 82a1965d2..cd7d693f3 100644 --- a/tests/deck_list_model/CMakeLists.txt +++ b/tests/deck_list_model/CMakeLists.txt @@ -21,15 +21,3 @@ cockatrice_add_test( libcockatrice_card libcockatrice_deck_list ) - -cockatrice_add_test( - NAME - collator_sort_test - SOURCES - ${VERSION_STRING_CPP} - collator_sort_test.cpp - LIBS - libcockatrice_models - libcockatrice_card - libcockatrice_deck_list -) diff --git a/tests/deck_list_model/collator_sort_test.cpp b/tests/deck_list_model/collator_sort_test.cpp deleted file mode 100644 index b4ed85525..000000000 --- a/tests/deck_list_model/collator_sort_test.cpp +++ /dev/null @@ -1,30 +0,0 @@ -#include -#include -#include -#include -#include -#include - -namespace -{ -TEST(CollatorSort, NumericNamesSortNaturally) -{ - QStandardItemModel source(0, 2); - const QStringList names = {"Forest 10", "Forest 2", "Forest 1"}; - for (int row = 0; row < names.size(); ++row) { - source.setItem(row, DeckListModelColumns::CARD_NAME, new QStandardItem(names.at(row))); - } - - DeckListSortFilterProxyModel proxy; - proxy.setSourceModel(&source); - proxy.setSortCriteria({"name"}); - proxy.sort(DeckListModelColumns::CARD_NAME, Qt::AscendingOrder); - - QStringList actual; - for (int row = 0; row < proxy.rowCount(); ++row) { - actual.append(proxy.index(row, DeckListModelColumns::CARD_NAME).data(Qt::DisplayRole).toString()); - } - - EXPECT_EQ(actual, (QStringList{"Forest 1", "Forest 2", "Forest 10"})); -} -} // namespace diff --git a/tests/game/CMakeLists.txt b/tests/game/CMakeLists.txt deleted file mode 100644 index 42c935c8f..000000000 --- a/tests/game/CMakeLists.txt +++ /dev/null @@ -1,18 +0,0 @@ -cockatrice_add_test( - NAME - game_logic_test - SOURCES - game_test_main.cpp - card_state_test.cpp - card_zone_logic_test.cpp - game_event_handler_test.cpp - player_actions_test.cpp - replay_test.cpp - ${VERSION_STRING_CPP} - LIBS - libcockatrice_game - INCLUDE_DIRS - ${CMAKE_CURRENT_SOURCE_DIR} - DEFINITIONS - GAME_TEST_DATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data" -) diff --git a/tests/game/card_state_test.cpp b/tests/game/card_state_test.cpp deleted file mode 100644 index 2f2c43bf4..000000000 --- a/tests/game/card_state_test.cpp +++ /dev/null @@ -1,110 +0,0 @@ -#include "gtest/gtest.h" -#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(CardStateTest, CountersAddUpdateAndClear) -{ - CardState card(nullptr, nullptr, cardRefFor(QStringLiteral("Cat")), 1); - int changes = 0; - QObject::connect(&card, &CardState::countersChanged, [&changes]() { ++changes; }); - - card.insertCounter(1, 2); - EXPECT_EQ(card.getCounters().value(1), 2); - - card.setCounter(1, 3); - EXPECT_EQ(card.getCounters().value(1), 3); - - card.setCounter(1, 0); - EXPECT_FALSE(card.getCounters().contains(1)); - - card.setCounter(2, 1); - card.clearCounters(); - EXPECT_TRUE(card.getCounters().isEmpty()); - - EXPECT_EQ(changes, 5); -} - -TEST(CardStateTest, TapFlipAttackingAndReset) -{ - CardState card(nullptr, nullptr, cardRefFor(QStringLiteral("Cat")), 1); - card.setTapped(true); - card.setFaceDown(true); - card.setAttacking(true); - card.setAnnotation(QStringLiteral("note")); - card.setPT(QStringLiteral("4/4")); - card.setDoesntUntap(true); - card.setDestroyOnZoneChange(true); - card.setColor(QStringLiteral("red")); - card.insertCounter(1, 2); - - card.resetState(true); - - EXPECT_FALSE(card.getTapped()); - EXPECT_FALSE(card.getAttacking()); - EXPECT_TRUE(card.getCounters().isEmpty()); - EXPECT_TRUE(card.getPT().isEmpty()); - EXPECT_FALSE(card.getDoesntUntap()); - EXPECT_EQ(card.getAnnotation(), QStringLiteral("note")); - // resetState() deliberately leaves these three alone. - EXPECT_TRUE(card.getFaceDown()); - EXPECT_EQ(card.getColor(), QStringLiteral("red")); - EXPECT_TRUE(card.getDestroyOnZoneChange()); - - card.setAnnotation(QStringLiteral("other")); - card.resetState(false); - EXPECT_TRUE(card.getAnnotation().isEmpty()); -} - -TEST(CardStateTest, ParsePT) -{ - const QVariantList plain = CardState::parsePT(QStringLiteral("3/3")); - ASSERT_EQ(plain.size(), 2); - EXPECT_EQ(plain.at(0).toString(), QStringLiteral("3")); - EXPECT_EQ(plain.at(1).toString(), QStringLiteral("3")); - - const QVariantList negative = CardState::parsePT(QStringLiteral("-1/2")); - ASSERT_EQ(negative.size(), 2); - EXPECT_EQ(negative.at(0).typeId(), QMetaType::Int); - EXPECT_EQ(negative.at(0).toInt(), -1); - EXPECT_EQ(negative.at(1).toString(), QStringLiteral("2")); - - const QVariantList star = CardState::parsePT(QStringLiteral("*")); - ASSERT_EQ(star.size(), 1); - EXPECT_EQ(star.at(0).toString(), QStringLiteral("*")); - - EXPECT_TRUE(CardState::parsePT(QString()).isEmpty()); -} diff --git a/tests/game/card_zone_logic_test.cpp b/tests/game/card_zone_logic_test.cpp deleted file mode 100644 index 7f0c39b47..000000000 --- a/tests/game/card_zone_logic_test.cpp +++ /dev/null @@ -1,107 +0,0 @@ -#include "gtest/gtest.h" -#include -#include -#include -#include -#include -#include - -namespace -{ - -CardRef cardRefFor(const QString &name) -{ - CardRef cardRef; - cardRef.name = name; - return cardRef; -} - -} // namespace - -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); - // The graphics side owns the view logic and registers it on the zone it - // shows; adding a card then goes through prepareAddCard and the request - // signal instead of the test emitting the signal itself. - ZoneViewZoneLogic view(nullptr, &zone, 1, true, false, false); - zone.getViews().append(&view); - - QSignalSpy requestSpy(&view, &CardZoneLogic::createCardRequested); - - CardState card(nullptr, nullptr, cardRefFor(QStringLiteral("Lightning Bolt")), 7); - zone.addCard(&card, true, 0, 0); - - ASSERT_EQ(requestSpy.count(), 1); - const QList arguments = requestSpy.takeFirst(); - const ServerInfo_Card cardInfo = arguments.at(0).value(); - EXPECT_EQ(cardInfo.id(), 7); - EXPECT_EQ(cardInfo.name(), "Lightning Bolt"); - EXPECT_TRUE(arguments.at(1).toBool()); - EXPECT_EQ(card.getZone(), &zone); - EXPECT_EQ(zone.getCards().size(), 1); -} - -TEST(CardZoneLogicTest, MoveBetweenZonesUpdatesCardZone) -{ - HandZoneLogic first(nullptr, QStringLiteral("hand"), false, false, true); - HandZoneLogic second(nullptr, QStringLiteral("grave"), false, false, true); - CardState card(nullptr, nullptr, cardRefFor(QStringLiteral("Cat")), 7); - - first.addCard(&card, false, 0); - ASSERT_EQ(first.getCards().size(), 1); - - CardState *taken = first.takeCard(0, 7); - ASSERT_EQ(taken, &card); - EXPECT_TRUE(first.getCards().isEmpty()); - - second.addCard(taken, false, 0); - EXPECT_EQ(second.getCards().size(), 1); - EXPECT_EQ(card.getZone(), &second); -} - -TEST(CardZoneLogicTest, GetCardFindsById) -{ - HandZoneLogic zone(nullptr, QStringLiteral("hand"), false, false, true); - CardState first(nullptr, nullptr, cardRefFor(QStringLiteral("Cat")), 7); - CardState second(nullptr, nullptr, cardRefFor(QStringLiteral("Dog")), 8); - - zone.addCard(&first, false, 0); - zone.addCard(&second, false, 0); - - EXPECT_EQ(zone.getCard(7), &first); - EXPECT_EQ(zone.getCard(8), &second); - EXPECT_EQ(zone.getCard(99), nullptr); -} - -TEST(CardZoneLogicTest, ReorganizeSignalFiresOnAdd) -{ - HandZoneLogic zone(nullptr, QStringLiteral("hand"), false, false, true); - CardState card(nullptr, nullptr, cardRefFor(QStringLiteral("Cat")), 7); - QSignalSpy reorganizeSpy(&zone, &CardZoneLogic::reorganizeCards); - - zone.addCard(&card, true, 0); - - EXPECT_EQ(reorganizeSpy.count(), 1); -} diff --git a/tests/game/data/cards.xml b/tests/game/data/cards.xml deleted file mode 100644 index e315dbf48..000000000 --- a/tests/game/data/cards.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - Cat - CAT - 0 - Meow! - - 111 - G - 2G - 2 - Creature — Cat - Creature - 3/3 - - - - Grizzly Bears - LEA - 0 - 1 - - - 112 - G - 1G - 2 - Creature — Bear - Creature - 2/2 - - - - Lightning Bolt - LEA - 3 - Lightning Bolt deals 3 damage to any target. - - 113 - R - R - 1 - Instant - Instant - - - - Forest - LEA - 2 - (Add G.) - - 114 - G - - 0 - Land - Land - - - - diff --git a/tests/game/data/spoilers.xml b/tests/game/data/spoilers.xml deleted file mode 100644 index 414ae10d4..000000000 --- a/tests/game/data/spoilers.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/tests/game/data/tokens.xml b/tests/game/data/tokens.xml deleted file mode 100644 index 30edbb24c..000000000 --- a/tests/game/data/tokens.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - Kitten - CAT - 0 - - 1 - - 211 - G - - - Token - Token - 1/1 - - - - diff --git a/tests/game/game_event_handler_test.cpp b/tests/game/game_event_handler_test.cpp deleted file mode 100644 index 2db6a1660..000000000 --- a/tests/game/game_event_handler_test.cpp +++ /dev/null @@ -1,566 +0,0 @@ -#include "game_test_fixture.h" - -#include "gtest/gtest.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ - -/** - * Wraps one player-scoped event in the container the server would send and - * pushes it through the real dispatcher, so the test exercises the same - * GameEventHandler -> PlayerEventHandler path as a live client. - */ -template -void sendGameEvent(GameHarness &harness, - int playerId, - const EventType &message, - const GameEventContext *context = nullptr) -{ - GameEventContainer container; - container.set_game_id(42); - if (context != nullptr) { - container.mutable_context()->CopyFrom(*context); - } - - GameEvent *event = container.add_event_list(); - event->set_player_id(playerId); - event->MutableExtension(EventType::ext)->CopyFrom(message); - - harness.game()->getGameEventHandler()->processGameEventContainer(container, harness.client(), - EventProcessingOptions()); -} - -Event_SetCardAttr cardAttrEvent(const char *zoneName, int cardId, CardAttribute attribute, const char *value) -{ - Event_SetCardAttr event; - event.set_zone_name(zoneName); - event.set_card_id(cardId); - event.set_attribute(attribute); - event.set_attr_value(value); - return event; -} - -} // namespace - -TEST(GameEventHandlerTest, MoveCardMovesBetweenZones) -{ - GameHarness harness; - CardState *cat = harness.addCardToZone(ZoneNames::HAND, QStringLiteral("Cat"), 7); - ASSERT_NE(cat, nullptr); - - Event_MoveCard event; - event.set_card_id(7); - event.set_start_player_id(1); - event.set_start_zone(ZoneNames::HAND); - event.set_position(0); - event.set_target_player_id(1); - event.set_target_zone(ZoneNames::TABLE); - event.set_x(0); - event.set_y(TableZoneLogic::tableRowToGridY(0)); - event.set_new_card_id(7); - event.set_face_down(false); - - sendGameEvent(harness, 1, event); - - EXPECT_TRUE(harness.zone(ZoneNames::HAND)->getCards().isEmpty()); - CardZoneLogic *table = harness.zone(ZoneNames::TABLE); - ASSERT_EQ(table->getCards().size(), 1); - EXPECT_EQ(table->getCards().first(), cat); - EXPECT_EQ(cat->getZone(), table); - EXPECT_EQ(cat->getId(), 7); - EXPECT_EQ(cat->getCardRef().name, QStringLiteral("Cat")); - EXPECT_FALSE(cat->getFaceDown()); - EXPECT_EQ(cat->getPT(), QStringLiteral("3/3")); - EXPECT_EQ(cat->getGridPoint(), QPoint(0, TableZoneLogic::tableRowToGridY(0))); -} - -TEST(GameEventHandlerTest, MoveCardFaceDownKeepsTheCardHidden) -{ - GameHarness harness; - CardState *cat = harness.addCardToZone(ZoneNames::HAND, QStringLiteral("Cat"), 7); - ASSERT_NE(cat, nullptr); - - Event_MoveCard event; - event.set_card_id(7); - event.set_start_player_id(1); - event.set_start_zone(ZoneNames::HAND); - event.set_position(0); - event.set_target_player_id(1); - event.set_target_zone(ZoneNames::TABLE); - event.set_x(1); - event.set_y(TableZoneLogic::tableRowToGridY(2)); - event.set_new_card_id(7); - event.set_face_down(true); - - sendGameEvent(harness, 1, event); - - ASSERT_EQ(harness.zone(ZoneNames::TABLE)->getCards().size(), 1); - EXPECT_TRUE(cat->getFaceDown()); - // A face-down card keeps whatever P/T it had instead of picking up the - // printed one from the card database. - EXPECT_TRUE(cat->getPT().isEmpty()); -} - -TEST(GameEventHandlerTest, SetCardAttrUpdatesSingleCard) -{ - GameHarness harness; - CardState *cat = harness.addCardToZone(ZoneNames::TABLE, QStringLiteral("Cat"), 7); - ASSERT_NE(cat, nullptr); - - sendGameEvent(harness, 1, cardAttrEvent(ZoneNames::TABLE, 7, AttrTapped, "1")); - sendGameEvent(harness, 1, cardAttrEvent(ZoneNames::TABLE, 7, AttrAttacking, "1")); - sendGameEvent(harness, 1, cardAttrEvent(ZoneNames::TABLE, 7, AttrPT, "5/5")); - sendGameEvent(harness, 1, cardAttrEvent(ZoneNames::TABLE, 7, AttrAnnotation, "note")); - sendGameEvent(harness, 1, cardAttrEvent(ZoneNames::TABLE, 7, AttrColor, "red")); - sendGameEvent(harness, 1, cardAttrEvent(ZoneNames::TABLE, 7, AttrDoesntUntap, "1")); - sendGameEvent(harness, 1, cardAttrEvent(ZoneNames::TABLE, 7, AttrFaceDown, "1")); - - EXPECT_TRUE(cat->getTapped()); - EXPECT_TRUE(cat->getAttacking()); - EXPECT_EQ(cat->getPT(), QStringLiteral("5/5")); - EXPECT_EQ(cat->getAnnotation(), QStringLiteral("note")); - EXPECT_EQ(cat->getColor(), QStringLiteral("red")); - EXPECT_TRUE(cat->getDoesntUntap()); - EXPECT_TRUE(cat->getFaceDown()); -} - -TEST(GameEventHandlerTest, SetCardAttrWithoutCardIdTouchesEveryCardInZone) -{ - GameHarness harness; - CardState *cat = harness.addCardToZone(ZoneNames::TABLE, QStringLiteral("Cat"), 7); - CardState *bears = harness.addCardToZone(ZoneNames::TABLE, QStringLiteral("Grizzly Bears"), 9); - ASSERT_NE(cat, nullptr); - ASSERT_NE(bears, nullptr); - - Event_SetCardAttr event; - event.set_zone_name(ZoneNames::TABLE); - event.set_attribute(AttrAnnotation); - event.set_attr_value("marked"); - - sendGameEvent(harness, 1, event); - - EXPECT_EQ(cat->getAnnotation(), QStringLiteral("marked")); - EXPECT_EQ(bears->getAnnotation(), QStringLiteral("marked")); -} - -TEST(GameEventHandlerTest, UntapAllLeavesDoesntUntapCardsTapped) -{ - GameHarness harness; - CardState *cat = harness.addCardToZone(ZoneNames::TABLE, QStringLiteral("Cat"), 7); - CardState *bears = harness.addCardToZone(ZoneNames::TABLE, QStringLiteral("Grizzly Bears"), 9); - ASSERT_NE(cat, nullptr); - ASSERT_NE(bears, nullptr); - cat->setDoesntUntap(true); - - sendGameEvent(harness, 1, cardAttrEvent(ZoneNames::TABLE, 7, AttrTapped, "1")); - sendGameEvent(harness, 1, cardAttrEvent(ZoneNames::TABLE, 9, AttrTapped, "1")); - - Event_SetCardAttr untapAll; - untapAll.set_zone_name(ZoneNames::TABLE); - untapAll.set_attribute(AttrTapped); - untapAll.set_attr_value("0"); - - sendGameEvent(harness, 1, untapAll); - - EXPECT_TRUE(cat->getTapped()); - EXPECT_FALSE(bears->getTapped()); -} - -TEST(GameEventHandlerTest, SetCardCounterAddsAndRemoves) -{ - GameHarness harness; - CardState *cat = harness.addCardToZone(ZoneNames::HAND, QStringLiteral("Cat"), 7); - ASSERT_NE(cat, nullptr); - - Event_SetCardCounter add; - add.set_zone_name(ZoneNames::HAND); - add.set_card_id(7); - add.set_counter_id(1); - add.set_counter_value(2); - - sendGameEvent(harness, 1, add); - - EXPECT_EQ(cat->getCounters().value(1), 2); - - Event_SetCardCounter remove = add; - remove.set_counter_value(0); - - sendGameEvent(harness, 1, remove); - - EXPECT_FALSE(cat->getCounters().contains(1)); -} - -TEST(GameEventHandlerTest, AttachCardLinksBothEndsAndUnattachClearsThem) -{ - GameHarness harness; - CardState *host = harness.addCardToZone(ZoneNames::TABLE, QStringLiteral("Cat"), 7); - CardState *guest = harness.addCardToZone(ZoneNames::TABLE, QStringLiteral("Grizzly Bears"), 9); - ASSERT_NE(host, nullptr); - ASSERT_NE(guest, nullptr); - - Event_AttachCard attach; - attach.set_start_zone(ZoneNames::TABLE); - attach.set_card_id(9); - attach.set_target_player_id(1); - attach.set_target_zone(ZoneNames::TABLE); - attach.set_target_card_id(7); - - sendGameEvent(harness, 1, attach); - - EXPECT_EQ(guest->getAttachedTo(), host); - ASSERT_EQ(host->getAttachedCards().size(), 1); - EXPECT_EQ(host->getAttachedCards().first(), guest); - - Event_AttachCard detach; - detach.set_start_zone(ZoneNames::TABLE); - detach.set_card_id(9); - - sendGameEvent(harness, 1, detach); - - EXPECT_EQ(guest->getAttachedTo(), nullptr); - EXPECT_TRUE(host->getAttachedCards().isEmpty()); -} - -TEST(GameEventHandlerTest, ChangeZonePropertiesUpdatesReveal) -{ - GameHarness harness; - CardZoneLogic *deck = harness.zone(ZoneNames::DECK); - ASSERT_NE(deck, nullptr); - - Event_ChangeZoneProperties reveal; - reveal.set_zone_name(ZoneNames::DECK); - reveal.set_always_reveal_top_card(true); - - sendGameEvent(harness, 1, reveal); - EXPECT_TRUE(deck->getAlwaysRevealTopCard()); - - reveal.set_always_reveal_top_card(false); - sendGameEvent(harness, 1, reveal); - EXPECT_FALSE(deck->getAlwaysRevealTopCard()); -} - -TEST(GameEventHandlerTest, DrawCardsMovesNamedCardsFromDeckToHand) -{ - GameHarness harness; - ASSERT_NE(harness.addCardToZone(ZoneNames::DECK, QStringLiteral("Cat"), 7), nullptr); - - Event_DrawCards draw; - draw.set_number(1); - ServerInfo_Card *cardInfo = draw.add_cards(); - cardInfo->set_id(7); - cardInfo->set_name("Cat"); - - sendGameEvent(harness, 1, draw); - - EXPECT_TRUE(harness.zone(ZoneNames::DECK)->getCards().isEmpty()); - ASSERT_EQ(harness.zone(ZoneNames::HAND)->getCards().size(), 1); - CardState *drawn = harness.zone(ZoneNames::HAND)->getCards().first(); - EXPECT_EQ(drawn->getId(), 7); - EXPECT_EQ(drawn->getName(), QStringLiteral("Cat")); -} - -TEST(GameEventHandlerTest, FlipCardHidesAndRevealsTheCard) -{ - GameHarness harness; - CardState *cat = harness.addCardToZone(ZoneNames::TABLE, QStringLiteral("Cat"), 7); - ASSERT_NE(cat, nullptr); - - Event_FlipCard flip; - flip.set_zone_name(ZoneNames::TABLE); - flip.set_card_id(7); - flip.set_face_down(true); - - sendGameEvent(harness, 1, flip); - - EXPECT_TRUE(cat->getFaceDown()); - // Flipping face down does not touch the card reference: the client must not - // learn (or overwrite) the name of a card it is not allowed to see. - EXPECT_EQ(cat->getName(), QStringLiteral("Cat")); - - // Turning it face up carries the real printing, so the state picks it up. - flip.set_face_down(false); - flip.set_card_name("Grizzly Bears"); - - sendGameEvent(harness, 1, flip); - - EXPECT_FALSE(cat->getFaceDown()); - EXPECT_EQ(cat->getName(), QStringLiteral("Grizzly Bears")); -} - -TEST(GameEventHandlerTest, DestroyCardRemovesItAndDetachesAttachedCards) -{ - GameHarness harness; - CardState *host = harness.addCardToZone(ZoneNames::TABLE, QStringLiteral("Cat"), 7); - CardState *guest = harness.addCardToZone(ZoneNames::TABLE, QStringLiteral("Grizzly Bears"), 9); - ASSERT_NE(host, nullptr); - ASSERT_NE(guest, nullptr); - - guest->setAttachedTo(host); - ASSERT_EQ(guest->getAttachedTo(), host); - ASSERT_EQ(host->getAttachedCards().size(), 1); - - Event_DestroyCard destroy; - destroy.set_zone_name(ZoneNames::TABLE); - destroy.set_card_id(7); - - sendGameEvent(harness, 1, destroy); - - CardZoneLogic *table = harness.zone(ZoneNames::TABLE); - ASSERT_EQ(table->getCards().size(), 1); - EXPECT_EQ(table->getCards().first(), guest); - EXPECT_FALSE(table->getCards().contains(host)); - - // The attached card survives the host and is left un-attached, ready to be - // laid out again by the zone. - EXPECT_EQ(guest->getAttachedTo(), nullptr); - EXPECT_TRUE(host->getAttachedCards().isEmpty()); - EXPECT_EQ(guest->getZone(), table); -} - -TEST(GameEventHandlerTest, CreateTokenUsesDatabasePTUnlessFaceDown) -{ - GameHarness harness; - CardZoneLogic *table = harness.zone(ZoneNames::TABLE); - ASSERT_NE(table, nullptr); - - QList created; - QObject::connect(table, &CardZoneLogic::createCardRequested, table, - [&created](const ServerInfo_Card &cardInfo, bool) { created.append(cardInfo); }); - - Event_CreateToken token; - token.set_zone_name(ZoneNames::TABLE); - token.set_card_id(11); - token.set_card_name("Kitten"); - token.set_x(2); - token.set_y(1); - token.set_color("green"); - token.set_annotation("a kitten"); - token.set_destroy_on_zone_change(true); - - sendGameEvent(harness, 1, token); - - ASSERT_EQ(created.size(), 1); - // The event carries no P/T, so the token takes the printed one. - EXPECT_EQ(created.first().pt(), "1/1"); - EXPECT_EQ(created.first().name(), "Kitten"); - EXPECT_EQ(created.first().x(), 2); - EXPECT_EQ(created.first().y(), 1); - EXPECT_EQ(created.first().color(), "green"); - EXPECT_EQ(created.first().annotation(), "a kitten"); - EXPECT_TRUE(created.first().destroy_on_zone_change()); - EXPECT_FALSE(created.first().face_down()); - - // An explicit P/T always wins over the database. - Event_CreateToken explicitPt = token; - explicitPt.set_card_id(12); - explicitPt.set_pt("3/3"); - - sendGameEvent(harness, 1, explicitPt); - - ASSERT_EQ(created.size(), 2); - EXPECT_EQ(created.last().pt(), "3/3"); - - // A face-down token must not leak the printed P/T. - Event_CreateToken faceDown = token; - faceDown.set_card_id(13); - faceDown.set_face_down(true); - - sendGameEvent(harness, 1, faceDown); - - ASSERT_EQ(created.size(), 3); - EXPECT_TRUE(created.last().face_down()); - EXPECT_TRUE(created.last().pt().empty()); -} - -TEST(GameEventHandlerTest, ConcedeAndUnconcedeUpdatePlayerState) -{ - GameHarness harness; - ASSERT_NE(harness.addCardToZone(ZoneNames::TABLE, QStringLiteral("Cat"), 7), nullptr); - EXPECT_FALSE(harness.player()->getConceded()); - - Event_PlayerPropertiesChanged properties; - properties.mutable_player_properties()->set_player_id(1); - properties.mutable_player_properties()->set_conceded(true); - - GameEventContext concedeContext; - concedeContext.MutableExtension(Context_Concede::ext); - - sendGameEvent(harness, 1, properties, &concedeContext); - - EXPECT_TRUE(harness.player()->getConceded()); - // Conceding tears the board down for that player. - EXPECT_TRUE(harness.zone(ZoneNames::TABLE)->getCards().isEmpty()); - - Event_PlayerPropertiesChanged back = properties; - back.mutable_player_properties()->set_conceded(false); - - GameEventContext unconcedeContext; - unconcedeContext.MutableExtension(Context_Unconcede::ext); - - sendGameEvent(harness, 1, back, &unconcedeContext); - - EXPECT_FALSE(harness.player()->getConceded()); -} - -TEST(GameEventHandlerTest, SetActivePlayerAndPhaseUpdateGameState) -{ - GameHarness harness; - GameState *state = harness.game()->getGameState(); - ASSERT_NE(state, nullptr); - EXPECT_EQ(state->getActivePlayer(), -1); - EXPECT_EQ(state->getCurrentPhase(), -1); - - Event_SetActivePlayer setPlayer; - setPlayer.set_active_player_id(1); - - sendGameEvent(harness, 1, setPlayer); - - EXPECT_EQ(state->getActivePlayer(), 1); - - Event_SetActivePhase setPhase; - setPhase.set_phase(3); - - sendGameEvent(harness, 1, setPhase); - - EXPECT_EQ(state->getCurrentPhase(), 3); -} - -TEST(GameEventHandlerTest, CreateSetAndDeletePlayerCounter) -{ - GameHarness harness; - - ServerInfo_Counter counter; - counter.set_id(1); - counter.set_name("life"); - counter.set_radius(1); - counter.set_count(20); - - Event_CreateCounter create; - create.mutable_counter_info()->CopyFrom(counter); - - sendGameEvent(harness, 1, create); - - CounterState *state = harness.player()->getCounters().value(1); - ASSERT_NE(state, nullptr); - EXPECT_EQ(state->getName(), QStringLiteral("life")); - EXPECT_EQ(state->getValue(), 20); - EXPECT_EQ(harness.player()->getLifeCounter(), state); - - Event_SetCounter set; - set.set_counter_id(1); - set.set_value(15); - - sendGameEvent(harness, 1, set); - - EXPECT_EQ(state->getValue(), 15); - - // Updating a counter that does not exist is a no-op, not a crash. - Event_SetCounter unknown = set; - unknown.set_counter_id(99); - sendGameEvent(harness, 1, unknown); - - EXPECT_FALSE(harness.player()->getCounters().contains(99)); - - Event_DelCounter remove; - remove.set_counter_id(1); - - sendGameEvent(harness, 1, remove); - - EXPECT_FALSE(harness.player()->getCounters().contains(1)); - EXPECT_EQ(harness.player()->getLifeCounter(), nullptr); -} - -TEST(GameEventHandlerTest, LeaveRemovesThePlayer) -{ - GameHarness harness; - ASSERT_NE(harness.addCardToZone(ZoneNames::TABLE, QStringLiteral("Cat"), 7), nullptr); - ASSERT_NE(harness.game()->getPlayerManager()->getPlayer(1), nullptr); - - Event_Leave leave; - leave.set_reason(Event_Leave::USER_LEFT); - - sendGameEvent(harness, 1, leave); - - EXPECT_EQ(harness.game()->getPlayerManager()->getPlayer(1), nullptr); - EXPECT_TRUE(harness.game()->getPlayerManager()->getPlayers().isEmpty()); -} - -TEST(GameEventHandlerTest, KickedSignalsGameClosed) -{ - GameHarness harness; - - bool closed = false; - QObject::connect(harness.game()->getGameEventHandler(), &GameEventHandler::gameClosed, - [&closed] { closed = true; }); - - sendGameEvent(harness, 1, Event_Kicked()); - - EXPECT_TRUE(closed); -} - -TEST(GameEventHandlerTest, GameClosedMarksTheSessionClosed) -{ - GameHarness harness; - ASSERT_FALSE(harness.game()->getGameState()->isGameClosed()); - - Event_GameStateChanged started; - started.set_game_started(true); - sendGameEvent(harness, 1, started); - ASSERT_TRUE(harness.game()->getGameMetaInfo()->started()); - - sendGameEvent(harness, 1, Event_GameClosed()); - - EXPECT_TRUE(harness.game()->getGameState()->isGameClosed()); - EXPECT_FALSE(harness.game()->getGameMetaInfo()->started()); -} - -TEST(GameEventHandlerTest, GameHostChangedMovesTheHostRole) -{ - GameHarness harness; - - ServerInfo_User bob; - bob.set_name("Bob"); - bob.set_user_level(ServerInfo_User::IsUser); - ASSERT_NE(harness.game()->getPlayerManager()->addPlayer(2, bob), nullptr); - - sendGameEvent(harness, 2, Event_GameHostChanged()); - - EXPECT_EQ(harness.game()->getGameState()->getHostId(), 2); -} diff --git a/tests/game/game_test_fixture.h b/tests/game/game_test_fixture.h deleted file mode 100644 index e4703c917..000000000 --- a/tests/game/game_test_fixture.h +++ /dev/null @@ -1,129 +0,0 @@ -/** - * @file game_test_fixture.h - * - * Fixtures for tests that need a complete game/player/zone graph without the - * client, a server or any graphics object. Commands are captured on a recording - * AbstractClient, which is the same seam the real client uses. - */ - -#ifndef COCKATRICE_TESTS_GAME_TEST_FIXTURE_H -#define COCKATRICE_TESTS_GAME_TEST_FIXTURE_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -/** - * Captures every command the game logic sends, instead of writing it to a socket. - * - * AbstractClient::sendCommand uses a direct signal/slot connection in the same - * thread, so a command is in `sent` as soon as sendCommand returns. - */ -class RecordingClient : public AbstractClient -{ -public: - QList sent; - -protected: - void sendCommandContainer(const CommandContainer &cont) override - { - sent.append(cont); - } -}; - -/** - * Owns a local, single-client Game with one local player, plus the cards added - * to its zones. - */ -class GameHarness -{ -public: - explicit GameHarness(int playerId = 1) : playerId_(playerId), client_(std::make_unique()) - { - Event_GameJoined event; - event.set_host_id(playerId_); - event.set_player_id(playerId_); - event.mutable_game_info()->set_game_id(42); - event.mutable_game_info()->set_started(true); - - QList clients{client_.get()}; - QMap roomGameTypes; - game_ = std::make_unique(nullptr, true, clients, event, roomGameTypes); - - ServerInfo_User user; - user.set_name("Alice"); - user.set_user_level(ServerInfo_User::IsUser | ServerInfo_User::IsRegistered); - game_->getPlayerManager()->addPlayer(playerId_, user); - } - - ~GameHarness() - { - qDeleteAll(ownedCards_); - ownedCards_.clear(); - } - - GameHarness(const GameHarness &) = delete; - GameHarness &operator=(const GameHarness &) = delete; - - [[nodiscard]] Game *game() const - { - return game_.get(); - } - - [[nodiscard]] RecordingClient *client() const - { - return client_.get(); - } - - [[nodiscard]] PlayerLogic *player() const - { - return game_->getPlayerManager()->getPlayer(playerId_); - } - - [[nodiscard]] CardZoneLogic *zone(const QString &zoneName) const - { - return player()->getZones().value(zoneName); - } - - CardState *addCardToZone(const QString &zoneName, const QString &cardName, int cardId) - { - CardZoneLogic *targetZone = zone(zoneName); - if (!targetZone) { - return nullptr; - } - - auto *card = new CardState(nullptr, targetZone, CardRef{cardName, QString()}, cardId); - // The constructor only stores the ref; this is what fills in the card - // data (table row, P/T, ...) the actions read. - card->refreshCardInfo(); - targetZone->addCard(card, false, 0); - ownedCards_.append(card); - return card; - } - - [[nodiscard]] const CommandContainer &lastCommand() const - { - return client_->sent.last(); - } - -private: - int playerId_; - std::unique_ptr client_; - std::unique_ptr game_; - QList ownedCards_; -}; - -#endif // COCKATRICE_TESTS_GAME_TEST_FIXTURE_H diff --git a/tests/game/game_test_main.cpp b/tests/game/game_test_main.cpp deleted file mode 100644 index 47b9a6b8e..000000000 --- a/tests/game/game_test_main.cpp +++ /dev/null @@ -1,126 +0,0 @@ -/** - * @file game_test_main.cpp - * - * Test entry point for the headless game-logic suite. The game library has no - * graphics dependency, so a QCoreApplication is enough; the only process-wide - * setup left is the card database, which the game logic still reaches through - * CardDatabaseManager and which therefore has to exist before the first zone is - * constructed. - */ - -#include "gtest/gtest.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ - -class GameTestPathProvider : public ICardDatabasePathProvider -{ -public: - explicit GameTestPathProvider(QString baseDir) : baseDir_(std::move(baseDir)) - { - } - - QString getCardDatabasePath() const override - { - return baseDir_ + QStringLiteral("/cards.xml"); - } - - QString getCustomCardDatabasePath() const override - { - return baseDir_ + QStringLiteral("/customsets/"); - } - - QString getTokenDatabasePath() const override - { - return baseDir_ + QStringLiteral("/tokens.xml"); - } - - QString getSpoilerCardDatabasePath() const override - { - return baseDir_ + QStringLiteral("/spoilers.xml"); - } - -private: - QString baseDir_; -}; - -bool copyFixture(const QString &fileName, const QString &targetDir) -{ - const QString source = QString::fromUtf8(GAME_TEST_DATA_DIR) + QLatin1Char('/') + fileName; - const QString target = targetDir + QLatin1Char('/') + fileName; - if (QFile::copy(source, target)) { - return true; - } - - std::fprintf(stderr, "Failed to copy test fixture %s to %s\n", qPrintable(source), qPrintable(target)); - return false; -} - -} // namespace - -int main(int argc, char **argv) -{ - ::testing::InitGoogleTest(&argc, argv); - - // gtest_discover_tests runs the binary with --gtest_list_tests while the - // build is still in progress. No test runs there, and building the card - // database would leave process-lifetime allocations that the sanitizer - // leg's default leak detection reports at exit, failing discovery. - if (::testing::GTEST_FLAG(list_tests)) { - return RUN_ALL_TESTS(); - } - - QStandardPaths::setTestModeEnabled(true); - - QTemporaryDir home; - if (!home.isValid()) { - std::fprintf(stderr, "Failed to create a temporary home directory\n"); - return EXIT_FAILURE; - } - qputenv("HOME", home.path().toLocal8Bit()); - - QCoreApplication app(argc, argv); - QLoggingCategory::setFilterRules(QStringLiteral("card_database.loading=false\n" - "card_database.loading.success_or_failure=false\n" - "settings_cache.*=false")); - - // Copy the fixtures so the database loader never writes its .cache file into - // the source tree, then point the singleton at the copy. - QTemporaryDir dataDir; - if (!dataDir.isValid() || !QDir().mkpath(dataDir.filePath(QStringLiteral("customsets")))) { - std::fprintf(stderr, "Failed to create a temporary card database directory\n"); - return EXIT_FAILURE; - } - if (!copyFixture(QStringLiteral("cards.xml"), dataDir.path()) || - !copyFixture(QStringLiteral("tokens.xml"), dataDir.path()) || - !copyFixture(QStringLiteral("spoilers.xml"), dataDir.path())) { - return EXIT_FAILURE; - } - - // Function-local static: constructed before the CardDatabase singleton below, - // so it is destroyed after it and the raw pointer stored in the manager stays - // valid for the database's lifetime. - static GameTestPathProvider pathProvider(dataDir.path()); - CardDatabaseManager::setCardDatabasePathProvider(&pathProvider); - - CardDatabase *database = CardDatabaseManager::getInstance(); - database->loadCardDatabases(); - if (database->getLoadStatus() != Ok) { - std::fprintf(stderr, "Failed to load the game test card database (status %d)\n", - static_cast(database->getLoadStatus())); - return EXIT_FAILURE; - } - - return RUN_ALL_TESTS(); -} diff --git a/tests/game/player_actions_test.cpp b/tests/game/player_actions_test.cpp deleted file mode 100644 index 11ae7338d..000000000 --- a/tests/game/player_actions_test.cpp +++ /dev/null @@ -1,119 +0,0 @@ -#include "game_test_fixture.h" - -#include "gtest/gtest.h" -#include -#include -#include -#include -#include -#include - -TEST(PlayerActionsTest, PlayCreatureSendsMoveToTable) -{ - GameHarness harness; - CardState *cat = harness.addCardToZone(ZoneNames::HAND, QStringLiteral("Cat"), 7); - ASSERT_NE(cat, nullptr); - - harness.player()->getPlayerActions()->playCard(cat, false); - - ASSERT_FALSE(harness.client()->sent.isEmpty()); - const CommandContainer &container = harness.lastCommand(); - ASSERT_EQ(container.game_command_size(), 1); - const Command_MoveCard command = container.game_command(0).GetExtension(Command_MoveCard::ext); - - EXPECT_EQ(command.start_player_id(), 1); - EXPECT_EQ(command.start_zone(), std::string(ZoneNames::HAND)); - EXPECT_EQ(command.target_player_id(), 1); - EXPECT_EQ(command.target_zone(), std::string(ZoneNames::TABLE)); - EXPECT_EQ(command.x(), -1); - EXPECT_EQ(command.y(), TableZoneLogic::tableRowToGridY(0)); - - ASSERT_EQ(command.cards_to_move().card_size(), 1); - const CardToMove &card = command.cards_to_move().card(0); - EXPECT_EQ(card.card_id(), 7); - EXPECT_TRUE(card.has_face_down()); - EXPECT_FALSE(card.face_down()); - EXPECT_TRUE(card.has_pt()); - EXPECT_EQ(card.pt(), "3/3"); - EXPECT_TRUE(card.has_tapped()); - EXPECT_FALSE(card.tapped()); -} - -TEST(PlayerActionsTest, PlayInstantSendsMoveToStack) -{ - GameHarness harness; - CardState *bolt = harness.addCardToZone(ZoneNames::HAND, QStringLiteral("Lightning Bolt"), 8); - ASSERT_NE(bolt, nullptr); - - harness.player()->getPlayerActions()->playCard(bolt, false); - - ASSERT_FALSE(harness.client()->sent.isEmpty()); - const CommandContainer &container = harness.lastCommand(); - ASSERT_EQ(container.game_command_size(), 1); - const Command_MoveCard command = container.game_command(0).GetExtension(Command_MoveCard::ext); - - EXPECT_EQ(command.target_zone(), std::string(ZoneNames::STACK)); - EXPECT_EQ(command.x(), -1); - EXPECT_EQ(command.y(), 0); - - ASSERT_EQ(command.cards_to_move().card_size(), 1); - const CardToMove &card = command.cards_to_move().card(0); - EXPECT_EQ(card.card_id(), 8); - // The stack branch deliberately leaves the other attributes unset. - EXPECT_FALSE(card.has_face_down()); - EXPECT_FALSE(card.has_pt()); - EXPECT_FALSE(card.has_tapped()); -} - -TEST(PlayerActionsTest, PlayFaceDownCreatureUsesFaceDownRow) -{ - GameHarness harness; - CardState *cat = harness.addCardToZone(ZoneNames::HAND, QStringLiteral("Cat"), 7); - ASSERT_NE(cat, nullptr); - - harness.player()->getPlayerActions()->playCard(cat, true); - - ASSERT_FALSE(harness.client()->sent.isEmpty()); - const CommandContainer &container = harness.lastCommand(); - ASSERT_EQ(container.game_command_size(), 1); - const Command_MoveCard command = container.game_command(0).GetExtension(Command_MoveCard::ext); - - EXPECT_EQ(command.target_zone(), std::string(ZoneNames::TABLE)); - EXPECT_EQ(command.x(), -1); - EXPECT_EQ(command.y(), TableZoneLogic::tableRowToGridY(2)); - - ASSERT_EQ(command.cards_to_move().card_size(), 1); - const CardToMove &card = command.cards_to_move().card(0); - EXPECT_EQ(card.card_id(), 7); - EXPECT_TRUE(card.has_face_down()); - EXPECT_TRUE(card.face_down()); - EXPECT_FALSE(card.has_pt()); - EXPECT_TRUE(card.has_tapped()); - EXPECT_FALSE(card.tapped()); -} - -TEST(PlayerActionsTest, PlayCiptCreatureSendsTapped) -{ - GameHarness harness; - CardState *bears = harness.addCardToZone(ZoneNames::HAND, QStringLiteral("Grizzly Bears"), 9); - ASSERT_NE(bears, nullptr); - - harness.player()->getPlayerActions()->playCard(bears, false); - - ASSERT_FALSE(harness.client()->sent.isEmpty()); - const CommandContainer &container = harness.lastCommand(); - ASSERT_EQ(container.game_command_size(), 1); - const Command_MoveCard command = container.game_command(0).GetExtension(Command_MoveCard::ext); - - EXPECT_EQ(command.target_zone(), std::string(ZoneNames::TABLE)); - - ASSERT_EQ(command.cards_to_move().card_size(), 1); - const CardToMove &card = command.cards_to_move().card(0); - EXPECT_EQ(card.card_id(), 9); - EXPECT_TRUE(card.has_face_down()); - EXPECT_FALSE(card.face_down()); - EXPECT_TRUE(card.has_pt()); - EXPECT_EQ(card.pt(), "2/2"); - EXPECT_TRUE(card.has_tapped()); - EXPECT_TRUE(card.tapped()); -} diff --git a/tests/game/replay_test.cpp b/tests/game/replay_test.cpp deleted file mode 100644 index 218ffdf51..000000000 --- a/tests/game/replay_test.cpp +++ /dev/null @@ -1,349 +0,0 @@ -/** - * @file replay_test.cpp - * - * Replays a synthetic GameReplay through the real dispatcher. This is the only - * place that exercises PlayerLogic::processPlayerInfo / processCardAttachment - * end to end: the game state arrives as a GAME_STATE_CHANGED event and the - * graphics side is replaced by a small materializer that mirrors what - * CardZone::onCreateCardRequested does with the logic objects. - * - * The replay is built in code rather than from a .cor file: event application - * is deterministic (the client never shuffles on its own for a replay), and a - * checked-in binary fixture would be opaque about what it asserts. - */ - -#include "gtest/gtest.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ - -ServerInfo_Card makeCard(int id, const QString &name, int x = -1, int y = -1) -{ - ServerInfo_Card card; - card.set_id(id); - card.set_name(name.toStdString()); - card.set_x(x); - card.set_y(y); - return card; -} - -ServerInfo_Zone makeZone(const char *name, - ServerInfo_Zone::ZoneType type, - bool withCoords, - const QList &cards, - int cardCount = 0) -{ - ServerInfo_Zone zone; - zone.set_name(name); - zone.set_type(type); - zone.set_with_coords(withCoords); - for (const auto &card : cards) { - zone.add_card_list()->CopyFrom(card); - } - if (cardCount > 0) { - zone.set_card_count(cardCount); - } - return zone; -} - -ServerInfo_Player makePlayer(int playerId, const QString &name, const QList &zones) -{ - ServerInfo_Player player; - ServerInfo_PlayerProperties *properties = player.mutable_properties(); - properties->set_player_id(playerId); - properties->mutable_user_info()->set_name(name.toStdString()); - properties->mutable_user_info()->set_user_level(ServerInfo_User::IsUser); - for (const auto &zone : zones) { - player.add_zone_list()->CopyFrom(zone); - } - return player; -} - -template GameEventContainer makeContainer(int playerId, const EventType &message) -{ - GameEventContainer container; - container.set_game_id(42); - - GameEvent *event = container.add_event_list(); - event->set_player_id(playerId); - event->MutableExtension(EventType::ext)->CopyFrom(message); - - return container; -} - -/** - * Stands in for the graphics side of a replay: when the logic asks for a card - * item, create the CardState and add it to the zone, copying the state fields - * the real CardItem::processCardInfo applies. - * - * The production view owns the visual item; the state itself is all a headless - * test needs, and this is the smallest thing that lets processPlayerInfo build - * a real zone. - */ -class HeadlessZoneView : public QObject -{ -public: - using QObject::QObject; - - ~HeadlessZoneView() override - { - qDeleteAll(cards_); - cards_.clear(); - } - - void attach(PlayerLogic *player) - { - for (CardZoneLogic *zone : player->getZones()) { - connect(zone, &CardZoneLogic::createCardRequested, this, - [this, zone](const ServerInfo_Card &cardInfo, bool reorganize) { - materialize(zone, cardInfo, reorganize); - }); - } - } - -private: - void materialize(CardZoneLogic *zone, const ServerInfo_Card &info, bool reorganize) - { - const CardRef cardRef{QString::fromStdString(info.name()), QString::fromStdString(info.provider_id())}; - auto *card = new CardState(nullptr, zone, cardRef, info.id()); - // The real CardItem constructor refreshes the card info before - // processCardInfo overwrites the state fields. - card->refreshCardInfo(); - - card->clearCounters(); - for (int i = 0; i < info.counter_list_size(); ++i) { - card->insertCounter(info.counter_list(i).id(), info.counter_list(i).value()); - } - card->setAttacking(info.attacking()); - card->setFaceDown(info.face_down()); - card->setPT(QString::fromStdString(info.pt())); - card->setAnnotation(QString::fromStdString(info.annotation())); - card->setColor(QString::fromStdString(info.color())); - card->setTapped(info.tapped()); - card->setDestroyOnZoneChange(info.destroy_on_zone_change()); - card->setDoesntUntap(info.doesnt_untap()); - - zone->addCard(card, reorganize, info.x(), info.y()); - cards_.append(card); - } - - QList cards_; -}; - -/** Owns a Replay built from a GameReplay and feeds its containers through it. */ -class ReplayHarness -{ -public: - explicit ReplayHarness(GameReplay replay) - : replayProto_(std::move(replay)), view_(std::make_unique()), - replay_(std::make_unique(nullptr, &replayProto_, false)) - { - QObject::connect(replay_->getPlayerManager(), &PlayerManager::playerAdded, view_.get(), - &HeadlessZoneView::attach); - } - - ReplayHarness(const ReplayHarness &) = delete; - ReplayHarness &operator=(const ReplayHarness &) = delete; - - void processAll() - { - for (const GameEventContainer &container : replayProto_.event_list()) { - replay_->getGameEventHandler()->processGameEventContainer(container, nullptr, EventProcessingOptions()); - } - } - - [[nodiscard]] Replay *replay() const - { - return replay_.get(); - } - - [[nodiscard]] PlayerLogic *player(int playerId) const - { - return replay_->getPlayerManager()->getPlayer(playerId); - } - -private: - GameReplay replayProto_; - std::unique_ptr view_; - std::unique_ptr replay_; -}; - -GameReplay makeReplay(const QList &containers) -{ - GameReplay replay; - replay.mutable_game_info()->set_game_id(42); - replay.mutable_game_info()->set_started(true); - for (const auto &container : containers) { - replay.add_event_list()->CopyFrom(container); - } - return replay; -} - -Event_GameStateChanged initialGameState() -{ - Event_GameStateChanged state; - state.set_game_started(true); - state.set_active_player_id(1); - state.set_active_phase(0); - return state; -} - -} // namespace - -TEST(ReplayTest, MaterializesGameStateZonesAndAttachments) -{ - Event_GameStateChanged state = initialGameState(); - - ServerInfo_Card cat = makeCard(7, QStringLiteral("Cat")); - // Cross-player attachment: Alice's card is attached to Bob's creature. - cat.set_attach_player_id(2); - cat.set_attach_zone(ZoneNames::TABLE); - cat.set_attach_card_id(9); - - const ServerInfo_Player alice = makePlayer(1, QStringLiteral("Alice"), - {makeZone(ZoneNames::HAND, ServerInfo_Zone::PrivateZone, false, {cat}), - makeZone(ZoneNames::TABLE, ServerInfo_Zone::PublicZone, true, - {makeCard(9, QStringLiteral("Grizzly Bears"), 2, 1)}), - makeZone(ZoneNames::DECK, ServerInfo_Zone::HiddenZone, false, {}, 2)}); - const ServerInfo_Player bob = makePlayer(2, QStringLiteral("Bob"), - {makeZone(ZoneNames::TABLE, ServerInfo_Zone::PublicZone, true, - {makeCard(9, QStringLiteral("Grizzly Bears"), 1, 0)})}); - - state.add_player_list()->CopyFrom(alice); - state.add_player_list()->CopyFrom(bob); - - ReplayHarness harness(makeReplay({makeContainer(-1, state)})); - harness.processAll(); - - PlayerLogic *aliceLogic = harness.player(1); - PlayerLogic *bobLogic = harness.player(2); - ASSERT_NE(aliceLogic, nullptr); - ASSERT_NE(bobLogic, nullptr); - - CardZoneLogic *hand = aliceLogic->getZones().value(ZoneNames::HAND); - CardZoneLogic *table = aliceLogic->getZones().value(ZoneNames::TABLE); - CardZoneLogic *deck = aliceLogic->getZones().value(ZoneNames::DECK); - ASSERT_NE(hand, nullptr); - ASSERT_NE(table, nullptr); - ASSERT_NE(deck, nullptr); - - ASSERT_EQ(hand->getCards().size(), 1); - CardState *catState = hand->getCards().first(); - EXPECT_EQ(catState->getId(), 7); - EXPECT_EQ(catState->getName(), QStringLiteral("Cat")); - - ASSERT_EQ(table->getCards().size(), 1); - CardState *host = table->getCards().first(); - EXPECT_EQ(host->getName(), QStringLiteral("Grizzly Bears")); - EXPECT_EQ(host->getGridPoint(), QPoint(2, 1)); - - // The hidden deck only carries a count, and the view materializes that many - // unknown cards. - EXPECT_EQ(deck->getCards().size(), 2); - for (const CardState *hidden : deck->getCards()) { - EXPECT_EQ(hidden->getId(), -1); - } - - // processCardAttachment links both sides after every player is materialized. - CardZoneLogic *bobTable = bobLogic->getZones().value(ZoneNames::TABLE); - ASSERT_NE(bobTable, nullptr); - ASSERT_EQ(bobTable->getCards().size(), 1); - CardState *bobHost = bobTable->getCards().first(); - EXPECT_EQ(catState->getAttachedTo(), bobHost); - ASSERT_EQ(bobHost->getAttachedCards().size(), 1); - EXPECT_EQ(bobHost->getAttachedCards().first(), catState); - // The identically numbered card on Alice's own table is a different card. - EXPECT_TRUE(host->getAttachedCards().isEmpty()); - - EXPECT_TRUE(harness.replay()->getGameState()->isGameStateKnown()); -} - -TEST(ReplayTest, AppliesMovesAttributesAndCountersInOrder) -{ - Event_GameStateChanged state = initialGameState(); - state.add_player_list()->CopyFrom(makePlayer( - 1, QStringLiteral("Alice"), - {makeZone(ZoneNames::HAND, ServerInfo_Zone::PrivateZone, false, {makeCard(7, QStringLiteral("Cat"))}), - makeZone(ZoneNames::TABLE, ServerInfo_Zone::PublicZone, true, - {makeCard(9, QStringLiteral("Grizzly Bears"), 0, TableZoneLogic::tableRowToGridY(0))}), - makeZone(ZoneNames::DECK, ServerInfo_Zone::HiddenZone, false, {}, 2)})); - - Event_MoveCard move; - move.set_card_id(7); - move.set_start_player_id(1); - move.set_start_zone(ZoneNames::HAND); - move.set_position(0); - move.set_target_player_id(1); - move.set_target_zone(ZoneNames::TABLE); - move.set_x(3); - move.set_y(TableZoneLogic::tableRowToGridY(1)); - move.set_new_card_id(7); - move.set_face_down(false); - - Event_SetCardAttr tap; - tap.set_zone_name(ZoneNames::TABLE); - tap.set_card_id(7); - tap.set_attribute(AttrTapped); - tap.set_attr_value("1"); - - Event_SetCardCounter counter; - counter.set_zone_name(ZoneNames::TABLE); - counter.set_card_id(7); - counter.set_counter_id(1); - counter.set_counter_value(2); - - ReplayHarness harness(makeReplay( - {makeContainer(-1, state), makeContainer(1, move), makeContainer(1, tap), makeContainer(1, counter)})); - harness.processAll(); - - PlayerLogic *alice = harness.player(1); - ASSERT_NE(alice, nullptr); - CardZoneLogic *hand = alice->getZones().value(ZoneNames::HAND); - CardZoneLogic *table = alice->getZones().value(ZoneNames::TABLE); - ASSERT_NE(hand, nullptr); - ASSERT_NE(table, nullptr); - - EXPECT_TRUE(hand->getCards().isEmpty()); - ASSERT_EQ(table->getCards().size(), 2); - - CardState *moved = table->getCard(7); - ASSERT_NE(moved, nullptr); - EXPECT_EQ(moved->getGridPoint(), QPoint(3, TableZoneLogic::tableRowToGridY(1))); - EXPECT_EQ(moved->getPT(), QStringLiteral("3/3")); - EXPECT_TRUE(moved->getTapped()); - EXPECT_EQ(moved->getCounters().value(1), 2); - - // The untouched creature keeps its own position and state. - CardState *bears = table->getCard(9); - ASSERT_NE(bears, nullptr); - EXPECT_EQ(bears->getGridPoint(), QPoint(0, TableZoneLogic::tableRowToGridY(0))); - EXPECT_FALSE(bears->getTapped()); -} diff --git a/tests/holder_line_test.cpp b/tests/holder_line_test.cpp deleted file mode 100644 index 8588d8515..000000000 --- a/tests/holder_line_test.cpp +++ /dev/null @@ -1,102 +0,0 @@ -#include "holder_line.h" - -#include -#include - -namespace -{ -Holder runningProcess(const std::wstring &imageName, unsigned long processId) -{ - Holder holder; - holder.imageName = imageName; - holder.processId = processId; - return holder; -} - -Holder service(const std::wstring &appName, const std::wstring &shortName, unsigned long processId) -{ - Holder holder; - holder.appName = appName; - holder.serviceShortName = shortName; - holder.processId = processId; - holder.isService = true; - return holder; -} -} // namespace - -TEST(HolderLine, NamesARunningProcessTheWayTaskManagerDoes) -{ - EXPECT_EQ(formatHolder(runningProcess(L"cockatrice.exe", 24340)), - std::wstring(L"cockatrice.exe (pid 24340, running)")); -} - -TEST(HolderLine, NamesAServiceByTheNameTheUserCanSearchFor) -{ - // The image of a service is often a shared host process, so the image name would - // point at the wrong thing; both of the service's own names are kept. - Holder holder = service(L"Sophos Anti-Virus", L"SAVService", 4412); - holder.imageName = L"svchost.exe"; - EXPECT_EQ(formatHolder(holder), std::wstring(L"Sophos Anti-Virus (pid 4412, service SAVService)")); -} - -TEST(HolderLine, FallsBackFromTheImageToTheApplicationName) -{ - // Restart Manager can name a process whose image this one is not allowed to open. - Holder holder; - holder.appName = L"Cockatrice"; - holder.processId = 7; - EXPECT_EQ(formatHolder(holder), std::wstring(L"Cockatrice (pid 7, running)")); -} - -TEST(HolderLine, NamesAnEmptyHolderRatherThanPrintingAnEmptyParenthesis) -{ - Holder holder; - holder.processId = 9; - EXPECT_EQ(formatHolder(holder), std::wstring(L"an unnamed process (pid 9, running)")); -} - -TEST(HolderLine, JoinsHoldersIntoOneLine) -{ - const std::vector holders = { - runningProcess(L"cockatrice.exe", 1), - runningProcess(L"oracle.exe", 2), - }; - EXPECT_EQ(formatHolders(holders), std::wstring(L"cockatrice.exe (pid 1, running); oracle.exe (pid 2, running)")); -} - -TEST(HolderLine, CountsTheHoldersItLeavesOut) -{ - std::vector holders; - for (unsigned long processId = 1; processId <= HOLDER_LINE_MAX + 2; ++processId) { - holders.push_back(runningProcess(L"cockatrice.exe", processId)); - } - const std::wstring line = formatHolders(holders); - // The dialog has to stay readable, but a list that silently stopped at five would - // hide that there is more to close. - EXPECT_NE(line.find(L"pid 5, running"), std::wstring::npos); - EXPECT_EQ(line.find(L"pid 6, running"), std::wstring::npos); - EXPECT_NE(line.find(L"and 2 more"), std::wstring::npos); -} - -TEST(HolderLine, StopsNamingHoldersBeforeTheLineOutgrowsTheDialog) -{ - // A Windows service name can be 255 characters, and the abort dialog has to fit the - // list next to its own text and the log path in a 1024-character NSIS string. - std::vector holders; - for (unsigned long processId = 1; processId <= 3; ++processId) { - holders.push_back(service(std::wstring(255, L'S'), L"S", processId)); - } - const std::wstring line = formatHolders(holders); - // The first holder is still named in full; the others are counted rather than written - // out until the line overflows. - EXPECT_EQ(line.compare(0, 255, std::wstring(255, L'S')), 0); - EXPECT_NE(line.find(L"(pid 1, service S)"), std::wstring::npos); - EXPECT_EQ(line.find(L"pid 2"), std::wstring::npos); - EXPECT_NE(line.find(L"and 2 more"), std::wstring::npos); - EXPECT_LE(line.size(), HOLDER_LINE_CHAR_MAX + 20); -} - -TEST(HolderLine, FormatsNothingAsAnEmptyLine) -{ - EXPECT_EQ(formatHolders({}), std::wstring()); -} 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/local_client_lifetime_test.cpp b/tests/local_client_lifetime_test.cpp deleted file mode 100644 index 95a2b1380..000000000 --- a/tests/local_client_lifetime_test.cpp +++ /dev/null @@ -1,149 +0,0 @@ -/** - * @file local_client_lifetime_test.cpp - * @brief Tests that a LocalClient survives the death of the LocalServer it talks to. - * @ingroup Tests - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// The server library refers to this global; the real applications define it in main.cpp. -RNG_Abstract *rng = nullptr; - -namespace -{ - -// GTest's main() creates no QCoreApplication, which QObject::deleteLater() needs. -const QCoreApplication &application() -{ - static int argc = 1; - static char executable[] = "local_client_lifetime_test"; - static char *argv[] = {executable, nullptr}; - static const QCoreApplication app(argc, argv); - return app; -} - -/// Collects the warnings one logging category emits while this object is alive. -/// -/// The handler is process-global and Qt does not nest handlers, so only one capture can be -/// installed at a time; GTest running one case at a time is what makes that safe. -class CategoryWarningCapture -{ -public: - explicit CategoryWarningCapture(const char *_category) : category(_category) - { - previousHandler = qInstallMessageHandler(captureMessage); - s_active = this; - } - - ~CategoryWarningCapture() - { - qInstallMessageHandler(previousHandler); - s_active = nullptr; - } - - CategoryWarningCapture(const CategoryWarningCapture &) = delete; - CategoryWarningCapture &operator=(const CategoryWarningCapture &) = delete; - - const QStringList &warnings() const - { - return capturedWarnings; - } - -private: - static void captureMessage(QtMsgType type, const QMessageLogContext &context, const QString &message) - { - if (s_active != nullptr && type == QtWarningMsg && context.category != nullptr && - std::strcmp(s_active->category, context.category) == 0) { - s_active->capturedWarnings.append(message); - } - - // Forward everything else, so a failure elsewhere in the test is still visible. - if (previousHandler != nullptr) { - previousHandler(type, context, message); - return; - } - std::fprintf(stderr, "%s\n", qPrintable(qFormatLogMessage(type, context, message))); - } - - const char *category; - QStringList capturedWarnings; - - static CategoryWarningCapture *s_active; - static QtMessageHandler previousHandler; -}; - -CategoryWarningCapture *CategoryWarningCapture::s_active = nullptr; -QtMessageHandler CategoryWarningCapture::previousHandler = nullptr; - -class LocalClientLifetimeTest : public ::testing::Test -{ -protected: - void SetUp() override - { - (void)application(); - - server = std::make_unique(); - LocalServerInterface *lsi = server->newConnection(); - ASSERT_NE(lsi, nullptr); - // Building the client logs in and joins room 0 through that interface, exactly as - // MainWindow::startLocalGame() does. - client = std::make_unique(lsi, QStringLiteral("lifetime-test-player"), - QStringLiteral("lifetime-test-client-id")); - } - - void TearDown() override - { - // Either case may already have deleted one of these. - client.reset(); - server.reset(); - } - - std::unique_ptr server; - std::unique_ptr client; -}; - -TEST_F(LocalClientLifetimeTest, SendAfterServerDeletedIsDropped) -{ - server.reset(); // also destroys the LocalServerInterface the client was built on - - CategoryWarningCapture capture("local_client"); - // A default container stands in for a command still queued when the local game ended. - client->sendCommandContainer(CommandContainer()); - - ASSERT_EQ(capture.warnings().size(), 1); - EXPECT_TRUE(capture.warnings().first().contains(QStringLiteral("dropping command"))) - << capture.warnings().first().toStdString(); - - // The surviving client is intact: reading its state must not touch the deleted interface. - EXPECT_EQ(client->getUserName(), QStringLiteral("lifetime-test-player")); -} - -TEST_F(LocalClientLifetimeTest, DestroyingClientAfterServerIsSafe) -{ - server.reset(); - - CategoryWarningCapture capture("local_client"); - client.reset(); // must neither crash nor touch the destroyed interface - - // The destructor itself must have been silent. - EXPECT_TRUE(capture.warnings().isEmpty()); - - // A working capture proves the emptiness above is real rather than a broken handler. - qCWarning(LocalClientLog) << "local_client_lifetime_test capture self-check"; - EXPECT_EQ(capture.warnings().size(), 1); -} - -} // namespace diff --git a/tests/model_contract/CMakeLists.txt b/tests/model_contract/CMakeLists.txt deleted file mode 100644 index 3251c1443..000000000 --- a/tests/model_contract/CMakeLists.txt +++ /dev/null @@ -1,17 +0,0 @@ -cockatrice_add_test( - NAME - model_contract_test - SOURCES - ${VERSION_STRING_CPP} - model_contract_test.cpp - LIBS - libcockatrice_models - libcockatrice_card - libcockatrice_deck_list - libcockatrice_settings - INCLUDE_DIRS - ${CMAKE_CURRENT_SOURCE_DIR}/../support - ${CMAKE_CURRENT_SOURCE_DIR}/../carddatabase - DEFINITIONS - CARDDB_DATADIR="${CMAKE_SOURCE_DIR}/tests/carddatabase/data/" -) diff --git a/tests/model_contract/model_contract_test.cpp b/tests/model_contract/model_contract_test.cpp deleted file mode 100644 index aefa26f3d..000000000 --- a/tests/model_contract/model_contract_test.cpp +++ /dev/null @@ -1,137 +0,0 @@ -#include "libcockatrice/deck_list/deck_list.h" -#include "libcockatrice/deck_list/tree/inner_deck_list_node.h" -#include "libcockatrice/models/database/card_database_display_model.h" -#include "libcockatrice/models/database/card_database_model.h" -#include "libcockatrice/models/database/card_set/card_sets_model.h" -#include "libcockatrice/models/deck_list/deck_list_model.h" -#include "model_contract_checker.h" -#include "test_card_database_path_provider.h" - -#include "gtest/gtest.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ -QSharedPointer makeDeck() -{ - return QSharedPointer(new DeckList()); -} - -void loadCardDatabase(CardDatabase &database) -{ - database.loadCardDatabases(); - ASSERT_EQ(database.getLoadStatus(), Ok); -} - -TEST(ModelContract, DeckListModelKeepsContractWhileZonesAndCardsChange) -{ - const QSharedPointer deck = makeDeck(); - DeckListModel model(nullptr, deck); - - test_support::ModelContractChecker checker(&model); - - ASSERT_NE(deck->getTree()->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr); - model.rebuildTree(); - - CardInfoPtr bolt = CardInfo::newInstance("Lightning Bolt"); - const QModelIndex added = model.addCard(ExactCard(bolt), DECK_ZONE_MAIN); - ASSERT_TRUE(added.isValid()); - - CardInfoPtr bears = CardInfo::newInstance("Grizzly Bears"); - bears->setProperty(Mtg::MainCardType, "Creature"); - const QModelIndex grouped = model.addCard(ExactCard(bears), "Removal"); - ASSERT_TRUE(grouped.isValid()); - - ASSERT_TRUE(model.removeRows(grouped.row(), 1, grouped.parent())); -} - -TEST(ModelContract, DeckListModelEmitsCardAddedSignal) -{ - const QSharedPointer deck = makeDeck(); - DeckListModel model(nullptr, deck); - QSignalSpy spy(&model, &DeckListModel::cardAddedAt); - - const QModelIndex added = model.addCard(ExactCard(CardInfo::newInstance("Lightning Bolt")), DECK_ZONE_MAIN); - - ASSERT_TRUE(added.isValid()); - EXPECT_EQ(spy.count(), 1); -} - -TEST(ModelContract, CardDatabaseModelAndDisplayProxyKeepContract) -{ - CardDatabase database(nullptr, new NoopCardPreferenceProvider(), new TestCardDatabasePathProvider(), - new NoopCardSetPriorityController()); - ASSERT_NO_FATAL_FAILURE(loadCardDatabase(database)); - - CardDatabaseModel source(&database, false); - test_support::ModelContractChecker sourceChecker(&source); - - CardDatabaseDisplayModel display; - display.setSourceModel(&source); - test_support::ModelContractChecker displayChecker(&display); - - ASSERT_GT(source.rowCount(), 0); - EXPECT_EQ(display.rowCount(), source.rowCount()); - - // A card row is a leaf, so its index must not claim child columns. The - // tester does not check this, hence the explicit assertion. - const QModelIndex sourceCard = source.index(0, 0); - ASSERT_TRUE(sourceCard.isValid()); - EXPECT_EQ(source.columnCount(sourceCard), 0); - const QModelIndex displayCard = display.index(0, 0); - ASSERT_TRUE(displayCard.isValid()); - EXPECT_EQ(display.columnCount(displayCard), 0); - - QSignalSpy dirtySpy(&display, &CardDatabaseDisplayModel::modelDirty); - display.setCardName("Do"); - ASSERT_TRUE(dirtySpy.wait()); - EXPECT_GT(display.rowCount(), 0); - EXPECT_LT(display.rowCount(), source.rowCount()); -} - -TEST(ModelContract, SetsModelKeepsContract) -{ - CardDatabase database(nullptr, new NoopCardPreferenceProvider(), new TestCardDatabasePathProvider(), - new NoopCardSetPriorityController()); - ASSERT_NO_FATAL_FAILURE(loadCardDatabase(database)); - - SetsModel model(&database); - test_support::ModelContractChecker checker(&model); - - ASSERT_GT(model.rowCount(), 0); - ASSERT_GT(model.columnCount(), 0); -} - -TEST(ModelContract, SettingsSignalIsObservableWithQSignalSpy) -{ - QTemporaryDir tempDir; - ASSERT_TRUE(tempDir.isValid()); - DownloadSettings settings(tempDir.path() + "/", nullptr); - QSignalSpy spy(&settings, &DownloadSettings::downloadSpoilerStatusChanged); - - settings.setDownloadSpoilerStatus(true); - - ASSERT_EQ(spy.count(), 1); -} -} // namespace - -int main(int argc, char **argv) -{ - QCoreApplication app(argc, argv); - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/tests/password_hash_test.cpp b/tests/password_hash_test.cpp index 8e7eded72..99f2e1400 100644 --- a/tests/password_hash_test.cpp +++ b/tests/password_hash_test.cpp @@ -52,17 +52,6 @@ TEST(PasswordHashTest, DeriveKeyMatchesKnownVector) ASSERT_EQ(derived.toHex(), expected.toHex()); } -TEST(PasswordHashTest, DeriveKeyCoversTheWholeAcceptedRangeOfR) -{ - // costParamsAreSane accepts r up to 32. The memory budget handed to OpenSSL has to cover - // scrypt's XY array (256 * r bytes) on top of V and B, or derivation fails for r >= 17. - for (const int r : {17, 32}) { - const QByteArray derived = PasswordHasher::deriveKey("password", QByteArray("NaCl"), 1024, r, 1); - ASSERT_FALSE(derived.isEmpty()) << "deriveKey failed for accepted r=" << r; - ASSERT_EQ(derived.size(), SCRYPT_VERIFIER_LENGTH); - } -} - TEST(PasswordHashTest, PasswordVerifierRoundTrip) { const QString stored = PasswordHasher::generatePasswordVerifier("hunter2"); 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/picture_loader_benchmark/CMakeLists.txt b/tests/picture_loader_benchmark/CMakeLists.txt index 9b8d15999..0684f58f4 100644 --- a/tests/picture_loader_benchmark/CMakeLists.txt +++ b/tests/picture_loader_benchmark/CMakeLists.txt @@ -15,9 +15,8 @@ cockatrice_add_manual_test( settings_cache_mock.cpp LIBS libcockatrice_card - libcockatrice_interfaces - libcockatrice_network libcockatrice_settings + libcockatrice_interfaces libcockatrice_utility INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/cockatrice/src 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_forced_password_change_test.cpp b/tests/server_forced_password_change_test.cpp deleted file mode 100644 index 9717b610b..000000000 --- a/tests/server_forced_password_change_test.cpp +++ /dev/null @@ -1,234 +0,0 @@ -/** @file server_forced_password_change_test.cpp - * @brief Tests for the restricted session a forced password change leaves behind at login. - * @ingroup Tests - */ - -#include "libcockatrice/protocol/pb/response.pb.h" -#include "libcockatrice/protocol/pb/server_message.pb.h" -#include "server.h" -#include "server_database_interface.h" -#include "server_protocolhandler.h" - -#include -#include -#include -#include -#include -#include -#include - -RNG_Abstract *rng = nullptr; // referenced by the server_remote library - -namespace -{ - -class MockDatabaseInterface : public Server_DatabaseInterface -{ -public: - AuthenticationResult checkUserPassword(Server_ProtocolHandler *, - const QString &, - const QString &, - const QString &, - QString &, - int &, - bool) override - { - return PasswordChangeRequired; - } - ServerInfo_User getUserData(const QString &name, bool withId) override - { - ServerInfo_User user; - user.set_name(name.toStdString()); - user.set_user_level(ServerInfo_User::IsUser | ServerInfo_User::IsRegistered | ServerInfo_User::IsModerator); - if (withId) { - user.set_id(1); - } - return user; - } - int getNextReplayId() override - { - return 1; - } - int getNextGameId() override - { - return 1; - } - int getActiveUserCount(QString) override - { - return 0; - } -}; - -class FakeServer : public Server -{ -public: - FakeServer() - { - setDatabaseInterface(new MockDatabaseInterface()); - } -}; - -class TestHandler : public Server_ProtocolHandler -{ -public: - TestHandler(Server *_server, Server_DatabaseInterface *_db) : Server_ProtocolHandler(_server, _db) - { - } - - QString getAddress() const override - { - return {}; - } - QString getConnectionType() const override - { - return {}; - } - - Response::ResponseCode lastResponseCode = Response::RespNothing; - int lastExtendedSessionCommand = -1; - -protected: - Response::ResponseCode - processExtendedSessionCommand(int cmdType, const SessionCommand & /*cmd*/, ResponseContainer & /*rc*/) override - { - lastExtendedSessionCommand = cmdType; - return Response::RespOk; - } - - void transmitProtocolItem(const ServerMessage &item) override - { - if (item.message_type() == ServerMessage::RESPONSE) { - lastResponseCode = item.response().response_code(); - } - } -}; - -class ForcedPasswordChangeTest : public ::testing::Test -{ -protected: - FakeServer server; - TestHandler handler{&server, server.getDatabaseInterface()}; - - void login() - { - CommandContainer cont; - cont.set_cmd_id(1); - Command_Login cmd; - cmd.set_user_name("mod"); - cont.add_session_command()->MutableExtension(Command_Login::ext)->CopyFrom(cmd); - handler.processCommandContainer(cont); - } - - Response::ResponseCode sendPing() - { - CommandContainer cont; - cont.set_cmd_id(2); - cont.add_session_command()->MutableExtension(Command_Ping::ext); - handler.processCommandContainer(cont); - return handler.lastResponseCode; - } - - Response::ResponseCode sendSessionCommand() - { - CommandContainer cont; - cont.set_cmd_id(3); - cont.add_session_command()->MutableExtension(Command_ListRooms::ext); - handler.processCommandContainer(cont); - return handler.lastResponseCode; - } - - Response::ResponseCode sendRoomCommand() - { - CommandContainer cont; - cont.set_cmd_id(4); - cont.add_room_command()->MutableExtension(Command_RoomSay::ext)->set_message("hello"); - handler.processCommandContainer(cont); - return handler.lastResponseCode; - } - - Response::ResponseCode sendMixedSessionAndRoomCommand() - { - CommandContainer cont; - cont.set_cmd_id(5); - cont.add_session_command()->MutableExtension(Command_Ping::ext); - cont.add_room_command()->MutableExtension(Command_RoomSay::ext)->set_message("hello"); - handler.processCommandContainer(cont); - return handler.lastResponseCode; - } - - Response::ResponseCode sendAccountPassword() - { - CommandContainer cont; - cont.set_cmd_id(6); - Command_AccountPassword cmd; - cmd.set_old_password("temporary-password"); - cmd.set_new_password("replacement-password"); - cont.add_session_command()->MutableExtension(Command_AccountPassword::ext)->CopyFrom(cmd); - handler.processCommandContainer(cont); - return handler.lastResponseCode; - } -}; - -TEST_F(ForcedPasswordChangeTest, LoginReportsTheRequiredChange) -{ - login(); - - EXPECT_EQ(Response::RespPasswordChangeRequired, handler.lastResponseCode); - ASSERT_NE(nullptr, handler.getUserInfo()); - EXPECT_EQ("mod", QString::fromStdString(handler.getUserInfo()->name())); -} - -TEST_F(ForcedPasswordChangeTest, PendingSessionIsNotRegisteredAsAUser) -{ - login(); - // The session may change its password, but it must not take a user slot or appear in - // the session table before the client reconnects with the new password. - EXPECT_FALSE(handler.getUserInfo()->has_session_id()); - EXPECT_EQ(0, server.getUsersCount()); -} - -TEST_F(ForcedPasswordChangeTest, PendingModeratorIsNotReportedOnline) -{ - login(); - server.addClient(&handler); - - EXPECT_TRUE(server.getOnlineModeratorList().isEmpty()); -} - -TEST_F(ForcedPasswordChangeTest, OnlyPingAndPasswordChangeAreServed) -{ - login(); - - EXPECT_EQ(Response::RespOk, sendPing()); - EXPECT_EQ(Response::RespFunctionNotAllowed, sendSessionCommand()); - EXPECT_EQ(Response::RespFunctionNotAllowed, sendRoomCommand()); - // Dispatch prefers game/room over session, so a container carrying both must not slip past - // the restriction by carrying a session command. - EXPECT_EQ(Response::RespFunctionNotAllowed, sendMixedSessionAndRoomCommand()); -} - -TEST_F(ForcedPasswordChangeTest, AccountPasswordReachesTheCommand) -{ - login(); - // The forced change is useless if the gate refuses the command it needs. ACCOUNT_PASSWORD is - // served by the extended-command hook, so it must arrive there instead of being filtered. - EXPECT_EQ(Response::RespOk, sendAccountPassword()); - EXPECT_EQ(SessionCommand::ACCOUNT_PASSWORD, handler.lastExtendedSessionCommand); -} - -TEST_F(ForcedPasswordChangeTest, CompletedLoginIsStillReportedOnline) -{ - TestHandler loggedIn{&server, server.getDatabaseInterface()}; - ServerInfo_User user; - user.set_name("online-mod"); - user.set_user_level(ServerInfo_User::IsUser | ServerInfo_User::IsRegistered | ServerInfo_User::IsModerator); - user.set_session_id(7); - loggedIn.setUserInfo(user); - server.addClient(&loggedIn); - - const QList moderators = server.getOnlineModeratorList(); - ASSERT_EQ(1, moderators.size()); - EXPECT_EQ("online-mod", moderators.first()); -} - -} // namespace diff --git a/tests/server_game_join_test.cpp b/tests/server_game_join_test.cpp index 93895ab5c..8a2ae8c45 100644 --- a/tests/server_game_join_test.cpp +++ b/tests/server_game_join_test.cpp @@ -4,7 +4,6 @@ */ #include "game/game_config.h" -#include "game/role_info.h" #include "game/server_game.h" #include "libcockatrice/protocol/pb/response.pb.h" #include "server.h" @@ -120,13 +119,13 @@ TEST_F(GameJoinOverrideTest, StaffBypassPasswordRestriction) game = makeGame(true, false, false, true); // A plain user cannot override the password even with the override flag set. - EXPECT_EQ(game->checkJoin(&plainUser, "wrong", {}, true), Response::RespWrongPassword); + EXPECT_EQ(game->checkJoin(&plainUser, "wrong", false, true, false), Response::RespWrongPassword); // Moderators and judges may enter any game regardless of the password. - EXPECT_EQ(game->checkJoin(&moderator, "wrong", {}, true), Response::RespOk); - EXPECT_EQ(game->checkJoin(&judge, "wrong", {}, true), Response::RespOk); + EXPECT_EQ(game->checkJoin(&moderator, "wrong", false, true, false), Response::RespOk); + EXPECT_EQ(game->checkJoin(&judge, "wrong", false, true, false), Response::RespOk); // Without the override flag judges are still subject to the password. - EXPECT_EQ(game->checkJoin(&judge, "wrong", {.isJudge = true}, false), Response::RespWrongPassword); - EXPECT_EQ(game->checkJoin(&judge, "secret", {.isJudge = true}, false), Response::RespOk); + EXPECT_EQ(game->checkJoin(&judge, "wrong", false, false, true), Response::RespWrongPassword); + EXPECT_EQ(game->checkJoin(&judge, "secret", false, false, true), Response::RespOk); } TEST_F(GameJoinOverrideTest, StaffBypassRegisteredOnlyRestriction) @@ -134,9 +133,9 @@ TEST_F(GameJoinOverrideTest, StaffBypassRegisteredOnlyRestriction) game = makeGame(false, true, false, true); // Without the override flag the only-registered restriction still applies. - EXPECT_EQ(game->checkJoin(&unregisteredJudge, QString(), {}, false), Response::RespUserLevelTooLow); + EXPECT_EQ(game->checkJoin(&unregisteredJudge, QString(), false, false, false), Response::RespUserLevelTooLow); // An unregistered judge may enter when overriding restrictions. - EXPECT_EQ(game->checkJoin(&unregisteredJudge, QString(), {}, true), Response::RespOk); + EXPECT_EQ(game->checkJoin(&unregisteredJudge, QString(), false, true, false), Response::RespOk); } TEST_F(GameJoinOverrideTest, StaffBypassBuddiesOnlyRestriction) @@ -144,10 +143,10 @@ TEST_F(GameJoinOverrideTest, StaffBypassBuddiesOnlyRestriction) game = makeGame(false, false, true, true); // A plain user who is not on the creator's buddy list gets rejected. - EXPECT_EQ(game->checkJoin(&plainUser, QString(), {}, true), Response::RespOnlyBuddies); + EXPECT_EQ(game->checkJoin(&plainUser, QString(), false, true, false), Response::RespOnlyBuddies); // Moderators and judges bypass the buddies-only restriction. - EXPECT_EQ(game->checkJoin(&moderator, QString(), {}, true), Response::RespOk); - EXPECT_EQ(game->checkJoin(&judge, QString(), {}, true), Response::RespOk); + EXPECT_EQ(game->checkJoin(&moderator, QString(), false, true, false), Response::RespOk); + EXPECT_EQ(game->checkJoin(&judge, QString(), false, true, false), Response::RespOk); } TEST_F(GameJoinOverrideTest, StaffBypassSpectatorsNotAllowedRestriction) @@ -155,11 +154,11 @@ TEST_F(GameJoinOverrideTest, StaffBypassSpectatorsNotAllowedRestriction) game = makeGame(false, false, false, false); // A plain user cannot spectate when the game disallows spectators. - EXPECT_EQ(game->checkJoin(&plainUser, QString(), {.isSpectator = true}, false), Response::RespSpectatorsNotAllowed); + EXPECT_EQ(game->checkJoin(&plainUser, QString(), true, false, false), Response::RespSpectatorsNotAllowed); // Moderators and judges may spectate any game regardless of the password // and the spectator restriction. - EXPECT_EQ(game->checkJoin(&moderator, "wrong", {.isSpectator = true}, true), Response::RespOk); - EXPECT_EQ(game->checkJoin(&judge, "wrong", {.isSpectator = true}, true), Response::RespOk); + EXPECT_EQ(game->checkJoin(&moderator, "wrong", true, true, false), Response::RespOk); + EXPECT_EQ(game->checkJoin(&judge, "wrong", true, true, false), Response::RespOk); } TEST_F(GameJoinOverrideTest, JudgeOverrideDoesNotGrantJudgeJoinToPlainUser) @@ -167,8 +166,8 @@ TEST_F(GameJoinOverrideTest, JudgeOverrideDoesNotGrantJudgeJoinToPlainUser) game = makeGame(false, false, false, true); // joining with join_as_judge still requires the judge flag even when overriding. - EXPECT_EQ(game->checkJoin(&plainUser, QString(), {.isJudge = true}, true), Response::RespUserLevelTooLow); - EXPECT_EQ(game->checkJoin(&judge, QString(), {.isJudge = true}, true), Response::RespOk); + EXPECT_EQ(game->checkJoin(&plainUser, QString(), false, true, true), Response::RespUserLevelTooLow); + EXPECT_EQ(game->checkJoin(&judge, QString(), false, true, true), Response::RespOk); } } // namespace 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/settings/CMakeLists.txt b/tests/settings/CMakeLists.txt index 9dbda9d6b..98bed1f4b 100644 --- a/tests/settings/CMakeLists.txt +++ b/tests/settings/CMakeLists.txt @@ -1,7 +1,5 @@ cockatrice_add_test(NAME settings_manager_test SOURCES settings_manager_test.cpp LIBS libcockatrice_settings) -cockatrice_add_test(NAME servers_settings_test SOURCES servers_settings_test.cpp LIBS libcockatrice_settings) - cockatrice_add_test( NAME settings_defaults_test diff --git a/tests/settings/servers_settings_test.cpp b/tests/settings/servers_settings_test.cpp deleted file mode 100644 index 5d5953999..000000000 --- a/tests/settings/servers_settings_test.cpp +++ /dev/null @@ -1,228 +0,0 @@ -#include "gtest/gtest.h" -#include -#include -#include -#include - -namespace -{ -class ServersSettingsTest : public ::testing::Test -{ -protected: - QTemporaryDir tempDir; - QString settingsPath; - - void SetUp() override - { - settingsPath = tempDir.path() + "/"; - } - - QString settingsFile() const - { - return settingsPath + "servers.ini"; - } - - void writeLegacyServer(int index, - const QString &saveName, - const QString &host, - const QString &port, - const QString &username, - const QString &password, - bool savePassword, - const QString &site) - { - QSettings settings(settingsFile(), QSettings::IniFormat); - const QString prefix = QStringLiteral("server/server_details/"); - settings.setValue(prefix + QStringLiteral("saveName%1").arg(index), saveName); - settings.setValue(prefix + QStringLiteral("server%1").arg(index), host); - settings.setValue(prefix + QStringLiteral("port%1").arg(index), port); - settings.setValue(prefix + QStringLiteral("username%1").arg(index), username); - settings.setValue(prefix + QStringLiteral("password%1").arg(index), password); - settings.setValue(prefix + QStringLiteral("savePassword%1").arg(index), savePassword); - settings.setValue(prefix + QStringLiteral("site%1").arg(index), site); - } -}; - -TEST_F(ServersSettingsTest, AddServerStoresAllFields) -{ - ServersSettings settings(settingsPath, nullptr); - settings.addNewServer("Alpha", "alpha.example", "4748", "alice", "secret", true, "https://alpha.example"); - - const auto servers = settings.getServers(); - ASSERT_EQ(servers.size(), 1); - EXPECT_EQ(servers.first().saveName, QString("Alpha")); - EXPECT_EQ(servers.first().host, QString("alpha.example")); - EXPECT_EQ(servers.first().port, QString("4748")); - EXPECT_EQ(servers.first().username, QString("alice")); - EXPECT_EQ(servers.first().password, QString("secret")); - EXPECT_TRUE(servers.first().savePassword); - EXPECT_EQ(servers.first().site, QString("https://alpha.example")); -} - -TEST_F(ServersSettingsTest, AddExistingHostAndPortUpdatesInsteadOfDuplicating) -{ - ServersSettings settings(settingsPath, nullptr); - settings.addNewServer("Alpha", "alpha.example", "4748", "alice", "secret", true, "site-a"); - settings.addNewServer("Renamed", "alpha.example", "4748", "bob", "new-secret", true, "site-b"); - - const auto servers = settings.getServers(); - ASSERT_EQ(servers.size(), 1); - EXPECT_EQ(servers.first().saveName, QString("Renamed")); - EXPECT_EQ(servers.first().username, QString("bob")); - EXPECT_EQ(servers.first().password, QString("new-secret")); - EXPECT_EQ(servers.first().site, QString("site-b")); -} - -TEST_F(ServersSettingsTest, RemoveServerRemovesOnlyMatchingHostAndKeepsOrder) -{ - ServersSettings settings(settingsPath, nullptr); - settings.addNewServer("Alpha", "alpha.example", "4748", "alice", "a", true, ""); - settings.addNewServer("Beta", "beta.example", "4748", "bob", "b", true, ""); - settings.addNewServer("Gamma", "gamma.example", "4748", "carol", "c", true, ""); - - settings.removeServer("beta.example"); - - const auto servers = settings.getServers(); - ASSERT_EQ(servers.size(), 2); - EXPECT_EQ(servers.at(0).saveName, QString("Alpha")); - EXPECT_EQ(servers.at(1).saveName, QString("Gamma")); -} - -TEST_F(ServersSettingsTest, RemovingLastServerDropsItsStaleArraySlot) -{ - ServersSettings settings(settingsPath, nullptr); - settings.addNewServer("Alpha", "alpha.example", "4748", "alice", "a", true, ""); - settings.addNewServer("Beta", "beta.example", "4748", "bob", "b", true, ""); - settings.addNewServer("Gamma", "gamma.example", "4748", "carol", "gamma-secret", true, ""); - - settings.removeServer("gamma.example"); - - QSettings raw(settingsFile(), QSettings::IniFormat); - EXPECT_FALSE(raw.contains("server/server_details/servers/3/password")); - EXPECT_FALSE(raw.contains("server/server_details/servers/3/saveName")); - ASSERT_EQ(settings.getServers().size(), 2); -} - -TEST_F(ServersSettingsTest, UpdateWithoutLossKeepsCredentials) -{ - ServersSettings settings(settingsPath, nullptr); - settings.addNewServer("Alpha", "alpha.example", "4748", "alice", "secret", true, ""); - - EXPECT_TRUE(settings.updateExistingServerWithoutLoss("Renamed", "alpha.example", "1234", "site")); - - const auto server = settings.getServer(settings.findServerIndex("alpha.example", "1234")); - ASSERT_TRUE(server.has_value()); - EXPECT_EQ(server->saveName, QString("Renamed")); - EXPECT_EQ(server->username, QString("alice")); - EXPECT_EQ(server->password, QString("secret")); - EXPECT_TRUE(server->savePassword); - EXPECT_EQ(server->site, QString("site")); -} - -TEST_F(ServersSettingsTest, SetServerPasswordUpdatesOnlyTheNamedProfile) -{ - ServersSettings settings(settingsPath, nullptr); - settings.addNewServer("Alpha", "alpha.example", "4748", "alice", "old", true, ""); - settings.addNewServer("Beta", "beta.example", "4748", "bob", "keep", true, ""); - - settings.setServerPassword("Beta", "new"); - - EXPECT_EQ(settings.getServer(0)->password, QString("old")); - EXPECT_EQ(settings.getServer(1)->password, QString("new")); -} - -TEST_F(ServersSettingsTest, LegacyProfilesAreMigrated) -{ - { - QSettings settings(settingsFile(), QSettings::IniFormat); - settings.setValue("server/server_details/totalServers", 2); - } - writeLegacyServer(1, "Alpha", "alpha.example", "4748", "alice", "secret", true, "site-a"); - writeLegacyServer(2, "Beta", "beta.example", "4748", "bob", "other", false, "site-b"); - - ServersSettings settings(settingsPath, nullptr); - - const auto servers = settings.getServers(); - ASSERT_EQ(servers.size(), 2); - EXPECT_EQ(servers.at(0).saveName, QString("Alpha")); - EXPECT_EQ(servers.at(0).host, QString("alpha.example")); - EXPECT_TRUE(servers.at(0).savePassword); - EXPECT_EQ(servers.at(1).saveName, QString("Beta")); - EXPECT_FALSE(servers.at(1).savePassword); - - QSettings raw(settingsFile(), QSettings::IniFormat); - EXPECT_FALSE(raw.contains("server/server_details/totalServers")); - EXPECT_FALSE(raw.contains("server/server_details/saveName1")); - EXPECT_FALSE(raw.contains("server/server_details/server1")); -} - -TEST_F(ServersSettingsTest, MigrationRunsOnce) -{ - writeLegacyServer(1, "Alpha", "alpha.example", "4748", "alice", "secret", true, ""); - { - QSettings settings(settingsFile(), QSettings::IniFormat); - settings.setValue("server/server_details/totalServers", 1); - } - - { - ServersSettings first(settingsPath, nullptr); - ASSERT_EQ(first.getServers().size(), 1); - } - - ServersSettings second(settingsPath, nullptr); - ASSERT_EQ(second.getServers().size(), 1); -} - -TEST_F(ServersSettingsTest, LegacyProfileAddedAfterMigrationIsMerged) -{ - { - ServersSettings first(settingsPath, nullptr); - first.addNewServer("Alpha", "alpha.example", "4748", "alice", "secret", true, ""); - } - - // An older build writes its own profile back in the legacy format after the - // array migration already happened. - writeLegacyServer(1, "Beta", "beta.example", "4748", "bob", "other", true, "site-b"); - // A duplicate of a profile the array already holds must not be added twice. - writeLegacyServer(2, "Alpha duplicate", "alpha.example", "4748", "eve", "dupe", true, ""); - { - QSettings settings(settingsFile(), QSettings::IniFormat); - settings.setValue("server/server_details/totalServers", 2); - } - - ServersSettings settings(settingsPath, nullptr); - - const auto servers = settings.getServers(); - ASSERT_EQ(servers.size(), 2); - EXPECT_EQ(servers.at(0).saveName, QString("Alpha")); - EXPECT_EQ(servers.at(0).password, QString("secret")); - EXPECT_EQ(servers.at(1).saveName, QString("Beta")); - EXPECT_EQ(servers.at(1).host, QString("beta.example")); - EXPECT_EQ(servers.at(1).username, QString("bob")); - EXPECT_EQ(servers.at(1).password, QString("other")); - EXPECT_TRUE(servers.at(1).savePassword); - EXPECT_EQ(servers.at(1).site, QString("site-b")); - - QSettings raw(settingsFile(), QSettings::IniFormat); - EXPECT_FALSE(raw.contains("server/server_details/totalServers")); - EXPECT_FALSE(raw.contains("server/server_details/saveName1")); - EXPECT_FALSE(raw.contains("server/server_details/server2")); - - // The merge is persisted, so a further start adds nothing. - ServersSettings again(settingsPath, nullptr); - ASSERT_EQ(again.getServers().size(), 2); -} - -TEST_F(ServersSettingsTest, OutOfRangeIndexReturnsNullopt) -{ - ServersSettings settings(settingsPath, nullptr); - EXPECT_FALSE(settings.getServer(-1).has_value()); - EXPECT_FALSE(settings.getServer(0).has_value()); -} -} // namespace - -int main(int argc, char **argv) -{ - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/tests/support/model_contract_checker.h b/tests/support/model_contract_checker.h deleted file mode 100644 index 9b493ff6e..000000000 --- a/tests/support/model_contract_checker.h +++ /dev/null @@ -1,35 +0,0 @@ -#ifndef COCKATRICE_TEST_MODEL_CONTRACT_CHECKER_H -#define COCKATRICE_TEST_MODEL_CONTRACT_CHECKER_H - -#include -#include -#include - -namespace test_support -{ -/** - * @brief Keeps a QAbstractItemModelTester alive for the lifetime of a scope. - * - * The tester only observes the model while it exists, so construct this before - * the mutations a test wants to validate and keep it in scope until the end. - * Fatal reporting mode aborts on the first contract violation; a warning would - * let the test pass while printing a line nobody reads in CI. - */ -class ModelContractChecker -{ -public: - explicit ModelContractChecker(QAbstractItemModel *model) - : tester( - std::make_unique(model, QAbstractItemModelTester::FailureReportingMode::Fatal)) - { - } - - ModelContractChecker(const ModelContractChecker &) = delete; - ModelContractChecker &operator=(const ModelContractChecker &) = delete; - -private: - std::unique_ptr tester; -}; -} // namespace test_support - -#endif // COCKATRICE_TEST_MODEL_CONTRACT_CHECKER_H 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 deleted file mode 100644 index 99e7d5045..000000000 --- a/tools/installer-holder-probe/CMakeLists.txt +++ /dev/null @@ -1,36 +0,0 @@ -# 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 -) - -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( - installer-holder-probe - PROPERTIES MSVC_RUNTIME_LIBRARY "MultiThreaded" - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" - RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}" - RUNTIME_OUTPUT_DIRECTORY_MINSIZEREL "${CMAKE_BINARY_DIR}" - RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}" - RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_BINARY_DIR}" -) 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.cpp b/tools/installer-holder-probe/holder_line.cpp deleted file mode 100644 index 8d438ca17..000000000 --- a/tools/installer-holder-probe/holder_line.cpp +++ /dev/null @@ -1,64 +0,0 @@ -#include "holder_line.h" - -#include - -namespace -{ -std::wstring displayNameOf(const Holder &holder) -{ - // A service's application name is its long name, which is what the user can - // search for; its image name is usually a shared host process (svchost.exe) and - // would name the wrong thing. - if (holder.isService && !holder.appName.empty()) { - return holder.appName; - } - if (!holder.imageName.empty()) { - return holder.imageName; - } - if (!holder.appName.empty()) { - return holder.appName; - } - return L"an unnamed process"; -} -} // namespace - -std::wstring formatHolder(const Holder &holder) -{ - std::wstring line = displayNameOf(holder); - line += L" (pid "; - line += std::to_wstring(holder.processId); - if (holder.isService) { - line += L", service "; - line += holder.serviceShortName.empty() ? L"without a short name" : holder.serviceShortName; - } else { - line += L", running"; - } - line += L")"; - return line; -} - -std::wstring formatHolders(const std::vector &holders) -{ - const std::size_t wanted = std::min(holders.size(), HOLDER_LINE_MAX); - std::wstring line; - std::size_t listed = 0; - for (; listed < wanted; ++listed) { - const std::wstring entry = formatHolder(holders[listed]); - const std::size_t separator = line.empty() ? 0 : 2; - // The first holder is always named, however long its name: it is the answer the - // dialog exists to show, and Windows bounds one entry below the whole budget. - if (!line.empty() && line.size() + separator + entry.size() > HOLDER_LINE_CHAR_MAX) { - break; - } - if (!line.empty()) { - line += L"; "; - } - line += entry; - } - if (holders.size() > listed) { - line += L"; and "; - line += std::to_wstring(holders.size() - listed); - line += L" more"; - } - return line; -} diff --git a/tools/installer-holder-probe/holder_line.h b/tools/installer-holder-probe/holder_line.h deleted file mode 100644 index 675d79fc6..000000000 --- a/tools/installer-holder-probe/holder_line.h +++ /dev/null @@ -1,62 +0,0 @@ -#ifndef HOLDER_LINE_H -#define HOLDER_LINE_H - -#include -#include -#include - -/** - * @brief One application or service that has an installer file open or mapped. - * - * Deliberately free of Windows types: the wording is what the abort dialog shows, - * and it is the only part of the probe that can be exercised off Windows. - */ -struct Holder -{ - std::wstring imageName; ///< "cockatrice.exe", empty when the image could not be read - std::wstring appName; ///< Restart Manager's name for the application or service - std::wstring serviceShortName; ///< service short name, empty when this is not a service - unsigned long processId = 0; - bool isService = false; -}; - -/// How many holders the single-line summary names before it says how many were left out. -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. - */ -inline constexpr std::size_t HOLDER_LINE_CHAR_MAX = 320; - -/** - * @brief Renders one holder the way the user has to recognise it, e.g. - * "cockatrice.exe (pid 24340, running)". - * - * The image name is preferred over the application name because it is what Task - * Manager shows, and the application name is what remains for a process whose - * image cannot be opened (another account, a protected process). A service is - * named by Restart Manager's long name and identified by its short name, because - * both are how the user finds it again in services.msc. - */ -std::wstring formatHolder(const Holder &holder); - -/** - * @brief Renders every holder as a single "; "-joined line. - * - * One line because it is written to the installer log as one and repeated in a - * message box, where nsExec hands the text back as one string. Only the first - * HOLDER_LINE_MAX are named, and only while they fit in HOLDER_LINE_CHAR_MAX - * characters; the rest are counted, so a machine with a dozen open handles cannot - * push the real answer off the dialog. - */ -std::wstring formatHolders(const std::vector &holders); - -#endif 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 deleted file mode 100644 index 31901a0ec..000000000 --- a/tools/installer-holder-probe/main.cpp +++ /dev/null @@ -1,300 +0,0 @@ -#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 - -namespace -{ -/// The installer sets this, the same variable the running-instance guard uses, instead -/// of passing the directory on the command line: a path with spaces, quotes or a -/// 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 -/// too, so a name can never forge a second log line. -void writeLine(const std::wstring &text) -{ - std::string narrow; - narrow.reserve(text.size() + 2); - for (const wchar_t character : text) { - narrow.push_back(character >= 0x20 && character < 0x7F ? static_cast(character) : '?'); - } - narrow += "\r\n"; - DWORD written = 0; - 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) -{ - 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 {}; -} - -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) -{ - const std::wstring installDir = installDirFromEnvironment(); - if (installDir.empty()) { - writeLine(L"the probe was not given the install directory"); - 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); - } - - // 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"); - } - 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; -} 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 deleted file mode 100644 index 21ddd6988..000000000 --- a/tools/installer-holder-probe/restart_manager_holders.cpp +++ /dev/null @@ -1,305 +0,0 @@ -#include "restart_manager_holders.h" - -// clang-format off -// restartmanager.h uses the types from windows.h without including it, so the order -// here is a requirement; Regroup would sort them the other way round. -#include -#include -#include -// clang-format on - -#include -#include -#include - -namespace -{ -/// Stop registering after this many files. The install directory carries around 150 -/// files in practice; the cap only exists so a directory someone has filled with files -/// cannot turn the abort path into a long Restart Manager call. -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) { - return; - } - WIN32_FIND_DATAW entry{}; - const HANDLE find = FindFirstFileW((directory + L"\\*").c_str(), &entry); - if (find == INVALID_HANDLE_VALUE) { - return; - } - do { - if (files.size() >= MAX_PROBED_FILES) { - break; - } - const std::wstring name = entry.cFileName; - if (name == L"." || name == L".." || (entry.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { - continue; - } - const std::wstring path = directory + L"\\" + name; - if ((entry.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) { - if (depth < maxDepth) { - collectFilesOf(path, depth + 1, maxDepth, files); - } - } else { - files.push_back(path); - } - } while (FindNextFileW(find, &entry) != 0); - FindClose(find); -} - -std::wstring imagePathOf(unsigned long processId) -{ - // PROCESS_QUERY_LIMITED_INFORMATION is the right to ask, not to debug: it reads the - // path of a process this one does not own, which is what keeps a SYSTEM-owned holder - // from turning the whole list into an error. One that refuses even this is still - // named, by whatever Restart Manager called it. - const HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, processId); - if (process == nullptr) { - return {}; - } - wchar_t path[MAX_PATH * 2] = {}; - DWORD length = static_cast(std::size(path)); - std::wstring imagePath; - if (QueryFullProcessImageNameW(process, 0, path, &length) != 0) { - imagePath.assign(path, length); - } - CloseHandle(process); - 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 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; -} - -/// Copies a fixed-size, NUL-terminated WCHAR array the API filled in. -template std::wstring fixedString(const wchar_t (&text)[N]) -{ - return std::wstring(text, std::find(text, text + N, L'\0')); -} - -Holder holderOf(const RM_PROCESS_INFO &info) -{ - Holder holder; - holder.processId = info.Process.dwProcessId; - holder.isService = info.ApplicationType == RmService; - holder.appName = fixedString(info.strAppName); - if (holder.isService) { - holder.serviceShortName = fixedString(info.strServiceShortName); - } - holder.imageName = fileNameOf(imagePathOf(holder.processId)); - return holder; -} -} // namespace - -std::vector collectPayloadFiles(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. - if (files.size() < MAX_PROBED_FILES) { - collectFilesOf(installDir + L"\\Plugins", 0, MAX_PLUGIN_DEPTH, files); - } - return files; -} - -unsigned long findProcessesUsingFiles(const std::vector &files, std::vector &holders) -{ - holders.clear(); - if (files.empty()) { - return ERROR_SUCCESS; - } - - DWORD session = 0; - wchar_t sessionKey[CCH_RM_SESSION_KEY + 1] = {}; - DWORD error = RmStartSession(&session, 0, sessionKey); - if (error != ERROR_SUCCESS) { - return error; - } - - // One call for the whole set, as the API asks: RmRegisterResources does registry - // work per file and is documented as too expensive to call once per file. - std::vector names; - names.reserve(files.size()); - for (const std::wstring &file : files) { - names.push_back(file.c_str()); - } - error = RmRegisterResources(session, static_cast(names.size()), names.data(), 0, nullptr, 0, nullptr); - - if (error == ERROR_SUCCESS) { - UINT needed = 0; - UINT count = 0; - DWORD rebootReasons = 0; - // The first call asks how large the answer is; Restart Manager reports - // ERROR_MORE_DATA rather than truncating a list that does not fit. - error = RmGetList(session, &needed, &count, nullptr, &rebootReasons); - // Bounded: each retry more than covers the list growing between the two calls, - // and an abort path cannot chase a process that keeps opening files. Giving up - // reports the code, which is a truthful answer instead of a truncated list. - for (int attempt = 0; attempt < 3 && error == ERROR_MORE_DATA; ++attempt) { - count = needed; - std::vector infos(count); - error = RmGetList(session, &needed, &count, infos.data(), &rebootReasons); - if (error == ERROR_SUCCESS) { - holders.reserve(count); - for (UINT index = 0; index < count; ++index) { - holders.push_back(holderOf(infos[index])); - } - } - } - } - - 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 deleted file mode 100644 index e966b70b8..000000000 --- a/tools/installer-holder-probe/restart_manager_holders.h +++ /dev/null @@ -1,88 +0,0 @@ -#ifndef RESTART_MANAGER_HOLDERS_H -#define RESTART_MANAGER_HOLDERS_H - -#include "holder_line.h" -#include "payload_paths.h" - -#include -#include - -/** - * @brief Lists the payload files, 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. - * - * 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. - */ -std::vector collectPayloadFiles(const std::wstring &installDir); - -/** - * @brief Names the applications and services that have one of @p files open or mapped. - * - * Restart Manager reports both a process that loaded one of the files as a module and - * one that merely holds a handle to it, and it reports services, which is the whole - * reason it is used instead of walking every process's module list. - * - * @param holders replaced with one entry per holder, in the order Windows reported them. - * @return 0 when the lookup ran, even if nothing is holding the files; otherwise the - * Win32 code of the Restart Manager call that failed. - */ -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 8c9c64161..f12227024 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",