Merge branch 'master' into Circuitous-drop-11-22.04

This commit is contained in:
SlightlyCircuitous 2026-04-30 17:16:55 -04:00 committed by GitHub
commit 2b7eea6f38
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
97 changed files with 27251 additions and 21430 deletions

View file

@ -1,4 +1,4 @@
FROM fedora:42 FROM fedora:44
RUN dnf install -y \ RUN dnf install -y \
ccache \ ccache \

View file

@ -21,8 +21,8 @@ Available pre-compiled binaries for installation:
<kbd>Ubuntu 24.04 LTS</kbd> <sub><i>Noble Numbat</i></sub> <kbd>Ubuntu 24.04 LTS</kbd> <sub><i>Noble Numbat</i></sub>
<kbd>Debian 13</kbd> <sub><i>Trixie</i></sub> <kbd>Debian 13</kbd> <sub><i>Trixie</i></sub>
<kbd>Debian 12</kbd> <sub><i>Bookworm</i></sub> <kbd>Debian 12</kbd> <sub><i>Bookworm</i></sub>
<kbd>Fedora 44</kbd>
<kbd>Fedora 43</kbd> <kbd>Fedora 43</kbd>
<kbd>Fedora 42</kbd>
<sub>We are also packaged in <kbd>Arch Linux</kbd>'s <a href="https://archlinux.org/packages/extra/x86_64/cockatrice">official extra repository</a>, courtesy of @FFY00.</sub> <sub>We are also packaged in <kbd>Arch Linux</kbd>'s <a href="https://archlinux.org/packages/extra/x86_64/cockatrice">official extra repository</a>, courtesy of @FFY00.</sub>
<sub>General Linux support is available via a <kbd>flatpak</kbd> package at <a href="https://flathub.org/apps/io.github.Cockatrice.cockatrice">Flathub</a>!</sub> <sub>General Linux support is available via a <kbd>flatpak</kbd> package at <a href="https://flathub.org/apps/io.github.Cockatrice.cockatrice">Flathub</a>!</sub>

View file

@ -46,7 +46,7 @@ concurrency:
jobs: jobs:
configure: configure:
name: Configure name: Configure
runs-on: ubuntu-latest runs-on: ubuntu-slim
outputs: outputs:
tag: ${{steps.configure.outputs.tag}} tag: ${{steps.configure.outputs.tag}}
sha: ${{steps.configure.outputs.sha}} sha: ${{steps.configure.outputs.sha}}
@ -127,13 +127,14 @@ jobs:
package: DEB package: DEB
- distro: Fedora - distro: Fedora
version: 42 version: 43
package: RPM package: RPM
test: skip # Running tests on all distros is superfluous test: skip # Running tests on all distros is superfluous
- distro: Fedora - distro: Fedora
version: 43 version: 44
package: RPM package: RPM
test: skip # This is failing to build, currently. To be fixed.
- distro: Ubuntu - distro: Ubuntu
version: 24.04 version: 24.04

View file

@ -20,7 +20,7 @@ on:
jobs: jobs:
format: format:
runs-on: ubuntu-22.04 runs-on: ubuntu-slim
steps: steps:
- name: Checkout - name: Checkout

View file

@ -1,9 +1,10 @@
name: Build Docker Image name: Build Docker Image
on: on:
release:
types:
- released # publishing of stable releases
push: push:
tags:
- '*Release*'
branches: branches:
- master - master
pull_request: pull_request:
@ -16,12 +17,14 @@ on:
# Cancel earlier, unfinished runs of this workflow on the same branch (unless on release) # Cancel earlier, unfinished runs of this workflow on the same branch (unless on release)
concurrency: concurrency:
group: "${{ github.workflow }} @ ${{ github.ref_name }}" group: "${{ github.workflow }} @ ${{ github.ref_name }}"
cancel-in-progress: ${{ github.ref_type != 'tag' }} cancel-in-progress: ${{ github.event_name != 'release' }}
jobs: jobs:
docker: docker:
name: amd64 & arm64 name: amd64 & arm64
if: ${{ github.repository_owner == 'Cockatrice' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
packages: write packages: write
@ -52,7 +55,7 @@ jobs:
uses: docker/setup-buildx-action@v4 uses: docker/setup-buildx-action@v4
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
if: github.ref_type == 'tag' if: contains(github.event.release.tag_name, 'Release') && github.event.release.target_commitish == 'master'
uses: docker/login-action@v4 uses: docker/login-action@v4
with: with:
registry: ghcr.io registry: ghcr.io

View file

@ -33,7 +33,7 @@ jobs:
- name: Install Doxygen - name: Install Doxygen
uses: ssciwr/doxygen-install@v2 uses: ssciwr/doxygen-install@v2
with: with:
version: "1.14.0" version: "1.16.1"
- name: Update Doxygen Configuration - name: Update Doxygen Configuration
run: | run: |

View file

@ -16,7 +16,7 @@ jobs:
if: github.event_name != 'schedule' || github.repository_owner == 'Cockatrice' if: github.event_name != 'schedule' || github.repository_owner == 'Cockatrice'
name: Pull languages name: Pull languages
runs-on: ubuntu-latest runs-on: ubuntu-slim
steps: steps:
- name: Checkout repo - name: Checkout repo

View file

@ -16,7 +16,7 @@ jobs:
if: github.event_name != 'schedule' || github.repository_owner == 'Cockatrice' if: github.event_name != 'schedule' || github.repository_owner == 'Cockatrice'
name: Push strings name: Push strings
runs-on: ubuntu-latest runs-on: ubuntu-slim
steps: steps:
- name: Checkout repo - name: Checkout repo
@ -46,7 +46,7 @@ jobs:
- name: Render template - name: Render template
id: template id: template
uses: chuhlomin/render-template@v1 uses: chuhlomin/render-template/binary@v1
with: with:
template: .ci/update_translation_source_strings_template.md template: .ci/update_translation_source_strings_template.md
vars: | vars: |

View file

@ -10,7 +10,7 @@ on:
jobs: jobs:
ESLint: ESLint:
runs-on: ubuntu-latest runs-on: ubuntu-slim
defaults: defaults:
run: run:

View file

@ -74,11 +74,11 @@ endif()
# A project name is needed for CPack # A project name is needed for CPack
# Version can be overriden by git tags, see cmake/getversion.cmake # Version can be overriden by git tags, see cmake/getversion.cmake
project("Cockatrice" VERSION 2.11.0) project("Cockatrice" VERSION 3.0.0)
# Set release name if not provided via env/cmake var # Set release name if not provided via env/cmake var
if(NOT DEFINED GIT_TAG_RELEASENAME) if(NOT DEFINED GIT_TAG_RELEASENAME)
set(GIT_TAG_RELEASENAME "Omenpath") set(GIT_TAG_RELEASENAME "Graduation Day")
endif() endif()
# Use c++20 for all targets # Use c++20 for all targets

View file

@ -1,4 +1,4 @@
# Doxyfile 1.14.0 # Doxyfile 1.16.1
# This file describes the settings to be used by the documentation system # This file describes the settings to be used by the documentation system
# Doxygen (www.doxygen.org) for a project. # Doxygen (www.doxygen.org) for a project.
@ -361,6 +361,20 @@ EXTENSION_MAPPING =
MARKDOWN_SUPPORT = YES MARKDOWN_SUPPORT = YES
# If the MARKDOWN_STRICT tag is enabled then Doxygen treats text in comments as
# Markdown formatted also in cases where Doxygen's native markup format
# conflicts with that of Markdown. This is only relevant in cases where
# backticks are used. Doxygen's native markup style allows a single quote to end
# a text fragment started with a backtick and then treat it as a piece of quoted
# text, whereas in Markdown such text fragment is treated as verbatim and only
# ends when a second matching backtick is found. Also, Doxygen's native markup
# format requires double quotes to be escaped when they appear in a backtick
# section, whereas this is not needed for Markdown.
# The default value is: YES.
# This tag requires that the tag MARKDOWN_SUPPORT is set to YES.
MARKDOWN_STRICT = YES
# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up # When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up
# to that level are automatically included in the table of contents, even if # to that level are automatically included in the table of contents, even if
# they do not have an id attribute. # they do not have an id attribute.
@ -392,8 +406,8 @@ AUTOLINK_SUPPORT = YES
# This tag specifies a list of words that, when matching the start of a word in # This tag specifies a list of words that, when matching the start of a word in
# the documentation, will suppress auto links generation, if it is enabled via # the documentation, will suppress auto links generation, if it is enabled via
# AUTOLINK_SUPPORT. This list does not affect links explicitly created using \# # AUTOLINK_SUPPORT. This list does not affect links explicitly created using #
# or the \link or commands. # or the \link or \ref commands.
# This tag requires that the tag AUTOLINK_SUPPORT is set to YES. # This tag requires that the tag AUTOLINK_SUPPORT is set to YES.
AUTOLINK_IGNORE_WORDS = AUTOLINK_IGNORE_WORDS =
@ -510,9 +524,9 @@ LOOKUP_CACHE_SIZE = 0
# which effectively disables parallel processing. Please report any issues you # which effectively disables parallel processing. Please report any issues you
# encounter. Generating dot graphs in parallel is controlled by the # encounter. Generating dot graphs in parallel is controlled by the
# DOT_NUM_THREADS setting. # DOT_NUM_THREADS setting.
# Minimum value: 0, maximum value: 32, default value: 1. # Minimum value: 0, maximum value: 512, default value: 1.
NUM_PROC_THREADS = 1 NUM_PROC_THREADS = 0
# If the TIMESTAMP tag is set different from NO then each generated page will # If the TIMESTAMP tag is set different from NO then each generated page will
# contain the date or date and time when the page was generated. Setting this to # contain the date or date and time when the page was generated. Setting this to
@ -779,6 +793,27 @@ GENERATE_BUGLIST = YES
GENERATE_DEPRECATEDLIST= YES GENERATE_DEPRECATEDLIST= YES
# The GENERATE_REQUIREMENTS tag can be used to enable (YES) or disable (NO) the
# requirements page. When enabled, this page is automatically created when at
# least one comment block with a \requirement command appears in the input.
# The default value is: YES.
GENERATE_REQUIREMENTS = YES
# The REQ_TRACEABILITY_INFO tag controls if traceability information is shown on
# the requirements page (only relevant when using \requirement comment blocks).
# The setting NO will disable the traceablility information altogether. The
# setting UNSATISFIED_ONLY will show a list of requirements that are missing a
# satisfies relation (through the command: \satisfies). Similarly the setting
# UNVERIFIED_ONLY will show a list of requirements that are missing a verifies
# relation (through the command: \verifies). Setting the tag to YES (the
# default) will show both lists if applicable.
# Possible values are: YES, NO, UNSATISFIED_ONLY and UNVERIFIED_ONLY.
# The default value is: YES.
# This tag requires that the tag GENERATE_REQUIREMENTS is set to YES.
REQ_TRACEABILITY_INFO = YES
# The ENABLED_SECTIONS tag can be used to enable conditional documentation # The ENABLED_SECTIONS tag can be used to enable conditional documentation
# sections, marked by \if <section_label> ... \endif and \cond <section_label> # sections, marked by \if <section_label> ... \endif and \cond <section_label>
# ... \endcond blocks. # ... \endcond blocks.
@ -1887,7 +1922,7 @@ USE_MATHJAX = NO
# regards to the different settings, so it is possible that also other MathJax # regards to the different settings, so it is possible that also other MathJax
# settings have to be changed when switching between the different MathJax # settings have to be changed when switching between the different MathJax
# versions. # versions.
# Possible values are: MathJax_2 and MathJax_3. # Possible values are: MathJax_2, MathJax_3 and MathJax_4.
# The default value is: MathJax_2. # The default value is: MathJax_2.
# This tag requires that the tag USE_MATHJAX is set to YES. # This tag requires that the tag USE_MATHJAX is set to YES.
@ -1896,9 +1931,10 @@ MATHJAX_VERSION = MathJax_2
# When MathJax is enabled you can set the default output format to be used for # When MathJax is enabled you can set the default output format to be used for
# the MathJax output. For more details about the output format see MathJax # the MathJax output. For more details about the output format see MathJax
# version 2 (see: # version 2 (see:
# http://docs.mathjax.org/en/v2.7-latest/output.html) and MathJax version 3 # https://docs.mathjax.org/en/v2.7/output.html), MathJax version 3 (see:
# https://docs.mathjax.org/en/v3.2/output/index.html) and MathJax version 4
# (see: # (see:
# http://docs.mathjax.org/en/latest/web/components/output.html). # https://docs.mathjax.org/en/v4.0/output/index.htm).
# Possible values are: HTML-CSS (which is slower, but has the best # Possible values are: HTML-CSS (which is slower, but has the best
# compatibility. This is the name for Mathjax version 2, for MathJax version 3 # compatibility. This is the name for Mathjax version 2, for MathJax version 3
# this will be translated into chtml), NativeMML (i.e. MathML. Only supported # this will be translated into chtml), NativeMML (i.e. MathML. Only supported
@ -1911,36 +1947,50 @@ MATHJAX_VERSION = MathJax_2
MATHJAX_FORMAT = HTML-CSS MATHJAX_FORMAT = HTML-CSS
# When MathJax is enabled you need to specify the location relative to the HTML # When MathJax is enabled you need to specify the location relative to the HTML
# output directory using the MATHJAX_RELPATH option. The destination directory # output directory using the MATHJAX_RELPATH option. For Mathjax version 2 the
# should contain the MathJax.js script. For instance, if the mathjax directory # destination directory should contain the MathJax.js script. For instance, if
# is located at the same level as the HTML output directory, then # the mathjax directory is located at the same level as the HTML output
# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax # directory, then MATHJAX_RELPATH should be ../mathjax.s For Mathjax versions 3
# Content Delivery Network so you can quickly see the result without installing # and 4 the destination directory should contain the tex-<format>.js script
# MathJax. However, it is strongly recommended to install a local copy of # (where <format> is either chtml or svg). The default value points to the
# MathJax from https://www.mathjax.org before deployment. The default value is: # MathJax Content Delivery Network so you can quickly see the result without
# installing MathJax. However, it is strongly recommended to install a local
# copy of MathJax from https://www.mathjax.org before deployment. The default
# value is:
# - in case of MathJax version 2: https://cdn.jsdelivr.net/npm/mathjax@2 # - in case of MathJax version 2: https://cdn.jsdelivr.net/npm/mathjax@2
# - in case of MathJax version 3: https://cdn.jsdelivr.net/npm/mathjax@3 # - in case of MathJax version 3: https://cdn.jsdelivr.net/npm/mathjax@3
# - in case of MathJax version 4: https://cdn.jsdelivr.net/npm/mathjax@4
# This tag requires that the tag USE_MATHJAX is set to YES. # This tag requires that the tag USE_MATHJAX is set to YES.
MATHJAX_RELPATH = MATHJAX_RELPATH =
# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax # The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax
# extension names that should be enabled during MathJax rendering. For example # extension names that should be enabled during MathJax rendering. For example
# for MathJax version 2 (see # for MathJax version 2 (see https://docs.mathjax.org/en/v2.7/tex.html):
# https://docs.mathjax.org/en/v2.7-latest/tex.html#tex-and-latex-extensions):
# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols # MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols
# For example for MathJax version 3 (see # For example for MathJax version 3 (see
# http://docs.mathjax.org/en/latest/input/tex/extensions/index.html): # https://docs.mathjax.org/en/v3.2/input/tex/extensions/):
# MATHJAX_EXTENSIONS = ams # MATHJAX_EXTENSIONS = ams
# For example for MathJax version 4 (see
# https://docs.mathjax.org/en/v4.0/input/tex/extensions/):
# MATHJAX_EXTENSIONS = units
# Note that for Mathjax version 4 quite a few extensions are already
# automatically loaded. To disable a package in Mathjax version 4 one can use
# the package name prepended with a minus sign (- like MATHJAX_EXTENSIONS +=
# -textmacros)
# This tag requires that the tag USE_MATHJAX is set to YES. # This tag requires that the tag USE_MATHJAX is set to YES.
MATHJAX_EXTENSIONS = MATHJAX_EXTENSIONS =
# The MATHJAX_CODEFILE tag can be used to specify a file with JavaScript pieces # The MATHJAX_CODEFILE tag can be used to specify a file with JavaScript pieces
# of code that will be used on startup of the MathJax code. See the MathJax site # of code that will be used on startup of the MathJax code. See the Mathjax site
# (see: # for more details:
# http://docs.mathjax.org/en/v2.7-latest/output.html) for more details. For an # - MathJax version 2 (see:
# example see the documentation. # https://docs.mathjax.org/en/v2.7/)
# - MathJax version 3 (see:
# https://docs.mathjax.org/en/v3.2/)
# - MathJax version 4 (see:
# https://docs.mathjax.org/en/v4.0/) For an example see the documentation.
# This tag requires that the tag USE_MATHJAX is set to YES. # This tag requires that the tag USE_MATHJAX is set to YES.
MATHJAX_CODEFILE = MATHJAX_CODEFILE =
@ -2601,7 +2651,7 @@ HAVE_DOT = YES
# processors available in the system. You can set it explicitly to a value # processors available in the system. You can set it explicitly to a value
# larger than 0 to get control over the balance between CPU load and processing # larger than 0 to get control over the balance between CPU load and processing
# speed. # speed.
# Minimum value: 0, maximum value: 32, default value: 0. # Minimum value: 0, maximum value: 512, default value: 0.
# This tag requires that the tag HAVE_DOT is set to YES. # This tag requires that the tag HAVE_DOT is set to YES.
DOT_NUM_THREADS = 0 DOT_NUM_THREADS = 0

View file

@ -11,6 +11,7 @@ SetCompressor LZMA
Var NormalDestDir Var NormalDestDir
Var PortableDestDir Var PortableDestDir
Var PortableMode Var PortableMode
Var ReinstallMode
!include LogicLib.nsh !include LogicLib.nsh
!include FileFunc.nsh !include FileFunc.nsh
@ -28,13 +29,23 @@ Var PortableMode
!define MUI_FINISHPAGE_RUN_TEXT "Run 'Cockatrice' now" !define MUI_FINISHPAGE_RUN_TEXT "Run 'Cockatrice' now"
!define MUI_ICON "${NSIS_SOURCE_PATH}\cockatrice\resources\appicon.ico" !define MUI_ICON "${NSIS_SOURCE_PATH}\cockatrice\resources\appicon.ico"
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfReinstall
!insertmacro MUI_PAGE_WELCOME !insertmacro MUI_PAGE_WELCOME
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfReinstall
!insertmacro MUI_PAGE_LICENSE "${NSIS_SOURCE_PATH}\LICENSE" !insertmacro MUI_PAGE_LICENSE "${NSIS_SOURCE_PATH}\LICENSE"
Page Custom PortableModePageCreate PortableModePageLeave Page Custom PortableModePageCreate PortableModePageLeave
!define MUI_PAGE_CUSTOMFUNCTION_PRE componentsPagePre !define MUI_PAGE_CUSTOMFUNCTION_PRE componentsPagePre
!insertmacro MUI_PAGE_COMPONENTS !insertmacro MUI_PAGE_COMPONENTS
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfReinstall
!insertmacro MUI_PAGE_DIRECTORY !insertmacro MUI_PAGE_DIRECTORY
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfReinstall
!insertmacro MUI_PAGE_INSTFILES !insertmacro MUI_PAGE_INSTFILES
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfReinstall
!insertmacro MUI_PAGE_FINISH !insertmacro MUI_PAGE_FINISH
!insertmacro MUI_UNPAGE_CONFIRM !insertmacro MUI_UNPAGE_CONFIRM
@ -73,6 +84,7 @@ ${IfNot} ${Errors}
MessageBox MB_ICONINFORMATION|MB_SETFOREGROUND "\ MessageBox MB_ICONINFORMATION|MB_SETFOREGROUND "\
/PORTABLE : Install in portable mode$\n\ /PORTABLE : Install in portable mode$\n\
/S : Silent install$\n\ /S : Silent install$\n\
/R : Silent upgrade$\n\
/D=%directory% : Specify destination directory$\n" /D=%directory% : Specify destination directory$\n"
Quit Quit
${EndIf} ${EndIf}
@ -90,6 +102,16 @@ ${Else}
${EndIf} ${EndIf}
${EndIf} ${EndIf}
ClearErrors
${GetOptions} $9 "/R" $8
${IfNot} ${Errors}
StrCpy $ReinstallMode 1
SetSilent silent
SetAutoClose true
${Else}
StrCpy $ReinstallMode 0
${EndIf}
${If} $InstDir == "" ${If} $InstDir == ""
; User did not use /D to specify a directory, ; User did not use /D to specify a directory,
; we need to set a default based on the install mode ; we need to set a default based on the install mode
@ -97,6 +119,22 @@ ${If} $InstDir == ""
${EndIf} ${EndIf}
Call SetModeDestinationFromInstdir Call SetModeDestinationFromInstdir
; --- Detect portable install when using /R ---
${If} $ReinstallMode = 1
IfFileExists "$InstDir\portable.dat" 0 not_portable
StrCpy $PortableMode 1
Goto portable_done
not_portable:
StrCpy $PortableMode 0
portable_done:
${EndIf}
${If} $ReinstallMode = 1
Call AutoUninstallIfNeeded
${EndIf}
FunctionEnd FunctionEnd
Function un.onInit Function un.onInit
@ -126,8 +164,46 @@ ${Else}
${EndIf} ${EndIf}
FunctionEnd FunctionEnd
Function SkipIfReinstall
${If} $ReinstallMode = 1
Abort
${EndIf}
FunctionEnd
Function AutoUninstallIfNeeded
SetShellVarContext all
; --- 32-bit uninstall ---
SetRegView 32
ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "QuietUninstallString"
StrCmp $R0 "" done32
DetailPrint "Removing previous version (32-bit)..."
ExecWait '$R0'
done32:
; --- 64-bit uninstall ---
${If} ${RunningX64}
SetRegView 64
ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "QuietUninstallString"
StrCmp $R0 "" done64
DetailPrint "Removing previous version (64-bit)..."
ExecWait '$R0'
done64:
${EndIf}
FunctionEnd
Function PortableModePageCreate Function PortableModePageCreate
${If} $ReinstallMode = 1
Abort
${EndIf}
Call SetModeDestinationFromInstdir ; If the user clicks BACK on the directory page we will remember their mode specific directory Call SetModeDestinationFromInstdir ; If the user clicks BACK on the directory page we will remember their mode specific directory
!insertmacro MUI_HEADER_TEXT "Install Mode" "Choose how you want to install Cockatrice." !insertmacro MUI_HEADER_TEXT "Install Mode" "Choose how you want to install Cockatrice."
nsDialogs::Create 1018 nsDialogs::Create 1018
@ -159,6 +235,11 @@ ${EndIf}
FunctionEnd FunctionEnd
Function componentsPagePre Function componentsPagePre
${If} $ReinstallMode = 1
Return
${EndIf}
${If} $PortableMode = 0 ${If} $PortableMode = 0
SetShellVarContext all SetShellVarContext all
@ -168,8 +249,12 @@ ${If} $PortableMode = 0
ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "UninstallString" ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "UninstallString"
StrCmp $R0 "" done32 StrCmp $R0 "" done32
${If} $ReinstallMode = 0
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "A previous version of Cockatrice must be uninstalled before installing the new one." IDOK uninst32 MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "A previous version of Cockatrice must be uninstalled before installing the new one." IDOK uninst32
Abort Abort
${Else}
Goto uninst32
${EndIf}
uninst32: uninst32:
ClearErrors ClearErrors
@ -184,8 +269,12 @@ ${If} $PortableMode = 0
ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "UninstallString" ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "UninstallString"
StrCmp $R0 "" done64 StrCmp $R0 "" done64
${If} $ReinstallMode = 0
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "A previous version of Cockatrice must be uninstalled before installing the new one." IDOK uninst64 MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "A previous version of Cockatrice must be uninstalled before installing the new one." IDOK uninst64
Abort Abort
${Else}
Goto uninst64
${EndIf}
uninst64: uninst64:
ClearErrors ClearErrors
@ -277,6 +366,12 @@ ${Else}
FileWrite $0 "PORTABLE" FileWrite $0 "PORTABLE"
FileClose $0 FileClose $0
${EndIf} ${EndIf}
${If} $ReinstallMode = 1
IfFileExists "$INSTDIR\cockatrice.exe" 0 +2
Exec '"$INSTDIR\cockatrice.exe"'
${EndIf}
SectionEnd SectionEnd
Section "Start menu item" SecStartMenu Section "Start menu item" SecStartMenu

View file

@ -89,6 +89,8 @@ void TappedOutInterface::analyzeDeck(const DeckList &deck)
QNetworkRequest request(QUrl("https://tappedout.net/mtg-decks/paste/")); QNetworkRequest request(QUrl("https://tappedout.net/mtg-decks/paste/"));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING)); request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
// we interpret the redirect and open it in the browser instead, do not follow redirects
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy);
manager->post(request, data); manager->post(request, data);
} }

View file

@ -664,6 +664,9 @@ private:
{"Player/aRollDie", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Roll Dice..."), {"Player/aRollDie", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Roll Dice..."),
parseSequenceString("Ctrl+I"), parseSequenceString("Ctrl+I"),
ShortcutGroup::Gameplay)}, ShortcutGroup::Gameplay)},
{"Player/aFlipCoin", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Flip Coin"),
parseSequenceString("Ctrl+Shift+I"),
ShortcutGroup::Gameplay)},
{"Player/aShuffle", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Shuffle Library"), {"Player/aShuffle", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Shuffle Library"),
parseSequenceString("Ctrl+S"), parseSequenceString("Ctrl+S"),
ShortcutGroup::Gameplay)}, ShortcutGroup::Gameplay)},

View file

@ -460,9 +460,6 @@ void CardItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
bool CardItem::animationEvent() bool CardItem::animationEvent()
{ {
if (owner == nullptr) {
return false;
}
int rotation = ROTATION_DEGREES_PER_FRAME; int rotation = ROTATION_DEGREES_PER_FRAME;
bool animationIncomplete = true; bool animationIncomplete = true;
if (!tapped) if (!tapped)

View file

@ -429,13 +429,13 @@ void GameEventHandler::eventLeave(const Event_Leave &event, int eventPlayerId, c
if (!player) if (!player)
return; return;
player->clear();
emit playerLeft(eventPlayerId); emit playerLeft(eventPlayerId);
emit logLeave(player, getLeaveReason(event.reason())); emit logLeave(player, getLeaveReason(event.reason()));
game->getPlayerManager()->removePlayer(eventPlayerId); game->getPlayerManager()->removePlayer(eventPlayerId);
player->clear();
player->deleteLater(); player->deleteLater();
// Rearrange all remaining zones so that attachment relationship updates take place // Rearrange all remaining zones so that attachment relationship updates take place

View file

@ -54,7 +54,7 @@ MessageLogWidget::getFromStr(CardZoneLogic *zone, QString cardName, int position
fromStr = tr(" from the top of their library"); fromStr = tr(" from the top of their library");
} }
} }
} else if (position >= zone->getCards().size() - 1) { } else if (position == zone->getCards().size()) {
if (cardName.isEmpty()) { if (cardName.isEmpty()) {
if (ownerChange) { if (ownerChange) {
cardName = tr("the bottom card of %1's library").arg(zone->getPlayer()->getPlayerInfo()->getName()); cardName = tr("the bottom card of %1's library").arg(zone->getPlayer()->getPlayerInfo()->getName());

View file

@ -19,6 +19,9 @@ UtilityMenu::UtilityMenu(Player *_player, QMenu *playerMenu) : QMenu(playerMenu)
aRollDie = new QAction(this); aRollDie = new QAction(this);
connect(aRollDie, &QAction::triggered, playerActions, &PlayerActions::actRollDie); connect(aRollDie, &QAction::triggered, playerActions, &PlayerActions::actRollDie);
aFlipCoin = new QAction(this);
connect(aFlipCoin, &QAction::triggered, playerActions, &PlayerActions::actFlipCoin);
aCreateToken = new QAction(this); aCreateToken = new QAction(this);
connect(aCreateToken, &QAction::triggered, playerActions, &PlayerActions::actCreateToken); connect(aCreateToken, &QAction::triggered, playerActions, &PlayerActions::actCreateToken);
@ -38,6 +41,7 @@ UtilityMenu::UtilityMenu(Player *_player, QMenu *playerMenu) : QMenu(playerMenu)
playerMenu->addAction(aUntapAll); playerMenu->addAction(aUntapAll);
playerMenu->addSeparator(); playerMenu->addSeparator();
playerMenu->addAction(aRollDie); playerMenu->addAction(aRollDie);
playerMenu->addAction(aFlipCoin);
playerMenu->addSeparator(); playerMenu->addSeparator();
playerMenu->addAction(aCreateToken); playerMenu->addAction(aCreateToken);
playerMenu->addAction(aCreateAnotherToken); playerMenu->addAction(aCreateAnotherToken);
@ -50,6 +54,7 @@ UtilityMenu::UtilityMenu(Player *_player, QMenu *playerMenu) : QMenu(playerMenu)
aIncrementAllCardCounters = nullptr; aIncrementAllCardCounters = nullptr;
aUntapAll = nullptr; aUntapAll = nullptr;
aRollDie = nullptr; aRollDie = nullptr;
aFlipCoin = nullptr;
} }
retranslateUi(); retranslateUi();
@ -89,6 +94,7 @@ void UtilityMenu::retranslateUi()
aIncrementAllCardCounters->setText(tr("Increment all card counters")); aIncrementAllCardCounters->setText(tr("Increment all card counters"));
aUntapAll->setText(tr("&Untap all permanents")); aUntapAll->setText(tr("&Untap all permanents"));
aRollDie->setText(tr("R&oll die...")); aRollDie->setText(tr("R&oll die..."));
aFlipCoin->setText(tr("Flip coin"));
aCreateToken->setText(tr("&Create token...")); aCreateToken->setText(tr("&Create token..."));
aCreateAnotherToken->setText(tr("C&reate another token")); aCreateAnotherToken->setText(tr("C&reate another token"));
createPredefinedTokenMenu->setTitle(tr("Cr&eate predefined token")); createPredefinedTokenMenu->setTitle(tr("Cr&eate predefined token"));
@ -103,6 +109,7 @@ void UtilityMenu::setShortcutsActive()
aIncrementAllCardCounters->setShortcuts(shortcuts.getShortcut("Player/aIncrementAllCardCounters")); aIncrementAllCardCounters->setShortcuts(shortcuts.getShortcut("Player/aIncrementAllCardCounters"));
aUntapAll->setShortcuts(shortcuts.getShortcut("Player/aUntapAll")); aUntapAll->setShortcuts(shortcuts.getShortcut("Player/aUntapAll"));
aRollDie->setShortcuts(shortcuts.getShortcut("Player/aRollDie")); aRollDie->setShortcuts(shortcuts.getShortcut("Player/aRollDie"));
aFlipCoin->setShortcuts(shortcuts.getShortcut("Player/aFlipCoin"));
aCreateToken->setShortcuts(shortcuts.getShortcut("Player/aCreateToken")); aCreateToken->setShortcuts(shortcuts.getShortcut("Player/aCreateToken"));
aCreateAnotherToken->setShortcuts(shortcuts.getShortcut("Player/aCreateAnotherToken")); aCreateAnotherToken->setShortcuts(shortcuts.getShortcut("Player/aCreateAnotherToken"));
} }
@ -113,6 +120,7 @@ void UtilityMenu::setShortcutsInactive()
if (player->getPlayerInfo()->getLocalOrJudge()) { if (player->getPlayerInfo()->getLocalOrJudge()) {
aUntapAll->setShortcut(QKeySequence()); aUntapAll->setShortcut(QKeySequence());
aRollDie->setShortcut(QKeySequence()); aRollDie->setShortcut(QKeySequence());
aFlipCoin->setShortcut(QKeySequence());
aCreateToken->setShortcut(QKeySequence()); aCreateToken->setShortcut(QKeySequence());
aCreateAnotherToken->setShortcut(QKeySequence()); aCreateAnotherToken->setShortcut(QKeySequence());
aIncrementAllCardCounters->setShortcut(QKeySequence()); aIncrementAllCardCounters->setShortcut(QKeySequence());

View file

@ -47,7 +47,7 @@ private:
QMenu *createPredefinedTokenMenu; QMenu *createPredefinedTokenMenu;
QAction *aIncrementAllCardCounters; QAction *aIncrementAllCardCounters;
QAction *aUntapAll, *aRollDie; QAction *aUntapAll, *aRollDie, *aFlipCoin;
QAction *aCreateToken, *aCreateAnotherToken; QAction *aCreateToken, *aCreateAnotherToken;
}; };

View file

@ -852,6 +852,14 @@ void PlayerActions::actRollDie()
sendGameCommand(cmd); sendGameCommand(cmd);
} }
void PlayerActions::actFlipCoin()
{
Command_RollDie cmd;
cmd.set_sides(2);
cmd.set_count(1);
sendGameCommand(cmd);
}
void PlayerActions::actCreateToken() void PlayerActions::actCreateToken()
{ {
DlgCreateToken dlg(player->getPlayerMenu()->getUtilityMenu()->getPredefinedTokens(), player->getGame()->getTab()); DlgCreateToken dlg(player->getPlayerMenu()->getUtilityMenu()->getPredefinedTokens(), player->getGame()->getTab());

View file

@ -77,6 +77,7 @@ public slots:
void actUntapAll(); void actUntapAll();
void actRollDie(); void actRollDie();
void actFlipCoin();
void actCreateToken(); void actCreateToken();
void actCreateAnotherToken(); void actCreateAnotherToken();
void actShuffle(); void actShuffle();

View file

@ -343,11 +343,9 @@ void CardInfoPictureWidget::mousePressEvent(QMouseEvent *event)
QWidget::mousePressEvent(event); QWidget::mousePressEvent(event);
if (event->button() == Qt::RightButton) { if (event->button() == Qt::RightButton) {
createRightClickMenu()->popup(QCursor::pos()); createRightClickMenu()->popup(QCursor::pos());
} else {
emit cardClicked();
} }
emit cardClicked(); emit cardClicked(event);
} }
void CardInfoPictureWidget::hideEvent(QHideEvent *event) void CardInfoPictureWidget::hideEvent(QHideEvent *event)

View file

@ -43,7 +43,7 @@ signals:
void hoveredOnCard(const ExactCard &hoveredCard); void hoveredOnCard(const ExactCard &hoveredCard);
void cardScaleFactorChanged(int _scale); void cardScaleFactorChanged(int _scale);
void cardChanged(const ExactCard &card); void cardChanged(const ExactCard &card);
void cardClicked(); void cardClicked(QMouseEvent *event);
protected: protected:
void resizeEvent(QResizeEvent *event) override; void resizeEvent(QResizeEvent *event) override;

View file

@ -62,7 +62,7 @@ void CardInfoTextWidget::setCard(const ExactCard &exactCard)
text += QString("<tr><td>%1</td><td width=\"5\"></td><td>%2</td></tr>") text += QString("<tr><td>%1</td><td width=\"5\"></td><td>%2</td></tr>")
.arg(tr("Name:"), card->getName().toHtmlEscaped()); .arg(tr("Name:"), card->getName().toHtmlEscaped());
if (exactCard.getPrinting() != PrintingInfo()) { if (!exactCard.getPrinting().isEmpty()) {
QString setShort = exactCard.getPrinting().getSet()->getShortName().toHtmlEscaped(); QString setShort = exactCard.getPrinting().getSet()->getShortName().toHtmlEscaped();
QString cardNum = exactCard.getPrinting().getProperty("num").toHtmlEscaped(); QString cardNum = exactCard.getPrinting().getProperty("num").toHtmlEscaped();

View file

@ -29,10 +29,14 @@ DeckAnalyticsWidget::DeckAnalyticsWidget(QWidget *parent, DeckListStatisticsAnal
removeButton = new QPushButton(this); removeButton = new QPushButton(this);
saveButton = new QPushButton(this); saveButton = new QPushButton(this);
loadButton = new QPushButton(this); loadButton = new QPushButton(this);
includeSideboardCheckBox = new QCheckBox(this);
includeSideboardCheckBox->setChecked(false);
controlLayout->addWidget(addButton); controlLayout->addWidget(addButton);
controlLayout->addWidget(removeButton); controlLayout->addWidget(removeButton);
controlLayout->addWidget(saveButton); controlLayout->addWidget(saveButton);
controlLayout->addWidget(loadButton); controlLayout->addWidget(loadButton);
controlLayout->addWidget(includeSideboardCheckBox);
layout->addWidget(controlContainer); layout->addWidget(controlContainer);
@ -40,6 +44,7 @@ DeckAnalyticsWidget::DeckAnalyticsWidget(QWidget *parent, DeckListStatisticsAnal
connect(removeButton, &QPushButton::clicked, this, &DeckAnalyticsWidget::onRemoveSelected); connect(removeButton, &QPushButton::clicked, this, &DeckAnalyticsWidget::onRemoveSelected);
connect(saveButton, &QPushButton::clicked, this, &DeckAnalyticsWidget::saveLayout); connect(saveButton, &QPushButton::clicked, this, &DeckAnalyticsWidget::saveLayout);
connect(loadButton, &QPushButton::clicked, this, &DeckAnalyticsWidget::loadLayout); connect(loadButton, &QPushButton::clicked, this, &DeckAnalyticsWidget::loadLayout);
connect(includeSideboardCheckBox, &QCheckBox::clicked, this, &DeckAnalyticsWidget::includeSideboardChanged);
// Scroll area and container // Scroll area and container
scrollArea = new QScrollArea(this); scrollArea = new QScrollArea(this);
@ -66,6 +71,13 @@ void DeckAnalyticsWidget::retranslateUi()
removeButton->setText(tr("Remove Panel")); removeButton->setText(tr("Remove Panel"));
saveButton->setText(tr("Save Layout")); saveButton->setText(tr("Save Layout"));
loadButton->setText(tr("Load Layout")); loadButton->setText(tr("Load Layout"));
includeSideboardCheckBox->setText(tr("Include Sideboard"));
}
void DeckAnalyticsWidget::includeSideboardChanged(bool checked)
{
statsAnalyzer->getConfig().includeSideboard = checked;
updateDisplays();
} }
void DeckAnalyticsWidget::updateDisplays() void DeckAnalyticsWidget::updateDisplays()

View file

@ -11,6 +11,7 @@
#include "deck_list_statistics_analyzer.h" #include "deck_list_statistics_analyzer.h"
#include "resizable_panel.h" #include "resizable_panel.h"
#include <QCheckBox>
#include <QJsonObject> #include <QJsonObject>
#include <QScrollArea> #include <QScrollArea>
#include <QVBoxLayout> #include <QVBoxLayout>
@ -29,6 +30,7 @@ public slots:
public: public:
explicit DeckAnalyticsWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer); explicit DeckAnalyticsWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer);
void retranslateUi(); void retranslateUi();
void includeSideboardChanged(bool checked);
private slots: private slots:
void onAddPanel(); void onAddPanel();
@ -57,6 +59,8 @@ private:
QPushButton *saveButton; QPushButton *saveButton;
QPushButton *loadButton; QPushButton *loadButton;
QCheckBox *includeSideboardCheckBox;
QScrollArea *scrollArea; QScrollArea *scrollArea;
QWidget *panelContainer; QWidget *panelContainer;
QVBoxLayout *panelLayout; QVBoxLayout *panelLayout;

View file

@ -19,7 +19,13 @@ void DeckListStatisticsAnalyzer::analyze()
{ {
clearData(); clearData();
QList<const DecklistCardNode *> nodes = model->getCardNodes(); QList<const DecklistCardNode *> nodes;
if (config.includeSideboard) {
nodes = model->getCardNodes();
} else {
nodes = model->getCardNodesForZone(DECK_ZONE_MAIN);
}
for (auto node : nodes) { for (auto node : nodes) {
CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(node->getName()); CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(node->getName());

View file

@ -17,6 +17,7 @@ struct DeckListStatisticsAnalyzerConfig
bool computeCategories = true; bool computeCategories = true;
bool computeCurveBreakdowns = true; bool computeCurveBreakdowns = true;
bool computeProbabilities = true; bool computeProbabilities = true;
bool includeSideboard = false;
}; };
class DeckListStatisticsAnalyzer : public QObject class DeckListStatisticsAnalyzer : public QObject
@ -133,6 +134,11 @@ public:
return model; return model;
} }
DeckListStatisticsAnalyzerConfig &getConfig()
{
return config;
}
signals: signals:
void statsUpdated(); void statsUpdated();

View file

@ -219,7 +219,9 @@ void DlgUpdate::downloadSuccessful(const QUrl &filepath)
{ {
setLabel(tr("Installing...")); setLabel(tr("Installing..."));
// Try to open the installer. If it opens, quit Cockatrice // Try to open the installer. If it opens, quit Cockatrice
if (QDesktopServices::openUrl(filepath)) { if (QProcess::startDetached(filepath.toLocalFile(),
QStringList()
<< "/R" << QString("/D=%1").arg(QCoreApplication::applicationDirPath()))) {
QMetaObject::invokeMethod(static_cast<MainWindow *>(parent()), "close", Qt::QueuedConnection); QMetaObject::invokeMethod(static_cast<MainWindow *>(parent()), "close", Qt::QueuedConnection);
qCInfo(DlgUpdateLog) << "Opened downloaded update file successfully - closing Cockatrice"; qCInfo(DlgUpdateLog) << "Opened downloaded update file successfully - closing Cockatrice";
close(); close();

View file

@ -91,6 +91,7 @@ GameSelector::GameSelector(AbstractClient *_client,
bool filtersSetToDefault = showFilters && gameListProxyModel->areFilterParametersSetToDefaults(); bool filtersSetToDefault = showFilters && gameListProxyModel->areFilterParametersSetToDefaults();
clearFilterButton->setEnabled(!filtersSetToDefault); clearFilterButton->setEnabled(!filtersSetToDefault);
connect(clearFilterButton, &QPushButton::clicked, this, &GameSelector::actClearFilter); connect(clearFilterButton, &QPushButton::clicked, this, &GameSelector::actClearFilter);
connect(gameListProxyModel, &GamesProxyModel::filtersChanged, this, &GameSelector::checkClearFilterButtonState);
if (room) { if (room) {
createButton = new QPushButton; createButton = new QPushButton;
@ -188,15 +189,16 @@ void GameSelector::actSetFilter()
dlg.getShowOnlyIfSpectatorsCanChat(), dlg.getShowOnlyIfSpectatorsCanSeeHands()); dlg.getShowOnlyIfSpectatorsCanChat(), dlg.getShowOnlyIfSpectatorsCanSeeHands());
gameListProxyModel->saveFilterParameters(gameTypeMap); gameListProxyModel->saveFilterParameters(gameTypeMap);
clearFilterButton->setEnabled(!gameListProxyModel->areFilterParametersSetToDefaults());
updateTitle(); updateTitle();
} }
void GameSelector::checkClearFilterButtonState()
{
clearFilterButton->setEnabled(!gameListProxyModel->areFilterParametersSetToDefaults());
}
void GameSelector::actClearFilter() void GameSelector::actClearFilter()
{ {
clearFilterButton->setEnabled(false);
gameListProxyModel->resetFilterParameters(); gameListProxyModel->resetFilterParameters();
gameListProxyModel->saveFilterParameters(gameTypeMap); gameListProxyModel->saveFilterParameters(gameTypeMap);

View file

@ -40,6 +40,7 @@ private slots:
* Updates the proxy model with selected filter parameters and refreshes the displayed game list. * Updates the proxy model with selected filter parameters and refreshes the displayed game list.
*/ */
void actSetFilter(); void actSetFilter();
void checkClearFilterButtonState();
/** /**
* @brief Clears all filters applied to the game list. * @brief Clears all filters applied to the game list.

View file

@ -19,32 +19,46 @@ GameSelectorQuickFilterToolBar::GameSelectorQuickFilterToolBar(QWidget *parent,
mainLayout->setSpacing(5); mainLayout->setSpacing(5);
searchBar = new QLineEdit(this); searchBar = new QLineEdit(this);
searchBar->setText(model->getCreatorNameFilters().join(", ")); searchBar->setText(model->getGameNameFilter());
connect(searchBar, &QLineEdit::textChanged, this, [this](const QString &text) { model->setGameNameFilter(text); }); connect(searchBar, &QLineEdit::textChanged, this, [this](const QString &text) {
applyFilters([&](auto &, auto &, auto &, auto &, auto &, auto &, auto &, QString &gameNameFilter, auto &,
auto &, auto &, auto &, auto &, auto &, auto &, auto &, auto &) { gameNameFilter = text; });
});
hideGamesNotCreatedByBuddiesCheckBox = new QCheckBox(this); hideGamesNotCreatedByBuddiesCheckBox = new QCheckBox(this);
hideGamesNotCreatedByBuddiesCheckBox->setChecked(model->getHideBuddiesOnlyGames()); hideGamesNotCreatedByBuddiesCheckBox->setChecked(model->getHideNotBuddyCreatedGames());
connect(hideGamesNotCreatedByBuddiesCheckBox, &QCheckBox::toggled, this, [this](bool checked) { connect(hideGamesNotCreatedByBuddiesCheckBox, &QCheckBox::toggled, this, [this](bool checked) {
applyFilters([&](auto &, auto &, auto &, auto &, auto &, bool &hideNotBuddyCreatedGames, auto &, auto &,
QStringList &creatorNameFilters, auto &, auto &, auto &, auto &, auto &, auto &, auto &,
auto &) {
hideNotBuddyCreatedGames = checked;
if (checked) { if (checked) {
QStringList buddyNames; QStringList buddyNames;
for (auto buddy : tabSupervisor->getUserListManager()->getBuddyList().values()) { for (auto buddy : tabSupervisor->getUserListManager()->getBuddyList().values()) {
buddyNames << QString::fromStdString(buddy.name()); buddyNames << QString::fromStdString(buddy.name());
} }
model->setCreatorNameFilters(buddyNames); creatorNameFilters = buddyNames;
} else { } else {
model->setCreatorNameFilters({}); creatorNameFilters.clear();
} }
}); });
});
hideFullGamesCheckBox = new QCheckBox(this); hideFullGamesCheckBox = new QCheckBox(this);
hideFullGamesCheckBox->setChecked(model->getHideFullGames()); hideFullGamesCheckBox->setChecked(model->getHideFullGames());
connect(hideFullGamesCheckBox, &QCheckBox::toggled, this, connect(hideFullGamesCheckBox, &QCheckBox::toggled, this, [this](bool checked) {
[this](bool checked) { model->setHideFullGames(checked); }); applyFilters([&](auto &, auto &, bool &hideFullGames, auto &, auto &, auto &, auto &, auto &, auto &, auto &,
auto &, auto &, auto &, auto &, auto &, auto &, auto &) { hideFullGames = checked; });
});
hideStartedGamesCheckBox = new QCheckBox(this); hideStartedGamesCheckBox = new QCheckBox(this);
hideStartedGamesCheckBox->setChecked(model->getHideGamesThatStarted()); hideStartedGamesCheckBox->setChecked(model->getHideGamesThatStarted());
connect(hideStartedGamesCheckBox, &QCheckBox::toggled, this, connect(hideStartedGamesCheckBox, &QCheckBox::toggled, this, [this](bool checked) {
[this](bool checked) { model->setHideGamesThatStarted(checked); }); applyFilters([&](auto &, auto &, auto &, bool &hideGamesThatStarted, auto &, auto &, auto &, auto &, auto &,
auto &, auto &, auto &, auto &, auto &, auto &, auto &,
auto &) { hideGamesThatStarted = checked; });
});
filterToFormatComboBox = new QComboBox(this); filterToFormatComboBox = new QComboBox(this);
@ -69,14 +83,16 @@ GameSelectorQuickFilterToolBar::GameSelectorQuickFilterToolBar(QWidget *parent,
// Update proxy model on selection change // Update proxy model on selection change
connect(filterToFormatComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int index) { connect(filterToFormatComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int index) {
applyFilters([&](auto &, auto &, auto &, auto &, auto &, auto &, auto &, auto &, auto &,
QSet<int> &gameTypeFilter, auto &, auto &, auto &, auto &, auto &, auto &, auto &) {
QVariant data = filterToFormatComboBox->itemData(index); QVariant data = filterToFormatComboBox->itemData(index);
if (!data.isValid()) { if (!data.isValid()) {
model->setGameTypeFilter({}); // empty = no filter gameTypeFilter.clear();
} else { } else {
int typeId = data.toInt(); gameTypeFilter = {data.toInt()};
model->setGameTypeFilter({typeId});
} }
}); });
});
hideGamesNotCreatedByBuddiesCheckBox->setMinimumSize(20, 20); hideGamesNotCreatedByBuddiesCheckBox->setMinimumSize(20, 20);
hideFullGamesCheckBox->setMinimumSize(20, 20); hideFullGamesCheckBox->setMinimumSize(20, 20);
@ -96,9 +112,87 @@ GameSelectorQuickFilterToolBar::GameSelectorQuickFilterToolBar(QWidget *parent,
setLayout(mainLayout); setLayout(mainLayout);
syncFromModel();
connect(model, &GamesProxyModel::filtersChanged, this, &GameSelectorQuickFilterToolBar::syncFromModel);
retranslateUi(); retranslateUi();
} }
void GameSelectorQuickFilterToolBar::syncFromModel()
{
QSignalBlocker b1(searchBar);
QSignalBlocker b2(filterToFormatComboBox);
QSignalBlocker b3(hideGamesNotCreatedByBuddiesCheckBox);
QSignalBlocker b4(hideFullGamesCheckBox);
QSignalBlocker b5(hideStartedGamesCheckBox);
searchBar->setText(model->getGameNameFilter());
hideGamesNotCreatedByBuddiesCheckBox->setChecked(model->getHideNotBuddyCreatedGames());
hideFullGamesCheckBox->setChecked(model->getHideFullGames());
hideStartedGamesCheckBox->setChecked(model->getHideGamesThatStarted());
QSet<int> types = model->getGameTypeFilter();
if (types.size() == 1) {
int idx = filterToFormatComboBox->findData(*types.begin());
filterToFormatComboBox->setCurrentIndex(idx >= 0 ? idx : 0);
} else {
filterToFormatComboBox->setCurrentIndex(0);
}
}
void GameSelectorQuickFilterToolBar::applyFilters(std::function<void(bool &,
bool &,
bool &,
bool &,
bool &,
bool &,
bool &,
QString &,
QStringList &,
QSet<int> &,
int &,
int &,
QTime &,
bool &,
bool &,
bool &,
bool &)> mutator)
{
bool hideBuddiesOnlyGames = model->getHideBuddiesOnlyGames();
bool hideIgnoredUserGames = model->getHideIgnoredUserGames();
bool hideFullGames = model->getHideFullGames();
bool hideGamesThatStarted = model->getHideGamesThatStarted();
bool hidePasswordProtectedGames = model->getHidePasswordProtectedGames();
bool hideNotBuddyCreatedGames = model->getHideNotBuddyCreatedGames();
bool hideOpenDecklistGames = model->getHideOpenDecklistGames();
QString gameNameFilter = model->getGameNameFilter();
QStringList creatorNameFilters = model->getCreatorNameFilters();
QSet<int> gameTypeFilter = model->getGameTypeFilter();
int minPlayers = model->getMaxPlayersFilterMin();
int maxPlayers = model->getMaxPlayersFilterMax();
QTime maxGameAge = model->getMaxGameAge();
bool showOnlyIfSpectatorsCanWatch = model->getShowOnlyIfSpectatorsCanWatch();
bool showSpectatorPasswordProtected = model->getShowSpectatorPasswordProtected();
bool showOnlyIfSpectatorsCanChat = model->getShowOnlyIfSpectatorsCanChat();
bool showOnlyIfSpectatorsCanSeeHands = model->getShowOnlyIfSpectatorsCanSeeHands();
mutator(hideBuddiesOnlyGames, hideIgnoredUserGames, hideFullGames, hideGamesThatStarted, hidePasswordProtectedGames,
hideNotBuddyCreatedGames, hideOpenDecklistGames, gameNameFilter, creatorNameFilters, gameTypeFilter,
minPlayers, maxPlayers, maxGameAge, showOnlyIfSpectatorsCanWatch, showSpectatorPasswordProtected,
showOnlyIfSpectatorsCanChat, showOnlyIfSpectatorsCanSeeHands);
model->setGameFilters(hideBuddiesOnlyGames, hideIgnoredUserGames, hideFullGames, hideGamesThatStarted,
hidePasswordProtectedGames, hideNotBuddyCreatedGames, hideOpenDecklistGames, gameNameFilter,
creatorNameFilters, gameTypeFilter, minPlayers, maxPlayers, maxGameAge,
showOnlyIfSpectatorsCanWatch, showSpectatorPasswordProtected, showOnlyIfSpectatorsCanChat,
showOnlyIfSpectatorsCanSeeHands);
}
void GameSelectorQuickFilterToolBar::retranslateUi() void GameSelectorQuickFilterToolBar::retranslateUi()
{ {
searchBar->setPlaceholderText(tr("Filter by game name...")); searchBar->setPlaceholderText(tr("Filter by game name..."));

View file

@ -18,6 +18,24 @@ public:
TabSupervisor *tabSupervisor, TabSupervisor *tabSupervisor,
GamesProxyModel *model, GamesProxyModel *model,
const QMap<int, QString> &allGameTypes); const QMap<int, QString> &allGameTypes);
void syncFromModel();
void applyFilters(std::function<void(bool &,
bool &,
bool &,
bool &,
bool &,
bool &,
bool &,
QString &,
QStringList &,
QSet<int> &,
int &,
int &,
QTime &,
bool &,
bool &,
bool &,
bool &)> mutator);
void retranslateUi(); void retranslateUi();
private: private:

View file

@ -326,6 +326,7 @@ void GamesProxyModel::setGameFilters(bool _hideBuddiesOnlyGames,
#else #else
invalidateFilter(); invalidateFilter();
#endif #endif
emit filtersChanged();
} }
int GamesProxyModel::getNumFilteredGames() const int GamesProxyModel::getNumFilteredGames() const

View file

@ -138,6 +138,9 @@ private:
bool showOnlyIfSpectatorsCanChat; bool showOnlyIfSpectatorsCanChat;
bool showOnlyIfSpectatorsCanSeeHands; bool showOnlyIfSpectatorsCanSeeHands;
signals:
void filtersChanged();
public: public:
/** /**
* @brief Constructs a GamesProxyModel. * @brief Constructs a GamesProxyModel.

View file

@ -4,10 +4,29 @@ void ArchidektApiResponseCardEntry::fromJson(const QJsonObject &json)
{ {
id = json.value("id").toInt(); id = json.value("id").toInt();
categories.clear();
auto categoriesJson = json.value("categories").toArray(); auto categoriesJson = json.value("categories").toArray();
for (auto category : categoriesJson) { for (const auto &categoryValue : categoriesJson) {
categories.append(category.toString()); Category cat;
if (categoryValue.isObject()) {
QJsonObject obj = categoryValue.toObject();
cat.id = obj.value("id").toInt();
cat.name = obj.value("name").toString();
cat.isPremier = obj.value("isPremier").toBool();
cat.includedInDeck = obj.value("includedInDeck").toBool();
cat.includedInPrice = obj.value("includedInPrice").toBool();
} else if (categoryValue.isString()) {
cat.name = categoryValue.toString();
// assume mainboard unless known otherwise
cat.includedInDeck = true;
}
categories.append(cat);
} }
companion = json.value("companion").toBool(); companion = json.value("companion").toBool();
@ -27,7 +46,13 @@ void ArchidektApiResponseCardEntry::fromJson(const QJsonObject &json)
void ArchidektApiResponseCardEntry::debugPrint() const void ArchidektApiResponseCardEntry::debugPrint() const
{ {
qDebug() << "Id:" << id; qDebug() << "Id:" << id;
qDebug() << "Categories:" << categories; for (auto category : categories) {
qDebug() << "Category ID:" << category.id;
qDebug() << "Category Name:" << category.name;
qDebug() << "Category Premier:" << category.isPremier;
qDebug() << "Category Included in Deck:" << category.includedInDeck;
qDebug() << "Category Included in Price:" << category.includedInPrice;
}
qDebug() << "Companion:" << companion; qDebug() << "Companion:" << companion;
qDebug() << "FlippedDefault:" << flippedDefault; qDebug() << "FlippedDefault:" << flippedDefault;
qDebug() << "Label:" << label; qDebug() << "Label:" << label;

View file

@ -9,6 +9,15 @@
#include <QString> #include <QString>
#include <QVector> #include <QVector>
struct Category
{
int id;
QString name;
bool isPremier;
bool includedInDeck;
bool includedInPrice;
};
class ArchidektApiResponseCardEntry class ArchidektApiResponseCardEntry
{ {
public: public:
@ -26,7 +35,7 @@ public:
return card; return card;
}; };
QStringList getCategories() const QList<Category> getCategories() const
{ {
return categories; return categories;
} }
@ -38,7 +47,7 @@ public:
private: private:
int id; int id;
QStringList categories; QList<Category> categories;
bool companion; bool companion;
bool flippedDefault; bool flippedDefault;
QString label; QString label;

View file

@ -63,16 +63,60 @@ ArchidektApiResponseDeckDisplayWidget::ArchidektApiResponseDeckDisplayWidget(QWi
QString tempDeck; QString tempDeck;
QTextStream deckStream(&tempDeck); QTextStream deckStream(&tempDeck);
for (auto card : response.getCards()) { QString mainboardText;
QString sideboardText;
QTextStream mainStream(&mainboardText);
QTextStream sideStream(&sideboardText);
for (const auto &card : response.getCards()) {
QString fullName = card.getCard().getOracleCard().value("name").toString(); QString fullName = card.getCard().getOracleCard().value("name").toString();
// We don't really care about the second card, the card database already has it as a relation // We don't really care about the second card, the card database already has it as a relation
QString cleanName = fullName.split("//").first().trimmed(); QString cleanName = fullName.split("//").first().trimmed();
tempDeck += QString("%1 %2 (%3) %4\n") QString line = QString("%1 %2 (%3) %4\n")
.arg(card.getQuantity()) .arg(card.getQuantity())
.arg(cleanName) .arg(cleanName)
.arg(card.getCard().getEdition().getEditionCode().toUpper()) .arg(card.getCard().getEdition().getEditionCode().toUpper())
.arg(card.getCard().getCollectorNumber()); .arg(card.getCard().getCollectorNumber());
bool isCommander = false;
bool isSideboardCategory = false;
bool includedInDeck = false;
for (const auto &cat : card.getCategories()) {
if (cat.name.compare("Commander", Qt::CaseInsensitive) == 0) {
isCommander = true;
}
if (cat.name.compare("Sideboard", Qt::CaseInsensitive) == 0 ||
cat.name.compare("Maybeboard", Qt::CaseInsensitive) == 0) {
isSideboardCategory = true;
}
if (cat.includedInDeck) {
includedInDeck = true;
}
}
QString target;
if (isCommander || isSideboardCategory) {
sideStream << line;
} else if (includedInDeck) {
mainStream << line;
} else {
sideStream << line;
}
}
// Combine with blank line separator
tempDeck = mainboardText;
if (!sideboardText.isEmpty()) {
tempDeck += "\n";
tempDeck += sideboardText;
} }
model = new DeckListModel(this); model = new DeckListModel(this);

View file

@ -3,6 +3,7 @@
#include "../../../../../general/display/background_plate_widget.h" #include "../../../../../general/display/background_plate_widget.h"
#include "../../tab_edhrec_main.h" #include "../../tab_edhrec_main.h"
#include <QMouseEvent>
#include <libcockatrice/card/database/card_database_manager.h> #include <libcockatrice/card/database/card_database_manager.h>
EdhrecApiResponseCardDetailsDisplayWidget::EdhrecApiResponseCardDetailsDisplayWidget( EdhrecApiResponseCardDetailsDisplayWidget::EdhrecApiResponseCardDetailsDisplayWidget(
@ -48,7 +49,7 @@ EdhrecApiResponseCardDetailsDisplayWidget::EdhrecApiResponseCardDetailsDisplayWi
if (parentTab) { if (parentTab) {
cardPictureWidget->setScaleFactor(parentTab->getCardSizeSlider()->getSlider()->value()); cardPictureWidget->setScaleFactor(parentTab->getCardSizeSlider()->getSlider()->value());
connect(cardPictureWidget, &CardInfoPictureWidget::cardClicked, this, connect(cardPictureWidget, &CardInfoPictureWidget::cardClicked, this,
&EdhrecApiResponseCardDetailsDisplayWidget::actRequestPageNavigation); &EdhrecApiResponseCardDetailsDisplayWidget::mousePressEvent);
connect(parentTab->getCardSizeSlider()->getSlider(), &QSlider::valueChanged, cardPictureWidget, connect(parentTab->getCardSizeSlider()->getSlider(), &QSlider::valueChanged, cardPictureWidget,
&CardInfoPictureWidget::setScaleFactor); &CardInfoPictureWidget::setScaleFactor);
connect(this, &EdhrecApiResponseCardDetailsDisplayWidget::requestUrl, parentTab, connect(this, &EdhrecApiResponseCardDetailsDisplayWidget::requestUrl, parentTab,
@ -59,8 +60,10 @@ EdhrecApiResponseCardDetailsDisplayWidget::EdhrecApiResponseCardDetailsDisplayWi
void EdhrecApiResponseCardDetailsDisplayWidget::mousePressEvent(QMouseEvent *event) void EdhrecApiResponseCardDetailsDisplayWidget::mousePressEvent(QMouseEvent *event)
{ {
QWidget::mousePressEvent(event); QWidget::mousePressEvent(event);
if (event->button() == Qt::LeftButton) {
actRequestPageNavigation(); actRequestPageNavigation();
} }
}
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
void EdhrecApiResponseCardDetailsDisplayWidget::enterEvent(QEnterEvent *event) void EdhrecApiResponseCardDetailsDisplayWidget::enterEvent(QEnterEvent *event)

View file

@ -259,6 +259,9 @@ TabGame::~TabGame()
if (replayManager) { if (replayManager) {
delete replayManager->replay; delete replayManager->replay;
} }
for (auto &player : game->getPlayerManager()->getPlayers()) {
player->clear();
}
} }
void TabGame::updatePlayerListDockTitle() void TabGame::updatePlayerListDockTitle()

View file

@ -57,6 +57,8 @@ TabDeckEditorVisualTabWidget::TabDeckEditorVisualTabWidget(QWidget *parent,
this->addNewTab(visualDatabaseDisplay, tr("Visual Database Display")); this->addNewTab(visualDatabaseDisplay, tr("Visual Database Display"));
this->addNewTab(deckAnalytics, tr("Deck Analytics")); this->addNewTab(deckAnalytics, tr("Deck Analytics"));
this->addNewTab(sampleHandWidget, tr("Sample Hand")); this->addNewTab(sampleHandWidget, tr("Sample Hand"));
setTabsClosable(false);
} }
/** /**

View file

@ -0,0 +1,21 @@
#ifndef COCKATRICE_VISUAL_DATABASE_DISPLAY_FILTER_BUTTON_H
#define COCKATRICE_VISUAL_DATABASE_DISPLAY_FILTER_BUTTON_H
#include <QString>
const QString visualDatabaseDisplayFilterButtonStyle = QString(R"(
QPushButton {
background-color: palette(button);
color: palette(button-text);
padding: 5px 10px;
border-radius: 4px;
border: 1px solid palette(dark);
}
QPushButton:checked {
background-color: palette(highlight);
color: palette(highlighted-text);
border: 1px solid palette(shadow);
}
)");
#endif // COCKATRICE_VISUAL_DATABASE_DISPLAY_FILTER_BUTTON_H

View file

@ -1,6 +1,7 @@
#include "visual_database_display_format_legality_filter_widget.h" #include "visual_database_display_format_legality_filter_widget.h"
#include "../../../filters/filter_tree_model.h" #include "../../../filters/filter_tree_model.h"
#include "visual_database_display_filter_button.h"
#include <QLabel> #include <QLabel>
#include <QPushButton> #include <QPushButton>
@ -80,8 +81,7 @@ void VisualDatabaseDisplayFormatLegalityFilterWidget::createFormatButtons()
for (auto it = allFormatsWithCount.begin(); it != allFormatsWithCount.end(); ++it) { for (auto it = allFormatsWithCount.begin(); it != allFormatsWithCount.end(); ++it) {
auto *button = new QPushButton(it.key(), flowWidget); auto *button = new QPushButton(it.key(), flowWidget);
button->setCheckable(true); button->setCheckable(true);
button->setStyleSheet("QPushButton { background-color: lightgray; border: 1px solid gray; padding: 5px; }" button->setStyleSheet(visualDatabaseDisplayFilterButtonStyle);
"QPushButton:checked { background-color: green; color: white; }");
flowWidget->addWidget(button); flowWidget->addWidget(button);
formatButtons[it.key()] = button; formatButtons[it.key()] = button;

View file

@ -1,6 +1,7 @@
#include "visual_database_display_main_type_filter_widget.h" #include "visual_database_display_main_type_filter_widget.h"
#include "../../../filters/filter_tree_model.h" #include "../../../filters/filter_tree_model.h"
#include "visual_database_display_filter_button.h"
#include <QLabel> #include <QLabel>
#include <QPushButton> #include <QPushButton>
@ -75,8 +76,8 @@ void VisualDatabaseDisplayMainTypeFilterWidget::createMainTypeButtons()
for (auto it = allMainCardTypesWithCount.begin(); it != allMainCardTypesWithCount.end(); ++it) { for (auto it = allMainCardTypesWithCount.begin(); it != allMainCardTypesWithCount.end(); ++it) {
auto *button = new QPushButton(it.key(), flowWidget); auto *button = new QPushButton(it.key(), flowWidget);
button->setCheckable(true); button->setCheckable(true);
button->setStyleSheet("QPushButton { background-color: lightgray; border: 1px solid gray; padding: 5px; }"
"QPushButton:checked { background-color: green; color: white; }"); button->setStyleSheet(visualDatabaseDisplayFilterButtonStyle);
flowWidget->addWidget(button); flowWidget->addWidget(button);
typeButtons[it.key()] = button; typeButtons[it.key()] = button;

View file

@ -3,6 +3,7 @@
#include "../../../interface/widgets/dialogs/dlg_load_deck_from_clipboard.h" #include "../../../interface/widgets/dialogs/dlg_load_deck_from_clipboard.h"
#include "../../../interface/widgets/tabs/abstract_tab_deck_editor.h" #include "../../../interface/widgets/tabs/abstract_tab_deck_editor.h"
#include "../deck_editor/deck_state_manager.h" #include "../deck_editor/deck_state_manager.h"
#include "visual_database_display_filter_button.h"
#include <QHBoxLayout> #include <QHBoxLayout>
@ -95,8 +96,8 @@ void VisualDatabaseDisplayNameFilterWidget::createNameFilter(const QString &name
// Create a button for the filter // Create a button for the filter
auto *button = new QPushButton(name, flowWidget); auto *button = new QPushButton(name, flowWidget);
button->setStyleSheet("QPushButton { background-color: lightgray; border: 1px solid gray; padding: 5px; }"
"QPushButton:hover { background-color: red; color: white; }"); button->setStyleSheet(visualDatabaseDisplayFilterButtonStyle);
connect(button, &QPushButton::clicked, this, [this, name]() { connect(button, &QPushButton::clicked, this, [this, name]() {
removeNameFilter(name); removeNameFilter(name);

View file

@ -2,6 +2,7 @@
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../../../filters/filter_tree_model.h" #include "../../../filters/filter_tree_model.h"
#include "visual_database_display_filter_button.h"
#include <QLineEdit> #include <QLineEdit>
#include <QPushButton> #include <QPushButton>
@ -101,8 +102,8 @@ void VisualDatabaseDisplaySetFilterWidget::createSetButtons()
auto *button = new QPushButton(longName + " (" + shortName + ")", flowWidget); auto *button = new QPushButton(longName + " (" + shortName + ")", flowWidget);
button->setCheckable(true); button->setCheckable(true);
button->setStyleSheet("QPushButton { background-color: lightgray; border: 1px solid gray; padding: 5px; }"
"QPushButton:checked { background-color: green; color: white; }"); button->setStyleSheet(visualDatabaseDisplayFilterButtonStyle);
flowWidget->addWidget(button); flowWidget->addWidget(button);
setButtons[shortName] = button; setButtons[shortName] = button;

View file

@ -1,6 +1,7 @@
#include "visual_database_display_sub_type_filter_widget.h" #include "visual_database_display_sub_type_filter_widget.h"
#include "../../../filters/filter_tree_model.h" #include "../../../filters/filter_tree_model.h"
#include "visual_database_display_filter_button.h"
#include <QLabel> #include <QLabel>
#include <QLineEdit> #include <QLineEdit>
@ -80,8 +81,8 @@ void VisualDatabaseDisplaySubTypeFilterWidget::createSubTypeButtons()
for (auto it = allSubCardTypesWithCount.begin(); it != allSubCardTypesWithCount.end(); ++it) { for (auto it = allSubCardTypesWithCount.begin(); it != allSubCardTypesWithCount.end(); ++it) {
auto *button = new QPushButton(it.key(), flowWidget); auto *button = new QPushButton(it.key(), flowWidget);
button->setCheckable(true); button->setCheckable(true);
button->setStyleSheet("QPushButton { background-color: lightgray; border: 1px solid gray; padding: 5px; }"
"QPushButton:checked { background-color: green; color: white; }"); button->setStyleSheet(visualDatabaseDisplayFilterButtonStyle);
flowWidget->addWidget(button); flowWidget->addWidget(button);
typeButtons[it.key()] = button; typeButtons[it.key()] = button;

View file

@ -140,6 +140,7 @@ void VisualDatabaseDisplayWidget::initialize()
databaseLoadIndicator->setVisible(false); databaseLoadIndicator->setVisible(false);
filterContainer->initialize(); filterContainer->initialize();
filterContainer->setVisible(true);
searchLayout->addWidget(colorFilterWidget); searchLayout->addWidget(colorFilterWidget);
searchLayout->addWidget(clearFilterWidget); searchLayout->addWidget(clearFilterWidget);

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -133,7 +133,7 @@ ExactCard CardDatabaseQuerier::getRandomCard() const
ExactCard CardDatabaseQuerier::getCardFromSameSet(const QString &cardName, const PrintingInfo &otherPrinting) const ExactCard CardDatabaseQuerier::getCardFromSameSet(const QString &cardName, const PrintingInfo &otherPrinting) const
{ {
// The source card does not have a printing defined, which means we can't get a card from the same set. // The source card does not have a printing defined, which means we can't get a card from the same set.
if (otherPrinting == PrintingInfo()) { if (otherPrinting.isEmpty()) {
return getCard({cardName}); return getCard({cardName});
} }

View file

@ -54,6 +54,16 @@ public:
return this->set == other.set && this->properties == other.properties; return this->set == other.set && this->properties == other.properties;
} }
/**
* @brief check if the info is empty, as if default constructed.
*
* @return True if both set and properties are empty, otherwise false.
*/
bool isEmpty() const
{
return set == nullptr && properties.isEmpty();
}
private: private:
CardSetPtr set; ///< The set this variation belongs to. CardSetPtr set; ///< The set this variation belongs to.
QVariantHash properties; ///< Key-value store for variation-specific attributes. QVariantHash properties; ///< Key-value store for variation-specific attributes.

View file

@ -49,6 +49,7 @@
#include <libcockatrice/rng/rng_abstract.h> #include <libcockatrice/rng/rng_abstract.h>
#include <libcockatrice/utility/trice_limits.h> #include <libcockatrice/utility/trice_limits.h>
#include <libcockatrice/utility/zone_names.h> #include <libcockatrice/utility/zone_names.h>
#include <ranges>
Server_AbstractPlayer::Server_AbstractPlayer(Server_Game *_game, Server_AbstractPlayer::Server_AbstractPlayer(Server_Game *_game,
int _playerId, int _playerId,
@ -228,6 +229,37 @@ shouldBeFaceDown(const MoveCardStruct &cardStruct, const Server_CardZone *startZ
return false; return false;
} }
/**
* @brief Determines whether a set of moved cards is from the bottom of the deck
*/
static bool shouldBeFromTheBottom(const Server_CardZone *startZone, const std::set<MoveCardStruct> &cardsToMove)
{
if (!startZone) {
return false;
}
if (startZone->getName() != ZoneNames::DECK) {
return false;
}
int movedCount = static_cast<int>(cardsToMove.size());
int tailStart = startZone->getCards().size() - movedCount;
if (tailStart <= 0) { // if the entire deck is moved it should not be considered from the bottom
return false;
}
// check if the move is a contiguous block at the end of the deck, fail fast when not
int expectedPosition = tailStart;
for (const auto &card : cardsToMove) {
if (card.position != expectedPosition) {
return false;
}
++expectedPosition;
}
return true;
}
Response::ResponseCode Server_AbstractPlayer::moveCard(GameEventStorage &ges, Response::ResponseCode Server_AbstractPlayer::moveCard(GameEventStorage &ges,
Server_CardZone *startzone, Server_CardZone *startzone,
const QList<const CardToMove *> &_cards, const QList<const CardToMove *> &_cards,
@ -244,9 +276,12 @@ Response::ResponseCode Server_AbstractPlayer::moveCard(GameEventStorage &ges,
return Response::RespContextError; return Response::RespContextError;
} }
if (!targetzone->hasCoords() && (xCoord <= -1)) { if (!targetzone->hasCoords()) {
yCoord = 0;
if (xCoord <= -1) {
xCoord = targetzone->getCards().size(); xCoord = targetzone->getCards().size();
} }
}
std::set<MoveCardStruct> cardsToMove; std::set<MoveCardStruct> cardsToMove;
QSet<int> cardIdsToMove; QSet<int> cardIdsToMove;
@ -285,7 +320,52 @@ Response::ResponseCode Server_AbstractPlayer::moveCard(GameEventStorage &ges,
bool revealTopStart = false; bool revealTopStart = false;
bool revealTopTarget = false; bool revealTopTarget = false;
for (auto cardStruct : cardsToMove) { bool isFromBottom = shouldBeFromTheBottom(startzone, cardsToMove);
if (isFromBottom) {
std::ranges::reverse_view reversedCardsToMove{cardsToMove};
for (auto card : reversedCardsToMove) {
processMoveCard(ges, startzone, targetzone, card, xCoord, yCoord, xIndex, revealTopStart, revealTopTarget,
isReversed, undoingDraw);
}
} else {
for (auto card : cardsToMove) {
processMoveCard(ges, startzone, targetzone, card, xCoord, yCoord, xIndex, revealTopStart, revealTopTarget,
isReversed, undoingDraw);
}
}
if (revealTopStart) {
revealTopCardIfNeeded(startzone, ges);
}
if (targetzone != startzone && revealTopTarget) {
revealTopCardIfNeeded(targetzone, ges);
}
if (undoingDraw) {
ges.setGameEventContext(Context_UndoDraw());
} else {
ges.setGameEventContext(Context_MoveCard());
}
if (startzone->hasCoords() && fixFreeSpaces) {
startzone->fixFreeSpaces(ges);
}
return Response::RespOk;
}
void Server_AbstractPlayer::processMoveCard(GameEventStorage &ges,
Server_CardZone *startzone,
Server_CardZone *targetzone,
MoveCardStruct cardStruct,
int xCoord,
int yCoord,
int &xIndex,
bool &revealTopStart,
bool &revealTopTarget,
bool isReversed,
bool undoingDraw)
{
Server_Card *card = cardStruct.card; Server_Card *card = cardStruct.card;
int originalPosition = cardStruct.position; int originalPosition = cardStruct.position;
@ -328,8 +408,7 @@ Response::ResponseCode Server_AbstractPlayer::moveCard(GameEventStorage &ges,
if (Server_Card *stashedCard = card->takeStashedCard()) { if (Server_Card *stashedCard = card->takeStashedCard()) {
stashedCard->setId(newCardId()); stashedCard->setId(newCardId());
ges.enqueueGameEvent(makeCreateTokenEvent(startzone, stashedCard, card->getX(), card->getY()), ges.enqueueGameEvent(makeCreateTokenEvent(startzone, stashedCard, card->getX(), card->getY()), playerId);
playerId);
card->deleteLater(); card->deleteLater();
card = stashedCard; card = stashedCard;
} else { } else {
@ -347,7 +426,6 @@ Response::ResponseCode Server_AbstractPlayer::moveCard(GameEventStorage &ges,
if (targetzone->hasCoords()) { if (targetzone->hasCoords()) {
newX = targetzone->getFreeGridColumn(newX, yCoord, card->getName(), faceDown); newX = targetzone->getFreeGridColumn(newX, yCoord, card->getName(), faceDown);
} else { } else {
yCoord = 0;
card->resetState(targetzone->getName() == ZoneNames::STACK); card->resetState(targetzone->getName() == ZoneNames::STACK);
} }
@ -443,24 +521,6 @@ Response::ResponseCode Server_AbstractPlayer::moveCard(GameEventStorage &ges,
onCardBeingMoved(ges, cardStruct, startzone, targetzone, undoingDraw); onCardBeingMoved(ges, cardStruct, startzone, targetzone, undoingDraw);
} }
} }
if (revealTopStart) {
revealTopCardIfNeeded(startzone, ges);
}
if (targetzone != startzone && revealTopTarget) {
revealTopCardIfNeeded(targetzone, ges);
}
if (undoingDraw) {
ges.setGameEventContext(Context_UndoDraw());
} else {
ges.setGameEventContext(Context_MoveCard());
}
if (startzone->hasCoords() && fixFreeSpaces) {
startzone->fixFreeSpaces(ges);
}
return Response::RespOk;
}
void Server_AbstractPlayer::onCardBeingMoved(GameEventStorage &ges, void Server_AbstractPlayer::onCardBeingMoved(GameEventStorage &ges,
const MoveCardStruct &cardStruct, const MoveCardStruct &cardStruct,
@ -1502,7 +1562,7 @@ Server_AbstractPlayer::cmdRevealCards(const Command_RevealCards &cmd, ResponseCo
zone->addWritePermission(cmd.player_id()); zone->addWritePermission(cmd.player_id());
} }
if (isJudge()) { if (judge) {
ges.setOverwriteOwnership(true); ges.setOverwriteOwnership(true);
} }

View file

@ -93,6 +93,19 @@ public:
bool fixFreeSpaces = true, bool fixFreeSpaces = true,
bool undoingDraw = false, bool undoingDraw = false,
bool isReversed = false); bool isReversed = false);
void processMoveCard(GameEventStorage &ges,
Server_CardZone *startzone,
Server_CardZone *targetzone,
MoveCardStruct cardStruct,
int xCoord,
int yCoord,
int &xIndex,
bool &revealTopStart,
bool &revealTopTarget,
bool isReversed,
bool undoingDraw);
virtual void onCardBeingMoved(GameEventStorage &ges, virtual void onCardBeingMoved(GameEventStorage &ges,
const MoveCardStruct &cardStruct, const MoveCardStruct &cardStruct,
Server_CardZone *startzone, Server_CardZone *startzone,

View file

@ -306,7 +306,7 @@ void Server_Game::sendGameStateToPlayers()
} }
} else { } else {
Event_GameStateChanged event; Event_GameStateChanged event;
createGameStateChangedEvent(&event, participant, false, false); createGameStateChangedEvent(&event, participant, participant->isJudge(), false);
gec = prepareGameEvent(event, -1); gec = prepareGameEvent(event, -1);
} }
@ -750,7 +750,7 @@ void Server_Game::createGameJoinedEvent(Server_AbstractParticipant *joiningParti
event2.set_active_player_id(activePlayer); event2.set_active_player_id(activePlayer);
event2.set_active_phase(activePhase); event2.set_active_phase(activePhase);
bool omniscient = joiningParticipant->isSpectator() && (spectatorsSeeEverything || joiningParticipant->isJudge()); bool omniscient = (joiningParticipant->isSpectator() && spectatorsSeeEverything) || joiningParticipant->isJudge();
for (auto *participant : participants.values()) { for (auto *participant : participants.values()) {
participant->getInfo(event2.add_player_list(), joiningParticipant, omniscient, true); participant->getInfo(event2.add_player_list(), joiningParticipant, omniscient, true);
} }
@ -766,8 +766,8 @@ void Server_Game::sendGameEventContainer(GameEventContainer *cont,
cont->set_game_id(gameId); cont->set_game_id(gameId);
for (auto *participant : participants.values()) { for (auto *participant : participants.values()) {
const bool playerPrivate = (participant->getPlayerId() == privatePlayerId) || const bool playerPrivate = (participant->getPlayerId() == privatePlayerId) || participant->isJudge() ||
(participant->isSpectator() && (spectatorsSeeEverything || participant->isJudge())); (participant->isSpectator() && spectatorsSeeEverything);
if ((recipients.testFlag(GameEventStorageItem::SendToPrivate) && playerPrivate) || if ((recipients.testFlag(GameEventStorageItem::SendToPrivate) && playerPrivate) ||
(recipients.testFlag(GameEventStorageItem::SendToOthers) && !playerPrivate)) (recipients.testFlag(GameEventStorageItem::SendToOthers) && !playerPrivate))
participant->sendGameEvent(*cont); participant->sendGameEvent(*cont);

View file

@ -366,8 +366,6 @@ int OracleImporter::importCardsFromSet(const CardSetPtr &currentSet, const QList
auto found_iter = splitCards.find(name + numProperty); auto found_iter = splitCards.find(name + numProperty);
if (found_iter == splitCards.end()) { if (found_iter == splitCards.end()) {
splitCards.insert(name + numProperty, {{split}, name}); splitCards.insert(name + numProperty, {{split}, name});
} else if (layout == "adventure" || layout == "prepare") {
found_iter->first.insert(0, split);
} else { } else {
found_iter->first.append(split); found_iter->first.append(split);
} }

View file

@ -276,7 +276,7 @@
<context> <context>
<name>OracleImporter</name> <name>OracleImporter</name>
<message> <message>
<location filename="src/oracleimporter.cpp" line="541"/> <location filename="src/oracleimporter.cpp" line="540"/>
<source>Dummy set containing tokens</source> <source>Dummy set containing tokens</source>
<translation>Platzhalter Edition mit Spielsteinen</translation> <translation>Platzhalter Edition mit Spielsteinen</translation>
</message> </message>
@ -284,7 +284,7 @@
<context> <context>
<name>OracleWizard</name> <name>OracleWizard</name>
<message> <message>
<location filename="src/oraclewizard.cpp" line="70"/> <location filename="src/oraclewizard.cpp" line="97"/>
<source>Oracle Importer</source> <source>Oracle Importer</source>
<translation>Oracle Importer</translation> <translation>Oracle Importer</translation>
</message> </message>

View file

@ -276,7 +276,7 @@
<context> <context>
<name>OracleImporter</name> <name>OracleImporter</name>
<message> <message>
<location filename="src/oracleimporter.cpp" line="541"/> <location filename="src/oracleimporter.cpp" line="540"/>
<source>Dummy set containing tokens</source> <source>Dummy set containing tokens</source>
<translation>Εικονικό σετ που περιέχει tokens</translation> <translation>Εικονικό σετ που περιέχει tokens</translation>
</message> </message>
@ -284,7 +284,7 @@
<context> <context>
<name>OracleWizard</name> <name>OracleWizard</name>
<message> <message>
<location filename="src/oraclewizard.cpp" line="70"/> <location filename="src/oraclewizard.cpp" line="97"/>
<source>Oracle Importer</source> <source>Oracle Importer</source>
<translation>Εισαγωγέας Oracle</translation> <translation>Εισαγωγέας Oracle</translation>
</message> </message>

View file

@ -276,7 +276,7 @@
<context> <context>
<name>OracleImporter</name> <name>OracleImporter</name>
<message> <message>
<location filename="src/oracleimporter.cpp" line="541"/> <location filename="src/oracleimporter.cpp" line="540"/>
<source>Dummy set containing tokens</source> <source>Dummy set containing tokens</source>
<translation>Dummy set containing tokens</translation> <translation>Dummy set containing tokens</translation>
</message> </message>
@ -284,7 +284,7 @@
<context> <context>
<name>OracleWizard</name> <name>OracleWizard</name>
<message> <message>
<location filename="src/oraclewizard.cpp" line="70"/> <location filename="src/oraclewizard.cpp" line="97"/>
<source>Oracle Importer</source> <source>Oracle Importer</source>
<translation>Oracle Importer</translation> <translation>Oracle Importer</translation>
</message> </message>

View file

@ -276,7 +276,7 @@
<context> <context>
<name>OracleImporter</name> <name>OracleImporter</name>
<message> <message>
<location filename="src/oracleimporter.cpp" line="541"/> <location filename="src/oracleimporter.cpp" line="540"/>
<source>Dummy set containing tokens</source> <source>Dummy set containing tokens</source>
<translation>Set dedicado para tokens</translation> <translation>Set dedicado para tokens</translation>
</message> </message>
@ -284,7 +284,7 @@
<context> <context>
<name>OracleWizard</name> <name>OracleWizard</name>
<message> <message>
<location filename="src/oraclewizard.cpp" line="70"/> <location filename="src/oraclewizard.cpp" line="97"/>
<source>Oracle Importer</source> <source>Oracle Importer</source>
<translation>Importador de Oracle</translation> <translation>Importador de Oracle</translation>
</message> </message>

View file

@ -276,7 +276,7 @@
<context> <context>
<name>OracleImporter</name> <name>OracleImporter</name>
<message> <message>
<location filename="src/oracleimporter.cpp" line="541"/> <location filename="src/oracleimporter.cpp" line="540"/>
<source>Dummy set containing tokens</source> <source>Dummy set containing tokens</source>
<translation>Nukk-komplekt mis sisaldab märgistusi</translation> <translation>Nukk-komplekt mis sisaldab märgistusi</translation>
</message> </message>
@ -284,7 +284,7 @@
<context> <context>
<name>OracleWizard</name> <name>OracleWizard</name>
<message> <message>
<location filename="src/oraclewizard.cpp" line="70"/> <location filename="src/oraclewizard.cpp" line="97"/>
<source>Oracle Importer</source> <source>Oracle Importer</source>
<translation>Oracle sissetooja</translation> <translation>Oracle sissetooja</translation>
</message> </message>

View file

@ -276,7 +276,7 @@
<context> <context>
<name>OracleImporter</name> <name>OracleImporter</name>
<message> <message>
<location filename="src/oracleimporter.cpp" line="541"/> <location filename="src/oracleimporter.cpp" line="540"/>
<source>Dummy set containing tokens</source> <source>Dummy set containing tokens</source>
<translation>Tokeneja sisältävä mallisetti</translation> <translation>Tokeneja sisältävä mallisetti</translation>
</message> </message>
@ -284,7 +284,7 @@
<context> <context>
<name>OracleWizard</name> <name>OracleWizard</name>
<message> <message>
<location filename="src/oraclewizard.cpp" line="70"/> <location filename="src/oraclewizard.cpp" line="97"/>
<source>Oracle Importer</source> <source>Oracle Importer</source>
<translation>Oracle-lataaja</translation> <translation>Oracle-lataaja</translation>
</message> </message>

View file

@ -276,7 +276,7 @@
<context> <context>
<name>OracleImporter</name> <name>OracleImporter</name>
<message> <message>
<location filename="src/oracleimporter.cpp" line="541"/> <location filename="src/oracleimporter.cpp" line="540"/>
<source>Dummy set containing tokens</source> <source>Dummy set containing tokens</source>
<translation>Fausse édition contenant les jetons</translation> <translation>Fausse édition contenant les jetons</translation>
</message> </message>
@ -284,7 +284,7 @@
<context> <context>
<name>OracleWizard</name> <name>OracleWizard</name>
<message> <message>
<location filename="src/oraclewizard.cpp" line="70"/> <location filename="src/oraclewizard.cpp" line="97"/>
<source>Oracle Importer</source> <source>Oracle Importer</source>
<translation>Importateur Oracle</translation> <translation>Importateur Oracle</translation>
</message> </message>

View file

@ -277,7 +277,7 @@ e pedine che verranno usate da Cockatrice.</translation>
<context> <context>
<name>OracleImporter</name> <name>OracleImporter</name>
<message> <message>
<location filename="src/oracleimporter.cpp" line="541"/> <location filename="src/oracleimporter.cpp" line="540"/>
<source>Dummy set containing tokens</source> <source>Dummy set containing tokens</source>
<translation>Set finto contenente i token</translation> <translation>Set finto contenente i token</translation>
</message> </message>
@ -285,7 +285,7 @@ e pedine che verranno usate da Cockatrice.</translation>
<context> <context>
<name>OracleWizard</name> <name>OracleWizard</name>
<message> <message>
<location filename="src/oraclewizard.cpp" line="70"/> <location filename="src/oraclewizard.cpp" line="97"/>
<source>Oracle Importer</source> <source>Oracle Importer</source>
<translation>Oracle Importer</translation> <translation>Oracle Importer</translation>
</message> </message>

View file

@ -276,7 +276,7 @@
<context> <context>
<name>OracleImporter</name> <name>OracleImporter</name>
<message> <message>
<location filename="src/oracleimporter.cpp" line="541"/> <location filename="src/oracleimporter.cpp" line="540"/>
<source>Dummy set containing tokens</source> <source>Dummy set containing tokens</source>
<translation></translation> <translation></translation>
</message> </message>
@ -284,7 +284,7 @@
<context> <context>
<name>OracleWizard</name> <name>OracleWizard</name>
<message> <message>
<location filename="src/oraclewizard.cpp" line="70"/> <location filename="src/oraclewizard.cpp" line="97"/>
<source>Oracle Importer</source> <source>Oracle Importer</source>
<translation>Oracle Importer - </translation> <translation>Oracle Importer - </translation>
</message> </message>

View file

@ -276,7 +276,7 @@
<context> <context>
<name>OracleImporter</name> <name>OracleImporter</name>
<message> <message>
<location filename="src/oracleimporter.cpp" line="541"/> <location filename="src/oracleimporter.cpp" line="540"/>
<source>Dummy set containing tokens</source> <source>Dummy set containing tokens</source>
<translation> </translation> <translation> </translation>
</message> </message>
@ -284,7 +284,7 @@
<context> <context>
<name>OracleWizard</name> <name>OracleWizard</name>
<message> <message>
<location filename="src/oraclewizard.cpp" line="70"/> <location filename="src/oraclewizard.cpp" line="97"/>
<source>Oracle Importer</source> <source>Oracle Importer</source>
<translation></translation> <translation></translation>
</message> </message>

View file

@ -276,7 +276,7 @@
<context> <context>
<name>OracleImporter</name> <name>OracleImporter</name>
<message> <message>
<location filename="src/oracleimporter.cpp" line="541"/> <location filename="src/oracleimporter.cpp" line="540"/>
<source>Dummy set containing tokens</source> <source>Dummy set containing tokens</source>
<translation>Dummy sett som inneholder tokens</translation> <translation>Dummy sett som inneholder tokens</translation>
</message> </message>
@ -284,7 +284,7 @@
<context> <context>
<name>OracleWizard</name> <name>OracleWizard</name>
<message> <message>
<location filename="src/oraclewizard.cpp" line="70"/> <location filename="src/oraclewizard.cpp" line="97"/>
<source>Oracle Importer</source> <source>Oracle Importer</source>
<translation>Oracle importerer</translation> <translation>Oracle importerer</translation>
</message> </message>

View file

@ -276,7 +276,7 @@
<context> <context>
<name>OracleImporter</name> <name>OracleImporter</name>
<message> <message>
<location filename="src/oracleimporter.cpp" line="541"/> <location filename="src/oracleimporter.cpp" line="540"/>
<source>Dummy set containing tokens</source> <source>Dummy set containing tokens</source>
<translation>Token voorbeeldset</translation> <translation>Token voorbeeldset</translation>
</message> </message>
@ -284,7 +284,7 @@
<context> <context>
<name>OracleWizard</name> <name>OracleWizard</name>
<message> <message>
<location filename="src/oraclewizard.cpp" line="70"/> <location filename="src/oraclewizard.cpp" line="97"/>
<source>Oracle Importer</source> <source>Oracle Importer</source>
<translation>Oracle importer</translation> <translation>Oracle importer</translation>
</message> </message>

View file

@ -276,7 +276,7 @@
<context> <context>
<name>OracleImporter</name> <name>OracleImporter</name>
<message> <message>
<location filename="src/oracleimporter.cpp" line="541"/> <location filename="src/oracleimporter.cpp" line="540"/>
<source>Dummy set containing tokens</source> <source>Dummy set containing tokens</source>
<translation>Dodatek-atrapa, zawierający tokeny.</translation> <translation>Dodatek-atrapa, zawierający tokeny.</translation>
</message> </message>
@ -284,7 +284,7 @@
<context> <context>
<name>OracleWizard</name> <name>OracleWizard</name>
<message> <message>
<location filename="src/oraclewizard.cpp" line="70"/> <location filename="src/oraclewizard.cpp" line="97"/>
<source>Oracle Importer</source> <source>Oracle Importer</source>
<translation>Oracle kreator importu</translation> <translation>Oracle kreator importu</translation>
</message> </message>

View file

@ -276,7 +276,7 @@
<context> <context>
<name>OracleImporter</name> <name>OracleImporter</name>
<message> <message>
<location filename="src/oracleimporter.cpp" line="541"/> <location filename="src/oracleimporter.cpp" line="540"/>
<source>Dummy set containing tokens</source> <source>Dummy set containing tokens</source>
<translation>Set básico contendo fichas</translation> <translation>Set básico contendo fichas</translation>
</message> </message>
@ -284,7 +284,7 @@
<context> <context>
<name>OracleWizard</name> <name>OracleWizard</name>
<message> <message>
<location filename="src/oraclewizard.cpp" line="70"/> <location filename="src/oraclewizard.cpp" line="97"/>
<source>Oracle Importer</source> <source>Oracle Importer</source>
<translation>Importar Oracle</translation> <translation>Importar Oracle</translation>
</message> </message>

View file

@ -276,7 +276,7 @@
<context> <context>
<name>OracleImporter</name> <name>OracleImporter</name>
<message> <message>
<location filename="src/oracleimporter.cpp" line="541"/> <location filename="src/oracleimporter.cpp" line="540"/>
<source>Dummy set containing tokens</source> <source>Dummy set containing tokens</source>
<translation>Esta expansão contém fichas.</translation> <translation>Esta expansão contém fichas.</translation>
</message> </message>
@ -284,7 +284,7 @@
<context> <context>
<name>OracleWizard</name> <name>OracleWizard</name>
<message> <message>
<location filename="src/oraclewizard.cpp" line="70"/> <location filename="src/oraclewizard.cpp" line="97"/>
<source>Oracle Importer</source> <source>Oracle Importer</source>
<translation>Importador Oracle</translation> <translation>Importador Oracle</translation>
</message> </message>

View file

@ -276,7 +276,7 @@
<context> <context>
<name>OracleImporter</name> <name>OracleImporter</name>
<message> <message>
<location filename="src/oracleimporter.cpp" line="541"/> <location filename="src/oracleimporter.cpp" line="540"/>
<source>Dummy set containing tokens</source> <source>Dummy set containing tokens</source>
<translation>Пример сета с фишками</translation> <translation>Пример сета с фишками</translation>
</message> </message>
@ -284,7 +284,7 @@
<context> <context>
<name>OracleWizard</name> <name>OracleWizard</name>
<message> <message>
<location filename="src/oraclewizard.cpp" line="70"/> <location filename="src/oraclewizard.cpp" line="97"/>
<source>Oracle Importer</source> <source>Oracle Importer</source>
<translation>Импортер Oracle</translation> <translation>Импортер Oracle</translation>
</message> </message>

View file

@ -276,7 +276,7 @@
<context> <context>
<name>OracleImporter</name> <name>OracleImporter</name>
<message> <message>
<location filename="src/oracleimporter.cpp" line="541"/> <location filename="src/oracleimporter.cpp" line="540"/>
<source>Dummy set containing tokens</source> <source>Dummy set containing tokens</source>
<translation type="unfinished"/> <translation type="unfinished"/>
</message> </message>
@ -284,7 +284,7 @@
<context> <context>
<name>OracleWizard</name> <name>OracleWizard</name>
<message> <message>
<location filename="src/oraclewizard.cpp" line="70"/> <location filename="src/oraclewizard.cpp" line="97"/>
<source>Oracle Importer</source> <source>Oracle Importer</source>
<translation type="unfinished"/> <translation type="unfinished"/>
</message> </message>

View file

@ -276,7 +276,7 @@
<context> <context>
<name>OracleImporter</name> <name>OracleImporter</name>
<message> <message>
<location filename="src/oracleimporter.cpp" line="541"/> <location filename="src/oracleimporter.cpp" line="540"/>
<source>Dummy set containing tokens</source> <source>Dummy set containing tokens</source>
<translation type="unfinished"/> <translation type="unfinished"/>
</message> </message>
@ -284,7 +284,7 @@
<context> <context>
<name>OracleWizard</name> <name>OracleWizard</name>
<message> <message>
<location filename="src/oraclewizard.cpp" line="70"/> <location filename="src/oraclewizard.cpp" line="97"/>
<source>Oracle Importer</source> <source>Oracle Importer</source>
<translation type="unfinished"/> <translation type="unfinished"/>
</message> </message>

View file

@ -276,7 +276,7 @@
<context> <context>
<name>OracleImporter</name> <name>OracleImporter</name>
<message> <message>
<location filename="src/oracleimporter.cpp" line="541"/> <location filename="src/oracleimporter.cpp" line="540"/>
<source>Dummy set containing tokens</source> <source>Dummy set containing tokens</source>
<translation></translation> <translation></translation>
</message> </message>
@ -284,7 +284,7 @@
<context> <context>
<name>OracleWizard</name> <name>OracleWizard</name>
<message> <message>
<location filename="src/oraclewizard.cpp" line="70"/> <location filename="src/oraclewizard.cpp" line="97"/>
<source>Oracle Importer</source> <source>Oracle Importer</source>
<translation>Oracle導入器</translation> <translation>Oracle導入器</translation>
</message> </message>

View file

@ -276,7 +276,7 @@
<context> <context>
<name>OracleImporter</name> <name>OracleImporter</name>
<message> <message>
<location filename="src/oracleimporter.cpp" line="541"/> <location filename="src/oracleimporter.cpp" line="540"/>
<source>Dummy set containing tokens</source> <source>Dummy set containing tokens</source>
<translation></translation> <translation></translation>
</message> </message>
@ -284,7 +284,7 @@
<context> <context>
<name>OracleWizard</name> <name>OracleWizard</name>
<message> <message>
<location filename="src/oraclewizard.cpp" line="70"/> <location filename="src/oraclewizard.cpp" line="97"/>
<source>Oracle Importer</source> <source>Oracle Importer</source>
<translation>Oracle导入器</translation> <translation>Oracle导入器</translation>
</message> </message>

View file

@ -65,4 +65,5 @@ target_link_libraries(
add_subdirectory(card_zone_algorithms) add_subdirectory(card_zone_algorithms)
add_subdirectory(carddatabase) add_subdirectory(carddatabase)
add_subdirectory(loading_from_clipboard) add_subdirectory(loading_from_clipboard)
add_subdirectory(movecard_tests)
add_subdirectory(oracle) add_subdirectory(oracle)

View file

@ -0,0 +1,16 @@
add_executable(reverse_card_move_test reverse_card_move_test.cpp)
if(NOT GTEST_FOUND)
add_dependencies(reverse_card_move_test gtest)
endif()
target_link_libraries(
reverse_card_move_test
PRIVATE libcockatrice_network_server_remote
PRIVATE libcockatrice_rng
PRIVATE Threads::Threads
PRIVATE ${GTEST_BOTH_LIBRARIES}
PRIVATE ${TEST_QT_MODULES}
)
add_test(NAME reverse_card_move_test COMMAND reverse_card_move_test)

View file

@ -0,0 +1,91 @@
#include "game/server_abstract_player.h"
#include "game/server_card.h"
#include "game/server_cardzone.h"
#include "game/server_game.h"
#include "server_response_containers.h"
#include "server_room.h"
#include "server_test_helpers.h"
#include <gtest/gtest.h>
#include <libcockatrice/protocol/pb/command_move_card.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
#include <libcockatrice/rng/rng_abstract.h>
#include <libcockatrice/utility/zone_names.h>
RNG_Abstract *rng = nullptr; // this needs to be defined due to other functions in server
TEST(ReverseCardMoveTest, MoveCardFromBottomTest)
{
ServerInfo_User user;
user.set_name("test-user");
// instantiate a fake server instance
FakeServer server;
Server_Room room(0, 0, "", "", "", "", false, "", {}, &server);
Server_Game game(user, 1, "", "", 2, QList<int>(), false, false, false, false, false, false, 20, false, &room);
Server_AbstractPlayer player(&game, 1, user, false, nullptr);
Server_CardZone deckZone(&player, ZoneNames::DECK, true, ServerInfo_Zone::PublicZone);
Server_CardZone exileZone(&player, ZoneNames::EXILE, true, ServerInfo_Zone::PublicZone);
// setup the deck with 20 useless cards
for (int i = 0; i < 20; i++) {
auto *cardUseless = new Server_Card({"Card Useless", "card-Useless"}, player.newCardId(), i, 0);
deckZone.insertCard(cardUseless, i, 0);
}
// add 4 cards to the end of it
auto *cardA = new Server_Card({"Card A", "card-a"}, player.newCardId(), 20, 0);
auto *cardB = new Server_Card({"Card B", "card-b"}, player.newCardId(), 21, 0);
auto *cardC = new Server_Card({"Card C", "card-c"}, player.newCardId(), 22, 0);
auto *cardD = new Server_Card({"Card D", "card-d"}, player.newCardId(), 23, 0);
deckZone.insertCard(cardA, 20, 0);
deckZone.insertCard(cardB, 21, 0);
deckZone.insertCard(cardC, 22, 0);
deckZone.insertCard(cardD, 23, 0);
// try to move them, with the expected client given order (n-3, n-2, n-1, n)
CardToMove moveA;
moveA.set_card_id(cardA->getId());
CardToMove moveB;
moveB.set_card_id(cardB->getId());
CardToMove moveC;
moveC.set_card_id(cardC->getId());
CardToMove moveD;
moveD.set_card_id(cardD->getId());
QList<const CardToMove *> cardsToMove = {&moveA, &moveB, &moveC, &moveD};
GameEventStorage ges;
const auto response = player.moveCard(ges, &deckZone, cardsToMove, &exileZone, 0, 0, false, false, false);
EXPECT_EQ(response, Response::RespOk);
int positionA;
int positionB;
int positionC;
int positionD;
// find the cards in the destination zone and check they are the right card
EXPECT_EQ(exileZone.getCard(cardA->getId(), &positionA), cardA);
EXPECT_EQ(exileZone.getCard(cardB->getId(), &positionB), cardB);
EXPECT_EQ(exileZone.getCard(cardC->getId(), &positionC), cardC);
EXPECT_EQ(exileZone.getCard(cardD->getId(), &positionD), cardD);
// check that they are at the expected index
EXPECT_EQ(cardA->getX(), 3);
EXPECT_EQ(cardB->getX(), 2);
EXPECT_EQ(cardC->getX(), 1);
EXPECT_EQ(cardD->getX(), 0);
// also check if the given positions are correct
EXPECT_EQ(positionA, 3);
EXPECT_EQ(positionB, 2);
EXPECT_EQ(positionC, 1);
EXPECT_EQ(positionD, 0);
}
int main(int argc, char **argv)
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

View file

@ -0,0 +1,42 @@
#include "server.h"
#include "server_database_interface.h"
class MockDatabaseInterface : public Server_DatabaseInterface
{
public:
AuthenticationResult checkUserPassword(Server_ProtocolHandler *,
const QString &,
const QString &,
const QString &,
QString &,
int &,
bool) override
{
return NotLoggedIn;
}
ServerInfo_User getUserData(const QString &, bool) override
{
return ServerInfo_User();
}
int getNextGameId() override
{
return 1;
}
int getNextReplayId() override
{
return 1;
}
int getActiveUserCount(QString) override
{
return 1;
}
};
class FakeServer : public Server
{
public:
FakeServer()
{
setDatabaseInterface(new MockDatabaseInterface());
}
};