diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index 2b9672882..000000000 --- a/.appveyor.yml +++ /dev/null @@ -1,118 +0,0 @@ -version: build {build} - -# Skipping commits affecting specific files (GitHub only). -# More details here: https://www.appveyor.com/docs/appveyor-yml and https://www.appveyor.com/docs/how-to/filtering-commits -skip_commits: - files: - - .ci/travis-* - - .github/ - - .tx/ - - webclient/ - - .clang-format - - .*ignore - - .codacy.yml - - .travis.yml - - '**/*.md' - - Dockerfile - - LICENSE - -skip_branch_with_pr: true - -clone_depth: 15 - -image: Visual Studio 2017 - -cache: - - c:\Tools\vcpkg\installed - -environment: - matrix: - - target_arch: win64 - qt_ver: 5.12\msvc2017_64 - cmake_generator: Visual Studio 15 2017 Win64 - cmake_toolset: v141,host=x64 - vcpkg_arch: x64 - - - target_arch: win32 - qt_ver: 5.12\msvc2017 - cmake_generator: Visual Studio 15 2017 - cmake_toolset: v141 - vcpkg_arch: x86 - -install: - - cd C:\Tools\vcpkg - - git pull -q - - .\bootstrap-vcpkg.bat - - cd %APPVEYOR_BUILD_FOLDER% - - vcpkg remove --outdated --recurse - - vcpkg install openssl protobuf liblzma zlib gtest --triplet %vcpkg_arch%-windows - -services: - - mysql - -build_script: - - ps: | - New-Item -ItemType directory -Path $env:APPVEYOR_BUILD_FOLDER\build - Set-Location -Path $env:APPVEYOR_BUILD_FOLDER\build - $vcpkgbindir = "C:\Tools\vcpkg\installed\$env:vcpkg_arch-windows\bin" - $mysqldll = "c:\Program Files\MySQL\MySQL Server 5.7\lib\libmysql.dll" - cmake --version - cmake .. -G "$env:cmake_generator" -T "$env:cmake_toolset" "-DCMAKE_C_FLAGS=/MP" "-DCMAKE_CXX_FLAGS=/MP" "-DCMAKE_TOOLCHAIN_FILE=c:/tools/vcpkg/scripts/buildsystems/vcpkg.cmake" "-DCMAKE_PREFIX_PATH=c:/Qt/$env:qt_ver;$vcpkgbindir" "-DWITH_SERVER=1" "-DMYSQLCLIENT_LIBRARIES=$mysqldll" "-DTEST=1" - - msbuild PACKAGE.vcxproj /p:Configuration=Release /logger:"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll" /m - - ps: | - $exe = dir -name *.exe - $new_name = $exe.Replace(".exe", "-${env:target_arch}.exe") - Push-AppveyorArtifact $exe -FileName $new_name - $cmake_name = $exe.Replace(".exe", "-${env:target_arch}.cmake.txt") - Push-AppveyorArtifact CMakeCache.txt -FileName $cmake_name - $json = New-Object PSObject - (New-Object PSObject | Add-Member -PassThru NoteProperty bin $new_name | Add-Member -PassThru NoteProperty cmake $cmake_name | Add-Member -PassThru NoteProperty commit $env:APPVEYOR_REPO_COMMIT) | ConvertTo-JSON | Out-File -FilePath "latest-$env:target_arch" -Encoding ASCII - Push-AppveyorArtifact "latest-$env:target_arch" - $version = $matches['content'] - -test_script: - - ps: | - $env:Path += ";c:/Qt/$env:qt_ver/bin" - ctest -T Test -C Release - $ctest_success = $? - $XSLInputElement = New-Object System.Xml.Xsl.XslCompiledTransform - $XSLInputElement.Load("https://raw.githubusercontent.com/rpavlik/jenkins-ctest-plugin/master/ctest-to-junit.xsl") - $XSLInputElement.Transform((Resolve-Path .\Testing\*\Test.xml), (Join-Path (Resolve-Path .) "ctest-to-junit-results.xml")) - $wc = New-Object 'System.Net.WebClient' - $wc.UploadFile("https://ci.appveyor.com/api/testresults/junit/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\ctest-to-junit-results.xml)) - if (-not $ctest_success) { throw "Tests failed" } - - -# Builds for pull requests skip the deployment step altogether -deploy: -# Deploy configuration for "beta" releases - - provider: GitHub - auth_token: - secure: z8Xh1lSCYtvs0SUfhOK6AijCFk0Rgf5jAxu7QvBByR42NG1SxFHPOmyrOllkfy1u - tag: "$(APPVEYOR_REPO_TAG_NAME)" - release: "Cockatrice $(APPVEYOR_REPO_TAG_NAME)" - description: "Beta release of Cockatrice" - artifact: /.*\.exe/ - draft: false - prerelease: true - on: - APPVEYOR_REPO_TAG: true - APPVEYOR_REPO_TAG_NAME: /([0-9]|[1-9][0-9])(\.([0-9]|[1-9][0-9])){2}-beta(\.([2-9]|[1-9][0-9]))?$/ # regex to match semver naming convention for beta pre-releases - -# Deploy configuration for "stable" releases - - provider: GitHub - auth_token: - secure: z8Xh1lSCYtvs0SUfhOK6AijCFk0Rgf5jAxu7QvBByR42NG1SxFHPOmyrOllkfy1u - tag: "$(APPVEYOR_REPO_TAG_NAME)" - release: "Cockatrice $(APPVEYOR_REPO_TAG_NAME)" - artifact: /.*\.exe/ - draft: false - prerelease: false - on: - APPVEYOR_REPO_TAG: true - APPVEYOR_REPO_TAG_NAME: /([0-9]|[1-9][0-9])(\.([0-9]|[1-9][0-9])){2}$/ # regex to match semver naming convention for stable full releases - - -# Announcements of build image updates: https://www.appveyor.com/updates/ -# Official validator for ".appveyor.yml" config file: https://ci.appveyor.com/tools/validate-yaml -# AppVeyor config documentation: https://www.appveyor.com/docs/build-configuration/ diff --git a/.ci/ArchLinux/Dockerfile b/.ci/ArchLinux/Dockerfile index b9f96e291..308854ebd 100644 --- a/.ci/ArchLinux/Dockerfile +++ b/.ci/ArchLinux/Dockerfile @@ -5,6 +5,7 @@ RUN pacman --sync --refresh --sysupgrade --needed --noconfirm \ ccache \ cmake \ git \ + mariadb-libs \ protobuf \ qt5-base \ qt5-multimedia \ diff --git a/.ci/DebianBuster/Dockerfile b/.ci/DebianBuster/Dockerfile index f81a08ac0..82109e70c 100644 --- a/.ci/DebianBuster/Dockerfile +++ b/.ci/DebianBuster/Dockerfile @@ -9,6 +9,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ g++ \ git \ liblzma-dev \ + libmariadb-dev-compat \ libprotobuf-dev \ libqt5multimedia5-plugins \ libqt5sql5-mysql \ diff --git a/.ci/Fedora32/Dockerfile b/.ci/Fedora33/Dockerfile similarity index 91% rename from .ci/Fedora32/Dockerfile rename to .ci/Fedora33/Dockerfile index a6f8d5829..ee11d2548 100644 --- a/.ci/Fedora32/Dockerfile +++ b/.ci/Fedora33/Dockerfile @@ -1,4 +1,4 @@ -FROM fedora:32 +FROM fedora:33 RUN dnf install -y \ @development-tools \ @@ -10,6 +10,7 @@ RUN dnf install -y \ git \ hicolor-icon-theme \ libappstream-glib \ + mariadb-devel \ protobuf-devel \ qt5-{qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \ rpm-build \ diff --git a/.ci/Fedora34/Dockerfile b/.ci/Fedora34/Dockerfile new file mode 100644 index 000000000..ed2d65818 --- /dev/null +++ b/.ci/Fedora34/Dockerfile @@ -0,0 +1,21 @@ +FROM fedora:34 + +RUN dnf install -y \ + @development-tools \ + ccache \ + cmake \ + desktop-file-utils \ + file \ + gcc-c++ \ + git \ + hicolor-icon-theme \ + libappstream-glib \ + mariadb-devel \ + protobuf-devel \ + qt5-{qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \ + rpm-build \ + sqlite-devel \ + wget \ + xz-devel \ + zlib-devel \ + && dnf clean all diff --git a/.ci/UbuntuBionic/Dockerfile b/.ci/UbuntuBionic/Dockerfile index 459ed0d61..10f340fdf 100644 --- a/.ci/UbuntuBionic/Dockerfile +++ b/.ci/UbuntuBionic/Dockerfile @@ -9,6 +9,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ g++ \ git \ liblzma-dev \ + libmariadb-dev-compat \ libprotobuf-dev \ libqt5multimedia5-plugins \ libqt5sql5-mysql \ diff --git a/.ci/UbuntuFocal/Dockerfile b/.ci/UbuntuFocal/Dockerfile index 1bd81271b..1b6b47c82 100644 --- a/.ci/UbuntuFocal/Dockerfile +++ b/.ci/UbuntuFocal/Dockerfile @@ -10,6 +10,7 @@ RUN apt-get update && \ g++ \ git \ liblzma-dev \ + libmariadb-dev-compat \ libprotobuf-dev \ libqt5multimedia5-plugins \ libqt5sql5-mysql \ diff --git a/.ci/UbuntuGroovy/Dockerfile b/.ci/UbuntuGroovy/Dockerfile new file mode 100644 index 000000000..92528ba3f --- /dev/null +++ b/.ci/UbuntuGroovy/Dockerfile @@ -0,0 +1,25 @@ +FROM ubuntu:groovy + +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + build-essential \ + ccache \ + clang-format \ + cmake \ + file \ + g++ \ + git \ + liblzma-dev \ + libmariadb-dev-compat \ + libprotobuf-dev \ + libqt5multimedia5-plugins \ + libqt5sql5-mysql \ + libqt5svg5-dev \ + libqt5websockets5-dev \ + protobuf-compiler \ + qt5-default \ + qtmultimedia5-dev \ + qttools5-dev \ + qttools5-dev-tools \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* diff --git a/.ci/UbuntuHirsute/Dockerfile b/.ci/UbuntuHirsute/Dockerfile new file mode 100644 index 000000000..e6d8998b4 --- /dev/null +++ b/.ci/UbuntuHirsute/Dockerfile @@ -0,0 +1,24 @@ +FROM ubuntu:hirsute + +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + build-essential \ + ccache \ + clang-format \ + cmake \ + file \ + g++ \ + git \ + liblzma-dev \ + libmariadb-dev-compat \ + libprotobuf-dev \ + libqt5multimedia5-plugins \ + libqt5sql5-mysql \ + libqt5svg5-dev \ + libqt5websockets5-dev \ + protobuf-compiler \ + qtmultimedia5-dev \ + qttools5-dev \ + qttools5-dev-tools \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* diff --git a/.ci/compile.sh b/.ci/compile.sh new file mode 100755 index 000000000..82c7ebca6 --- /dev/null +++ b/.ci/compile.sh @@ -0,0 +1,142 @@ +#!/bin/bash + +# This script is to be used by the ci environment from the project root directory, do not use it from somewhere else. + +# Read arguments +while [[ "$@" ]]; do + case "$1" in + '--') + shift + ;; + '--format') + CHECK_FORMAT=1 + shift + ;; + '--install') + MAKE_INSTALL=1 + shift + ;; + '--package') + MAKE_PACKAGE=1 + shift + if [[ $# != 0 && $1 != -* ]]; then + PACKAGE_TYPE="$1" + shift + fi + ;; + '--suffix') + shift + if [[ $# == 0 ]]; then + echo "::error file=$0::--suffix expects an argument" + exit 1 + fi + PACKAGE_SUFFIX="$1" + shift + ;; + '--server') + MAKE_SERVER=1 + shift + ;; + '--test') + MAKE_TEST=1 + shift + ;; + '--debug') + BUILDTYPE="Debug" + shift + ;; + '--release') + BUILDTYPE="Release" + shift + ;; + *) + if [[ $1 == -* ]]; then + echo "::error file=$0::unrecognized option: $1" + exit 3 + fi + BUILDTYPE="$1" + shift + ;; + esac +done + +# Check formatting using clang-format +if [[ $CHECK_FORMAT ]]; then + echo "::group::Run linter" + source ./.ci/lint.sh + echo "::endgroup::" +fi + +set -e + +# Setup +./servatrice/check_schema_version.sh +mkdir -p build +cd build + +if [[ ! $CMAKE_BUILD_PARALLEL_LEVEL ]]; then + CMAKE_BUILD_PARALLEL_LEVEL=2 # default machines have 2 cores +fi + +# Add cmake flags +if [[ $MAKE_SERVER ]]; then + flags+=" -DWITH_SERVER=1" +fi +if [[ $MAKE_TEST ]]; then + flags+=" -DTEST=1" +fi +if [[ $BUILDTYPE ]]; then + flags+=" -DCMAKE_BUILD_TYPE=$BUILDTYPE" +fi +if [[ $PACKAGE_TYPE ]]; then + flags+=" -DCPACK_GENERATOR=$PACKAGE_TYPE" +fi + +if [[ $(uname) == "Darwin" ]]; then + # prepend ccache compiler binaries to path + PATH="/usr/local/opt/ccache/libexec:$PATH" + # Add qt install location when using homebrew + flags+=" -DCMAKE_PREFIX_PATH=/usr/local/opt/qt5/" +fi + +# Compile +echo "::group::Show ccache stats" +ccache --show-stats +echo "::endgroup::" + +echo "::group::Configure cmake" +cmake --version +cmake .. $flags +echo "::endgroup::" + +echo "::group::Build project" +cmake --build . +echo "::endgroup::" + +echo "::group::Show ccache stats again" +ccache --show-stats +echo "::endgroup::" + +if [[ $MAKE_TEST ]]; then + echo "::group::Run tests" + cmake --build . --target test + echo "::endgroup::" +fi + +if [[ $MAKE_INSTALL ]]; then + echo "::group::Install" + cmake --build . --target install + echo "::endgroup::" +fi + +if [[ $MAKE_PACKAGE ]]; then + echo "::group::Create package" + cmake --build . --target package + echo "::endgroup::" + + if [[ $PACKAGE_SUFFIX ]]; then + echo "::group::Update package name" + ../.ci/name_build.sh "$PACKAGE_SUFFIX" + echo "::endgroup::" + fi +fi diff --git a/.ci/travis-docker.sh b/.ci/docker.sh similarity index 88% rename from .ci/travis-docker.sh rename to .ci/docker.sh index 3ba66b40b..337e4fc36 100644 --- a/.ci/travis-docker.sh +++ b/.ci/docker.sh @@ -1,6 +1,7 @@ #!/bin/bash -# This script is to be sourced in .travis.yaml from the project root directory, do not use it from somewhere else. +# This script is to be used by the ci environment from the project root directory, do not use it from somewhere else. + # Creates or loads docker images to use in compilation, creates RUN function to start compilation on the docker image. # --get loads the image from a previously saved image cache, will build if no image is found # --build builds the image from the Dockerfile in .ci/$NAME @@ -9,7 +10,7 @@ # uses env: NAME CACHE BUILD GET SAVE (correspond to args: --set-cache --build --get --save) # sets env: RUN CCACHE_DIR IMAGE_NAME RUN_ARGS RUN_OPTS BUILD_SCRIPT # exitcode: 1 for failure, 2 for missing dockerfile, 3 for invalid arguments -export BUILD_SCRIPT=".ci/travis-compile.sh" +export BUILD_SCRIPT=".ci/compile.sh" project_name="cockatrice" save_extension=".tar.gz" @@ -56,7 +57,7 @@ if ! [[ $NAME ]]; then return 3 fi -export IMAGE_NAME="${project_name,,}_${NAME,,}" +export IMAGE_NAME="${project_name,,}_${NAME,,}" # lower case docker_dir=".ci/$NAME" if ! [[ -r $docker_dir/Dockerfile ]]; then @@ -64,10 +65,14 @@ if ! [[ -r $docker_dir/Dockerfile ]]; then return 2 # even if the image is cached, we do not want to run if there is no way to build this image fi -if ! [[ -d $CACHE ]]; then - echo "could not find cache dir: $CACHE" >&2 - unset CACHE +if ! [[ $CACHE ]]; then + echo "cache dir is not set!" >&2 else + if ! [[ -d $CACHE ]]; then + echo "could not find cache dir: $CACHE" >&2 + mkdir -p $CACHE + unset GET # the dir is empty + fi if [[ $GET || $SAVE ]]; then img_dir="$CACHE/$image_cache" img_save="$img_dir/$IMAGE_NAME$save_extension" @@ -128,7 +133,7 @@ function RUN () { echo "running image:" if docker images | grep "$IMAGE_NAME"; then - args="--mount type=bind,source=$(pwd),target=/src -w=/src" + args="--mount type=bind,source=$PWD,target=/src -w=/src" if [[ $CCACHE_DIR ]]; then args+=" --mount type=bind,source=$CCACHE_DIR,target=/.ccache -e CCACHE_DIR=/.ccache" fi diff --git a/.ci/travis-lint.sh b/.ci/lint.sh similarity index 78% rename from .ci/travis-lint.sh rename to .ci/lint.sh index 51033ba8e..e8939da9f 100755 --- a/.ci/travis-lint.sh +++ b/.ci/lint.sh @@ -1,9 +1,19 @@ #!/bin/bash -# Check formatting using clang-format +# fetch master branch +git fetch origin master + +# unshallow if needed +echo "Finding merge base" +if ! git merge-base origin/master HEAD; then + echo "Could not find merge base, unshallowing repo" + git fetch --unshallow +fi + +# Check formatting using clangify echo "Checking your code using clang-format..." -diff="$(./clangify.sh --diff --cf-version)" +diff="$(./clangify.sh --diff --cf-version --branch origin/master)" err=$? case $err in @@ -18,7 +28,7 @@ case $err in *** Then commit and push those changes to this branch. *** *** Check our CONTRIBUTING.md file for more details. *** *** *** -*** Thank you ❤️ *** +*** Thank you ❤️ *** *** *** *********************************************************** diff --git a/.ci/name_build.sh b/.ci/name_build.sh new file mode 100755 index 000000000..d322f7272 --- /dev/null +++ b/.ci/name_build.sh @@ -0,0 +1,50 @@ +#!/bin/bash +# used by the ci to rename build artifacts +# renames the file to [original name][SUFFIX].[original extension] +# where SUFFIX is either available in the environment or as the first arg +# if MAKE_ZIP is set instead a zip is made +# expected to be run in the build directory +builddir="." +findrx="Cockatrice-*.*" + +if [[ $1 ]]; then + SUFFIX="$1" +fi + +# check env +if [[ ! $SUFFIX ]]; then + echo "::error file=$0::SUFFIX is missing" + exit 2 +fi + +set -e + +# find file +found="$(find "$builddir" -maxdepth 1 -type f -name "$findrx" -print -quit)" +path="${found%/*}" # remove all after last / +file="${found##*/}" # remove all before last / +if [[ ! $file ]]; then + echo "::error file=$0::could not find package" + exit 1 +fi +if ! cd "$path"; then + echo "::error file=$0::could not get file path" + exit 1 +fi + +# set filename +name="${file%.*}" # remove all after last . +new_name="$name$SUFFIX." +if [[ $MAKE_ZIP ]]; then + filename="${new_name}zip" + echo "creating zip '$filename' from '$file'" + zip "$filename" "$file" +else + extension="${file##*.}" # remove all before last . + filename="$new_name$extension" + echo "renaming '$file' to '$filename'" + mv "$file" "$filename" +fi +ls -l "$PWD/$filename" +echo "::set-output name=path::$PWD/$filename" +echo "::set-output name=name::$filename" diff --git a/.ci/prep_release.sh b/.ci/prep_release.sh new file mode 100755 index 000000000..5bef26ee0 --- /dev/null +++ b/.ci/prep_release.sh @@ -0,0 +1,112 @@ +#!/bin/bash +# sets the properties of ci releases +# this doesn't have to be 100% foolproof +# the releases are first made as drafts and will be vetted by a human +# it just has to provide a template +# this requires the repo to be unshallowed +template_path=".ci/release_template.md" +body_path="/tmp/release.md" +beta_regex='beta' +name_regex='set\(GIT_TAG_RELEASENAME "([[:print:]]+)")' +whitespace='^\s*$' + +if [[ $1 ]]; then + TAG="$1" +fi + +# check env +if [[ ! $TAG ]]; then + echo "::error file=$0::TAG is missing" + exit 2 +fi + +# create title +if [[ $TAG =~ $beta_regex ]]; then + echo "::set-output name=is_beta::yes" + title="$TAG" + echo "creating beta release '$title'" +elif [[ ! $(cat CMakeLists.txt) =~ $name_regex ]]; then + echo "::error file=$0::could not find releasename in CMakeLists.txt" + exit 1 +else + echo "::set-output name=is_beta::no" + name="${BASH_REMATCH[1]}" + version="${TAG##*-}" + title="Cockatrice $version: $name" + no_beta=1 + echo "::set-output name=friendly_name::$name" + echo "creating full release '$title'" +fi +echo "::set-output name=title::$title" + +# add release notes template +if [[ $no_beta ]]; then + body="$(cat "$template_path")" + if [[ ! $body ]]; then + echo "::warning file=$0::could not find release template" + fi + body="${body//--REPLACE-WITH-RELEASE-TITLE--/$title}" +else + body="--REPLACE-WITH-COMMIT-COUNT-- commits have been included over the previous --REPLACE-WITH-PREVIOUS-RELEASE-TYPE-- + +
+show changes + +--REPLACE-WITH-GENERATED-LIST-- +
" +fi + +# add git log to release notes +all_tags=" +$(git tag)" # tags are ordered alphabetically +before="${all_tags%% +$TAG*}" # strip line with current tag an all lines after it +# note the extra newlines are needed to always have a last line +if [[ $all_tags == $before ]]; then + echo "::warning file=$0::could not find current tag" +else + while + previous="${before##* +}" # get the last line + # skip this tag if this is a full release and it's a beta or if empty + [[ $no_beta && $previous =~ $beta_regex || ! $previous ]] + do + beta_list+=" $previous" # add to list of skipped betas + next_before="${before% +*}" # strip the last line + if [[ $next_before == $before ]]; then + unset previous + break + fi + before="$next_before" + done + if [[ $previous ]]; then + if generated_list="$(git log "$previous..$TAG" --pretty="- %s")"; then + count="$(git rev-list --count "$previous..$TAG")" + [[ $previous =~ $beta_regex ]] && previousreleasetype="beta release" || previousreleasetype="full release" + echo "adding list of commits to release notes:" + echo "'$previous' to '$TAG' ($count commits)" + # --> is the markdown comment escape sequence, emojis are way better + generated_list="${generated_list//-->/→}" + body="${body//--REPLACE-WITH-GENERATED-LIST--/$generated_list}" + body="${body//--REPLACE-WITH-COMMIT-COUNT--/$count}" + body="${body//--REPLACE-WITH-PREVIOUS-RELEASE-TAG--/$previous}" + body="${body//--REPLACE-WITH-PREVIOUS-RELEASE-TYPE--/$previousreleasetype}" + if [[ $beta_list =~ $whitespace ]]; then + beta_list="-n there are no betas to delete!" + else + echo "the following betas should be deleted after publishing:" + echo "$beta_list" + fi + body="${body//--REPLACE-WITH-BETA-LIST--/$beta_list}" + else + echo "::warning file=$0::failed to produce git log" + fi + else + echo "::warning file=$0::could not find previous tag" + fi +fi + +# write to file +echo "::set-output name=body_path::$body_path" +echo "$body" >"$body_path" diff --git a/.ci/release_template.md b/.ci/release_template.md new file mode 100644 index 000000000..17cd83e93 --- /dev/null +++ b/.ci/release_template.md @@ -0,0 +1,94 @@ + + + + + +
+Pre-compiled binaries we serve:
+ - Windows 7/8/10 (32-bit)
+ - Windows 7/8/10 (64-bit)
+ - macOS 10.14 ("Mojave")
+ - macOS 10.15 ("Catalina")
+ - macOS 11.0 ("Big Sur")
+ - Ubuntu 18.04 ("Bionic Beaver")
+ - Ubuntu 20.04 ("Focal Fossa")
+ - Ubuntu 20.10 ("Groovy Gorilla")
+ - Ubuntu 21.04 ("Hirsute Hippo")
+ - Debian 10 ("Buster")
+ - Fedora 33
+ - Fedora 34
+We are also packaged in Arch Linux's official community repository, courtesy of @FFY00
+General linux support is available via a flatpak package (Flathub)
+
+ + +## General Notes + + +We're pleased to announce the newest official release: --REPLACE-WITH-RELEASE-TITLE-- + +We hope you enjoy the changes made and we have listed all changes, with their corresponding tickets, since the last version of Cockatrice was released for your convenience. + +If you ever encounter a bug, have a suggestion or idea, or feel a need for a developer to look into something, please feel free to [open a ticket](https://github.com/Cockatrice/Cockatrice/issues). ([How to create a GitHub Ticket for Cockatrice](https://github.com/Cockatrice/Cockatrice/wiki/How-to-Create-a-GitHub-Ticket-Regarding-Cockatrice)) + +For any information relating to Cockatrice, please take a look at our official site: **https://cockatrice.github.io** + +If you'd like to help contribute to Cockatrice in any way, check out our [README](https://github.com/Cockatrice/Cockatrice#get-involved-). We're always available to answer questions you may have on how the program works and how you can provide a meaningful contribution. + + +## Upgrading Cockatrice + + +- Run the internal software updater: Help → Check for Client Updates + +Don't forget to update your card database right after! (Help → Check for Card Updates...) + + +## Changelog + + + +### ⚠️ Important: +### ✨ New Features: +### 🐛 Fixed Bugs / Resolved issues: + + +
+ +📘 Show all changes (--REPLACE-WITH-COMMIT-COUNT-- commits) + + +### User Interface +### Under the Hood +### Oracle +### Servatrice +### Webatrice + +
+ + +## Translations +- **Thanks for over 300 people contributing to 20+ different languages up to now!** +- Without the help of the community we couldn't offer that great language support... keep up the good work! +- It's actually very easy to join and help for yourself - find out more here: + - [Help us Translate Cockatrice into your native language!](https://github.com/Cockatrice/Cockatrice/wiki/Translation-FAQ) + + +## Special Thanks + +We continue to find it amazing that so many people contribute their time, knowledge, code, testing and more to the project. We'd like to thank the entire Cockatrice community for their efforts. + diff --git a/.ci/travis-compile.sh b/.ci/travis-compile.sh deleted file mode 100755 index db461f946..000000000 --- a/.ci/travis-compile.sh +++ /dev/null @@ -1,128 +0,0 @@ -#!/bin/bash - -# This script is to be used in .travis.yaml from the project root directory, do not use it from somewhere else. - -# Read arguments -while [[ "$@" ]]; do - case "$1" in - '--format') - CHECK_FORMAT=1 - shift - ;; - '--install') - MAKE_INSTALL=1 - shift - ;; - '--package') - MAKE_PACKAGE=1 - shift - if [[ $# != 0 && $1 != -* ]]; then - PACKAGE_NAME="$1" - shift - if [[ $# != 0 && $1 != -* ]]; then - PACKAGE_TYPE="$1" - shift - fi - fi - ;; - '--server') - MAKE_SERVER=1 - shift - ;; - '--test') - MAKE_TEST=1 - shift - ;; - '--debug') - BUILDTYPE="Debug" - shift - ;; - '--release') - BUILDTYPE="Release" - shift - ;; - '--zip') - MAKE_ZIP=1 - shift - ;; - *) - if [[ $1 == -* ]]; then - echo "unrecognized option: $1" - exit 3 - fi - BUILDTYPE="$1" - shift - ;; - esac -done - -# Check formatting using clang-format -if [[ $CHECK_FORMAT ]]; then - source ./.ci/travis-lint.sh -fi - -set -e - -# Setup -./servatrice/check_schema_version.sh -mkdir -p build -cd build - -if ! [[ $CORE_AMOUNT ]]; then - CORE_AMOUNT="2" # travis machines have 2 cores -fi - -# Add cmake flags -if [[ $MAKE_SERVER ]]; then - flags+=" -DWITH_SERVER=1" -fi -if [[ $MAKE_TEST ]]; then - flags+=" -DTEST=1" - BUILDTYPE="Debug" # test requires buildtype Debug -fi -if [[ $BUILDTYPE ]]; then - flags+=" -DCMAKE_BUILD_TYPE=$BUILDTYPE" -fi -if [[ $PACKAGE_TYPE ]]; then - flags+=" -DCPACK_GENERATOR=$PACKAGE_TYPE" -fi - -# Add qt install location when using brew -if [[ $(uname) == "Darwin" ]]; then - PATH="/usr/local/opt/ccache/bin:$PATH" - flags+=" -DCMAKE_PREFIX_PATH=/usr/local/opt/qt5/" -fi - -# Compile -cmake --version -cmake .. $flags -make -j"$CORE_AMOUNT" - -if [[ $MAKE_TEST ]]; then - make test -fi - -if [[ $MAKE_INSTALL ]]; then - make install -fi - -if [[ $MAKE_PACKAGE ]]; then - make package - if [[ $PACKAGE_NAME ]]; then - found="$(find . -maxdepth 1 -type f -name "Cockatrice-*.*" -print -quit)" - path="${found%/*}" - file="${found##*/}" - if [[ ! $file ]]; then - echo "could not find package" >&2 - exit 1 - fi - new_name="$path/${file%.*}-$PACKAGE_NAME." - if [[ $MAKE_ZIP ]]; then - zip "${new_name}zip" "$path/$file" - mv "$path/$file" "$path/_$file" - else - extension="${file##*.}" - mv "$path/$file" "$new_name$extension" - fi - fi -fi diff --git a/.clang-format b/.clang-format index f2a5fa559..6347a2037 100644 --- a/.clang-format +++ b/.clang-format @@ -25,3 +25,7 @@ IndentCaseLabels: true PointerAlignment: Right SortIncludes: true IncludeBlocks: Regroup +--- +Language: Proto +AllowShortFunctionsOnASingleLine: None +SpacesInContainerLiterals: false diff --git a/.dockerignore b/.dockerignore index 075acd689..2abeb2727 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,7 +1,6 @@ .git/ build/ .github/ -.travis/ .tx/ cockatrice/ doc/ diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 41defe1ea..b8904bb09 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,4 +1,6 @@ -  [Introduction](#contributing-to-cockatrice) | [Code Style Guide](#code-style-guide) | [Translations](#translations) | [Release Management](#release-management) +  [Introduction](#contributing-to-cockatrice) | [Code Style Guide]( +#code-style-guide) | [Translations](#translations) | [Release Management]( +#release-management) ---- @@ -7,50 +9,89 @@ # Contributing to Cockatrice # First off, thanks for taking the time to contribute to our project! 🎉 ❤ ️✨ -The following is a set of guidelines for contributing to Cockatrice. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. +The following is a set of guidelines for contributing to Cockatrice. These are +mostly guidelines, not rules. Use your best judgment, and feel free to propose +changes to this document in a pull request. # Recommended Setups # -For those developers who like the Linux or MacOS environment, many of our developers like working with a nifty program called [CLion](https://www.jetbrains.com/clion/). The program's a great asset and one of the best tools you'll find on these systems, but you're welcomed to use any IDE you most enjoy. +For those developers who like the Linux or MacOS environment, many of our +developers like working with a nifty program called [CLion]( +https://www.jetbrains.com/clion/). The program's a great asset and one of the +best tools you'll find on these systems, but you're welcomed to use any IDE +you most enjoy. -Developers who like Windows development tend to find [Visual Studio](https://www.visualstudio.com/) the best tool for the job. +Developers who like Windows development tend to find [Visual Studio]( +https://www.visualstudio.com/) the best tool for the job. -If you have any questions on IDEs, feel free to chat with us on [Gitter](https://gitter.im/Cockatrice/Cockatrice) and we would love to help answer your questions! +[![Discord](https://img.shields.io/discord/314987288398659595?label=Discord&logo=discord&logoColor=white&color=7289da)](https://discord.gg/ZASRzKu) +[![Gitter Chat](https://img.shields.io/gitter/room/Cockatrice/Cockatrice.svg)](https://gitter.im/Cockatrice/Cockatrice) + +If you'd like to ask questions, get advice, or just want to say hi, +the Cockatrice Development Team uses [Discord](https://discord.gg/ZASRzKu) +for communications in the #dev channel. If you're not into Discord, we also +have a [Gitter](https://gitter.im/Cockatrice/Cockatrice) channel available, +albeit slightly less active. # Code Style Guide # -### Formatting and continuous integration (ci) ### +### Formatting and continuous integration (CI) ### -We currently use Travis CI to check your code for formatting issues, if your pull request was rejected because of this it would show a message in the logs. Click on "Details" next to the failed Travis CI build and then click on the failed build (most likely the fastest one) to see the log. +We use a separate job on the CI to check your code for formatting issues. If +your pull request failed the test, you can check the output on the checks tab. +It's the first job called "linter", you can click the "Run clangify" step to +see the output of the test. -The message will look somewhat similar to this: +The message will look like this: ``` -************************************************************ -*** Your code does not meet our formatting guidelines. *** -*** Please correct it then commit and push your changes. *** -*** See our CONTRIBUTING.md file for more information. *** -************************************************************ +*********************************************************** +*** *** +*** Your code does not comply with our style guide. *** +*** *** +*** Please correct it or run the "clangify.sh" script. *** +*** Then commit and push those changes to this branch. *** +*** Check our CONTRIBUTING.md file for more details. *** +*** *** +*** Thank you ❤️ *** +*** *** +*********************************************************** ``` -The CONTRIBUTING.md file mentioned is this file. Please read [this section](#Formatting) for full information on our formatting guidelines. +The CONTRIBUTING.md file mentioned in that message is the file you are +currently reading. Please see [this section](#formatting) below for full +information on our formatting guidelines. ### Compatibility ### -Cockatrice is currently compiled on all platforms using C++11. You'll notice C++03 code throughout the codebase. Please feel free to help convert it over! +Cockatrice is currently compiled on all platforms using C++11. +You'll notice C++03 code throughout the codebase. Please feel free +to help convert it over! -For consistency, we use Qt data structures where possible. For example, `QString` over -`std::string` and `QList` over `std::vector`. +For consistency, we use Qt data structures where possible. For example, +`QString` over `std::string` and `QList` over `std::vector`. + +Do not use old C style casts in new code, instead use a [`static_cast<>`]( +https://en.cppreference.com/w/cpp/language/static_cast) +or other appropriate conversion. ### Formatting ### -The handy tool `clang-format` can format your code for you, it is available for almost any environment. A special `.clang-format` configuration file is included in the project and is used to format your code. +The handy tool `clang-format` can format your code for you, it is available for +almost any environment. A special `.clang-format` configuration file is +included in the project and is used to format your code. -We've also included a bash script, `clangify.sh`, that will use clang-format to format all files in one go. Use `./clangify.sh --help` to show a full help page. +We've also included a bash script, `clangify.sh`, that will use clang-format to +format all files in your pr in one go. Use `./clangify.sh --help` to show a +full help page. -To run clang-format on a single source file simply use the command `clang-format -i ` to format it in place. (some systems install clang-format with a specific version number appended, `find /usr/bin -name clang-format*` should find it for you) +To run clang-format on a single source file simply use the command +`clang-format -i ` to format it in place. (Some systems install +clang-format with a specific version number appended, +`find /usr/bin -name clang-format*` should find it for you) -See [the clang-format documentation](https://clang.llvm.org/docs/ClangFormat.html) for more information about the tool. +See [the clang-format documentation]( +https://clang.llvm.org/docs/ClangFormat.html) for more information about the tool. #### Header files #### @@ -62,7 +103,8 @@ Use header guards in the form of `FILE_NAME_H`. Simple functions, such as getters, may be written inline in the header file, but other functions should be written in the source file. -Group library includes after project includes, and in alphabetic order. Like this: +Group project includes first, followed by library includes. All in alphabetic order. +Like this: ```c++ // Good #include "card.h" @@ -88,9 +130,15 @@ Group library includes after project includes, and in alphabetic order. Like thi Use `UpperCamelCase` for classes, structs, enums, etc. and `lowerCamelCase` for function and variable names. -Member variables aren't decorated in any way. Don't prefix or suffix with +Don't use [Hungarian Notation]( +https://en.wikipedia.org/wiki/Hungarian_notation). + +Member variables aren't decorated in any way. Don't prefix or suffix them with underscores, etc. +Use a separate line for each declaration, don't use a single line like this +`int one = 1, two = 2` and instead split them into two lines. + For arguments to constructors which have the same names as member variables, prefix those arguments with underscores: ```c++ @@ -115,7 +163,8 @@ If you find any usage of the old keywords, we encourage you to fix it. #### Braces #### -Braces should go on their own line except for control statements, the use of braces around single line statements is preferred. +Braces should go on their own line except for control statements, the use of +braces around single line statements is preferred. See the following example: ```c++ int main() @@ -136,17 +185,25 @@ int main() #### Indentation and Spacing #### -Always indent using 4 spaces, do not use tabs. Opening and closing braces should be on the same indentation layer, member access specifiers in classes or structs should not be indented. +Always indent using 4 spaces, do not use tabs. Opening and closing braces +should be on the same indentation layer, member access specifiers in classes or +structs should not be indented. -All operators and braces should be separated by spaces, do not add a space next to the inside of a brace. +All operators and braces should be separated by spaces, do not add a space next +to the inside of a brace. -If multiple lines of code that follow eachother have single line comments behind them, place all of them on the same indentation level. This indentation level should be equal to the longest line of code for each of these comments, without added spacing. +If multiple lines of code that follow eachother have single line comments +behind them, place all of them on the same indentation level. This indentation +level should be equal to the longest line of code for each of these comments, +without added spacing. #### Lines #### -Do not have trailing whitespace in your lines. Most IDEs check for this nowadays and clean it up for you. +Do not leave trailing whitespace on any line. Most IDEs check for this +nowadays and clean it up for you. -Lines should be 120 characters or less. Please break up lines that are too long into smaller parts, for example at spaces or after opening a brace. +Lines should be 120 characters or less. Please break up lines that are too long +into smaller parts, for example at spaces or after opening a brace. ### Memory Management ### @@ -182,44 +239,93 @@ as `QScopedPointer`, or, less preferably, `QSharedPointer`. The servatrice database's schema can be found at `servatrice/servatrice.sql`. Everytime the schema gets modified, some other steps are due: 1. Increment the value of `cockatrice_schema_version` in `servatrice.sql`; - 2. Increment the value of `DATABASE_SCHEMA_VERSION` in `servatrice_database_interface.h` accordingly; - 3. Create a new migration file inside the `servatrice/migrations` directory named after the new schema version. - 4. Run the `servatrice/check_schema_version.sh` script to ensure everything is fine. + 2. Increment the value of `DATABASE_SCHEMA_VERSION` in + `servatrice_database_interface.h` accordingly; + 3. Create a new migration file inside the `servatrice/migrations` directory + named after the new schema version. + 4. Run the `servatrice/check_schema_version.sh` script to ensure everything is + fine. -The migration file should include the sql statements needed to migrate the database schema and data from the previous to the new version, and an additional statement that updates `cockatrice_schema_version` to the correct value. +The migration file should include the sql statements needed to migrate the +database schema and data from the previous to the new version, and an +additional statement that updates `cockatrice_schema_version` to the correct +value. -Ensure that the migration produces the expected effects; e.g. if you add a new column, make sure the migration places it in the same order as servatrice.sql. +Ensure that the migration produces the expected effects; e.g. if you add a +new column, make sure the migration places it in the same order as +servatrice.sql. ### Protocol buffer ### -Cockatrice and Servatrice exchange data using binary messages. The syntax of these messages is defined in the `proto` files in the `common/pb` folder. These files defines the way data contained in each message is serialized using Google's [protocol buffer](https://developers.google.com/protocol-buffers/). -Any change to the `proto` file should be taken with caution and tested intensively before being merged, becaus a change to the protocol could make new clients incompatible to the old server and vice versa. +Cockatrice and Servatrice exchange data using binary messages. The syntax of +these messages is defined in the `proto` files in the `common/pb` folder. These +files define the way data contained in each message is serialized using +Google's [protocol buffers](https://developers.google.com/protocol-buffers/). +Any change to the `proto` files should be taken with caution and tested +intensively before being merged, because a change to the protocol could make +new clients incompatible to the old server and vice versa. -You can find more information on how we use Protobuf on [our wiki!](https://github.com/Cockatrice/Cockatrice/wiki/Client-server-protocol) +You can find more information on how we use Protobuf on [our wiki!]( +https://github.com/Cockatrice/Cockatrice/wiki/Client-server-protocol) +# Reviewing Pull Requests # + +After you have finished your changes to the project you should put them on a +separate branch of your fork on GitHub and open a [pull request]( +https://docs.github.com/en/free-pro-team@latest/desktop/contributing-and-collaborating-using-github-desktop/creating-an-issue-or-pull-request +). +Your code will then be automatically compiled by GitHub actions for Linux and +macOS, and by Appveyor for Windows. Additionally GitHub will perform a [Linting +check](#formatting-and-continuous-integration-ci). If any issues come up you +can check their status at the bottom of the pull request page, click on details +to go to the CI website and see the different build logs. + +If your pull request passes our tests and has no merge conflicts, it will be +reviewed by our team members. You can then address any requested changes. When +all changes have been approved your pull request will be squashed into a single +commit and merged into the master branch by a team member. Your change will then +be included in the next release 👍 # Translations # Basic workflow for translations: 1. Developer adds a `tr("foo")` string in the code; 2. Every few days, a maintainer updates the `*_en.ts files` with the new strings; - 3. Transifex picks up the new files from github every 24 hours; + 3. Transifex picks up the new files from GitHub every 24 hours; 4. Translators translate the new untranslated strings on Transifex; 5. Before a release, a maintainer fetches the updated translations from Transifex. ### Using Translations (for developers) ### -All the user-interface strings inside Cockatrice's source code must be written in english. -Translations to other languages are managed using [Transifex](https://www.transifex.com/projects/p/cockatrice/). +All the user-interface strings inside Cockatrice's source code must be written +in English(US). +Translations to other languages are managed using [Transifex]( +https://www.transifex.com/projects/p/cockatrice/). -Adding a new string to translate is as easy as adding the string in the 'tr("")' function, the string will be picked up as translatable automatically and translated as needed. -For example setting the text of this label in a way that the string "My name is:" can be translated: +Adding a new string to translate is as easy as adding the string in the +'tr("")' function, the string will be picked up as translatable automatically +and translated as needed. +For example, setting the text of a label in the way that the string +`"My name is:"` can be translated: ```c++ nameLabel.setText(tr("My name is:")); ``` -If you're about to propose a change that adds or modifies any translatable string in the code, you don't need to take care of adding the new strings to the translation files. -Every few days, or when a lot of new strings have been added, someone from the development team will take care of extracting all the new strings and adding them to the english translation files and making them available to translators on Transifex. +To translate a string that would have plural forms you can add the amount to +the tr call, also you can add an extra string as a hint for translators: +```c++ +QString message = tr("Everyone draws %n cards", "pop up message", amount); +``` +See [QT's wiki on translations]( +https://doc.qt.io/qt-5/i18n-source-translation.html#handling-plurals) + +If you're about to propose a change that adds or modifies any translatable +string in the code, you don't need to take care of adding the new strings to +the translation files. +Every few days, or when a lot of new strings have been added, someone from the +development team will take care of extracting all the new strings and adding +them to the english translation files and making them available to translators +on Transifex. ### Maintaining Translations (for maintainers) ### @@ -253,8 +359,9 @@ cmake .. -DUPDATE_TRANSLATIONS=OFF ``` Now you are ready to propose your change. -Once your change gets merged, Transifex will pick up the modified files automatically (checked every 24 hours) -and update the interface where translators will be able to translate the new strings. +Once your change gets merged, Transifex will pick up the modified files +automatically (checked every 24 hours) and update the interface where +translators will be able to translate the new strings. ### Releasing Translations (for maintainers) ### @@ -272,51 +379,85 @@ from Transifex to the source code and vice versa. ### Adding Translations (for translators) ### -As a translator you can help translate the new strings on [Transifex](https://www.transifex.com/projects/p/cockatrice/). -Please have a look at the specific [FAQ for translators](https://github.com/Cockatrice/Cockatrice/wiki/Translation-FAQ). +As a translator you can help translate the new strings on [Transifex]( +https://www.transifex.com/projects/p/cockatrice/). +Please have a look at the specific [FAQ for translators]( +https://github.com/Cockatrice/Cockatrice/wiki/Translation-FAQ). # Release Management # -### Publishing A New Beta Release ### +### Publishing A New Release ### -Travis and AppVeyor have been configured to upload files to GitHub Releases whenever a tag is pushed.
-Usually, tags are created through publishing a (pre-)release, but there's a way around that. +We use [GitHub Releases](https://github.com/Cockatrice/Cockatrice/releases) to +publish new stable versions and betas. +Whenever a git tag is pushed to the repository github will create a draft +release and upload binaries automatically. -To trigger Travis and AppVeyor, simply do the following: +To create a tag, simply do the following: ```bash -cd $COCKATRICE_REPO git checkout master git remote update -p git pull git tag $TAG_NAME -git push upstream $TAG_NAME +git push $UPSTREAM $TAG_NAME ``` + You should define the variables as such: ``` -upstream - git@github.com:Cockatrice/Cockatrice.git -$COCKATRICE_REPO - /Location/of/repository/cockatrice.git -`$TAG_NAME` should be: +`$UPSTREAM` - the remote for git@github.com:Cockatrice/Cockatrice.git +`$TAG_NAME` should be formatted as: - `YYYY-MM-DD-Release-MAJ.MIN.PATCH` for **stable releases** - `YYYY-MM-DD-Development-MAJ.MIN.PATCH-beta.X` for **beta releases**
With *MAJ.MIN.PATCH* being the NEXT release version! ``` -This will cause a tagged release to be established on the GitHub repository, which will then lead to the upload of binaries. If you use this method, the tags (releases) that you create will be marked as a "Pre-release". The `/latest` URL will not be impacted (for stable release downloads) so that's good. +This will cause a tagged release to be established on the GitHub repository, +with the binaries being added to the release whenever they are ready. +The release is initially a draft, where the path notes can be edited and after +all is good and ready it can be published on GitHub. +If you use a SemVer tag including "beta", the release that will be created at +GitHub will be marked as a "Pre-release" automatically. +The target of the `.../latest` URL will not be changed in that case, it always +points to the latest stable release and not pre-releases/betas. -If you accidentally push a tag incorrectly (the tag is outdated, you didn't pull in the latest branch accidentally, you named the tag wrong, etc.) you can revoke the tag by doing the following: +If you accidentally push a tag incorrectly (the tag is outdated, you didn't +pull in the latest branch accidentally, you named the tag wrong, etc.) you can +revoke the tag by doing the following: ```bash git push --delete upstream $TAG_NAME git tag -d $TAG_NAME ``` +You can also do this on GitHub, you'll also want to delete the new release. -**NOTE:** Unfortunately, due to the method of how Travis and AppVeyor work, to publish a stable release you will need to make a copy of the release notes locally and then paste them into the GitHub GUI once the binaries have been uploaded by them. These CI services will automatically overwrite the name of the release (to "Cockatrice $TAG_NAME"), the status of the release (to "Pre-release"), and the release body (to "Beta build of Cockatrice"). - -**NOTE 2:** In the first lines of https://github.com/Cockatrice/Cockatrice/blob/master/CMakeLists.txt there's an hardcoded version number used when compiling custom (not tagged) versions. While on tagged versions these numbers are overridden by the version numbers coming from the tag title, it's good practice to keep them aligned with the real ones. +In the first lines of [CMakeLists.txt]( +https://github.com/Cockatrice/Cockatrice/blob/master/CMakeLists.txt) +there's an hardcoded version number and pretty name used when compiling from +master or custom (not tagged) versions. +While on tagged versions these numbers are overridden by the version numbers +coming from the tag title, it's good practice to increment the ones at CMake +after every full release to stress that master is ahead of the last stable +release. The preferred flow of operation is: - * just before a release, update the version number in CMakeLists.txt to "next release version"; - * tag the release following the previously described syntax in order to get it built by CI; - * wait for CI to upload the binaries, double check if everything is in order - * after the release is complete, update the version number again to "next targeted beta version", typically increasing `PROJECT_VERSION_PATCH` by one. + * Just before a release, make sure the version number in CMakeLists.txt is set + to the same release version you are about to tag. + * This is also the time to change the pretty name in CMakeLists.txt called + GIT_TAG_RELEASENAME and commit and push these changes. + * Tag the release following the previously described syntax in order to get it + correctly built and deployed by CI. + * Wait for the configure step to create the release and update the patch + notes. + * Check on the github actions page for build progress which should be the top + listed [here]( + https://github.com/Cockatrice/Cockatrice/actions?query=event%3Apush+-branch%3Amaster + ). + * When the build has been completed you can verify all uploaded files on the + release are in order and hit the publish button. + * After the release is complete, update the CMake version number again to the + next targeted beta version, typically increasing `PROJECT_VERSION_PATCH` by + one. -**NOTE 3:** When releasing a new stable version, all the previous beta versions should be deleted. This is needed for Cockatrice to update users of the "beta" release channel to the latest version like other users. +When releasing a new stable version, all previous beta releases (and tags) +should be deleted. This is needed for Cockatrice to update users of the "Beta" +release channel correctly to the latest stable version as well. +This can be done the same way as revoking tags, mentioned above. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 178716a02..3ab3b7730 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -7,11 +7,27 @@ assignees: '' --- -System Information: - + + +**System Information:** + - _______________________________________________________________________________________ - + + + + + +_______________________________________________________________________________________ + + + +**Steps to reproduce:** + - Do A + - Do B + - Do C ... diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 62c77f6ed..fd9691673 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -3,3 +3,7 @@ contact_links: - name: 💬 Discord Community (Get help with server issues, e.g. Login) url: https://discord.gg/3Z9yzmA about: Need help with using the client? Want to find some games? Try the Discord server! + - name: 🌐 Translations (Help improve the localization of the app) + url: https://www.transifex.com/cockatrice/cockatrice/ + # it is not possible to add a link to the wiki to this description + about: For more information and guidance check our Translation FAQ on our wiki! diff --git a/.github/workflows/ci-builds.yml b/.github/workflows/ci-builds.yml new file mode 100644 index 000000000..a29df84ac --- /dev/null +++ b/.github/workflows/ci-builds.yml @@ -0,0 +1,409 @@ +name: Build + +on: + push: + branches: + - master + paths-ignore: + - '**.md' + - 'webclient/**' + tags: + - '*' + pull_request: + branches: + - master + paths-ignore: + - '**.md' + - 'webclient/**' + +jobs: + configure: + name: Configure + + runs-on: ubuntu-latest + + outputs: + tag: ${{steps.configure.outputs.tag}} + sha: ${{steps.configure.outputs.sha}} + upload_url: ${{steps.create_release.outputs.upload_url}} + + steps: + - name: Cancel previous runs + uses: styfle/cancel-workflow-action@0.6.0 + with: + access_token: ${{github.token}} # needs other token https://github.com/styfle/cancel-workflow-action/issues/7 + + - name: Configure + id: configure + shell: bash + run: | + tag_regex='^refs/tags/' + if [[ $GITHUB_EVENT_NAME == pull-request ]]; then # pull request + sha="${{github.event.pull_request.head.sha}}" + elif [[ $GITHUB_REF =~ $tag_regex ]]; then # release + sha="$GITHUB_SHA" + tag="${GITHUB_REF/refs\/tags\//}" + echo "::set-output name=tag::$tag" + else # push to branch + sha="$GITHUB_SHA" + fi + echo "::set-output name=sha::$sha" + + - name: Checkout + if: steps.configure.outputs.tag != null + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Prepare release paramaters + id: prepare + if: steps.configure.outputs.tag != null + shell: bash + env: + TAG: ${{steps.configure.outputs.tag}} + run: .ci/prep_release.sh + + - name: Create release + if: steps.configure.outputs.tag != null + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{github.token}} + with: + tag_name: ${{github.ref}} + release_name: ${{steps.prepare.outputs.title}} + body_path: ${{steps.prepare.outputs.body_path}} + draft: true + prerelease: ${{steps.prepare.outputs.is_beta == 'yes'}} + + build-linux: + strategy: + fail-fast: false + matrix: + # these names correspond to the files in .ci/$distro + include: + - distro: UbuntuHirsute + package: DEB + + - distro: UbuntuGroovy + package: DEB + test: skip + + - distro: UbuntuFocal + package: DEB + test: skip # UbuntuFocal has a broken qt for debug builds + + - distro: UbuntuBionic + package: DEB + + - distro: ArchLinux + package: skip # we are packaged in arch already + allow-failure: yes + + - distro: DebianBuster + package: DEB + + - distro: Fedora33 + package: RPM + test: skip # Fedora is our slowest build + + - distro: Fedora34 + package: RPM + test: skip # gtest does not compile for some reason + + name: ${{matrix.distro}} + + needs: configure + + runs-on: ubuntu-latest + + continue-on-error: ${{matrix.allow-failure == 'yes'}} + + env: + NAME: ${{matrix.distro}} + CACHE: /tmp/${{matrix.distro}}-cache # ${{runner.temp}} does not work? + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Get cache timestamp + id: cache_timestamp + shell: bash + run: echo "::set-output name=timestamp::$(date -u '+%Y%m%d%H%M%S')" + + - name: Restore cache + uses: actions/cache@v2 + env: + timestamp: ${{steps.cache_timestamp.outputs.timestamp}} + with: + path: ${{env.CACHE}} + key: docker-${{matrix.distro}}-cache-${{env.timestamp}} + restore-keys: | + docker-${{matrix.distro}}-cache- + + - name: Build ${{matrix.distro}} Docker image + shell: bash + run: source .ci/docker.sh --build + + - name: Build debug and test + if: matrix.test != 'skip' + shell: bash + run: | + source .ci/docker.sh + RUN --server --debug --test + + - name: Build release package + id: package + if: matrix.package != 'skip' + shell: bash + env: + suffix: '-${{matrix.distro}}' + type: '${{matrix.package}}' + run: | + source .ci/docker.sh + RUN --server --release --package "$type" --suffix "$suffix" + + - name: Upload artifact + if: matrix.package != 'skip' + uses: actions/upload-artifact@v2 + with: + name: ${{matrix.distro}}-package + path: ./build/${{steps.package.outputs.name}} + if-no-files-found: error + + - name: Upload to release + if: matrix.package != 'skip' && needs.configure.outputs.tag != null + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{github.token}} + with: + upload_url: ${{needs.configure.outputs.upload_url}} + asset_path: ./build/${{steps.package.outputs.name}} + asset_name: ${{steps.package.outputs.name}} + asset_content_type: application/octet-stream + + build-macos: + strategy: + fail-fast: false + matrix: + target: + - Debug + - 10.14_Mojave + - 10.15_Catalina + - 11.0_Big_Sur + include: + - target: Debug # tests only + os: macos-latest + xcode: 11.7 + type: Debug + do_tests: 0 # tests do not work yet on mac + make_package: false + + - target: 10.14_Mojave + os: macos-10.15 # runs on Catalina + xcode: 10.3 # allows compatibility with macos 10.14 + type: Release + do_tests: 0 + make_package: true + + - target: 10.15_Catalina + os: macos-10.15 + xcode: 11.7 + type: Release + do_tests: 0 + make_package: true + + - target: 11.0_Big_Sur + os: macos-11.0 + xcode: 12.2 + type: Release + do_tests: 0 + make_package: true + + name: macOS ${{matrix.target}} + + needs: configure + + runs-on: ${{matrix.os}} + + continue-on-error: ${{matrix.allow-failure == 'yes'}} + + env: + CCACHE_DIR: ~/.ccache + DEVELOPER_DIR: + /Applications/Xcode_${{matrix.xcode}}.app/Contents/Developer + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Install dependencies using homebrew + shell: bash + # cmake cannot find the mysql connector + # neither of these works: mariadb-connector-c mysql-connector-c++ + run: brew install ccache protobuf + + - name: Install QT using homebrew + id: brew_install_qt + continue-on-error: true + shell: bash + run: brew install qt@5 --force-bottle + + - name: Install QT using actions + if: steps.brew_install_qt.outcome != 'success' + uses: jurplel/install-qt-action@v2 + + - name: Get ccache timestamp + id: ccache_timestamp + shell: bash + run: echo "::set-output name=timestamp::$(date -u '+%Y%m%d%H%M%S')" + + - name: Restore ccache cache + uses: actions/cache@v2 + env: + timestamp: ${{steps.ccache_timestamp.outputs.timestamp}} + with: + path: ${{env.CCACHE_DIR}} + key: ${{runner.os}}-xcode-${{matrix.xcode}}-ccache-${{env.timestamp}} + restore-keys: | + ${{runner.os}}-xcode-${{matrix.xcode}}-ccache- + + - name: Build on Xcode ${{matrix.xcode}} + shell: bash + env: + CMAKE_BUILD_PARALLEL_LEVEL: 3 # mac machines actually have 3 cores + run: .ci/compile.sh ${{matrix.type}} --server + + - name: Test + if: matrix.do_tests == 1 + shell: bash + working-directory: build + run: cmake --build . --target test + + - name: Package for ${{matrix.target}} + id: package + if: matrix.make_package + shell: bash + working-directory: build + run: | + cmake --build . --target package + ../.ci/name_build.sh "-macOS-${{matrix.target}}" + + - name: Upload artifact + if: matrix.make_package + uses: actions/upload-artifact@v2 + with: + name: macOS-${{matrix.target}}-xcode-${{matrix.xcode}}-dmg + path: ${{steps.package.outputs.path}} + if-no-files-found: error + + - name: Upload to release + if: matrix.make_package && needs.configure.outputs.tag != null + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{github.token}} + with: + upload_url: ${{needs.configure.outputs.upload_url}} + asset_path: ${{steps.package.outputs.path}} + asset_name: ${{steps.package.outputs.name}} + asset_content_type: application/octet-stream + + build-windows: + strategy: + fail-fast: false + matrix: + arch: + - 64 + - 32 + include: + - arch: 64 + triplet: x64 + cmake: x64 + append: _64 + + - arch: 32 + triplet: x86 + cmake: Win32 + + name: Windows ${{matrix.arch}} + + needs: configure + + runs-on: windows-latest + + env: + QT_VERSION: '5.12.9' + QT_ARCH: msvc2017${{matrix.append}} + CMAKE_GENERATOR: 'Visual Studio 16 2019' + + steps: + - name: Add msbuild to PATH + id: add-msbuild + uses: microsoft/setup-msbuild@v1.0.2 + + - name: Checkout + uses: actions/checkout@v2 + with: + submodules: recursive + + - name: Restore Qt ${{env.QT_VERSION}} ${{matrix.arch}}-bit from cache + id: cache-qt + uses: actions/cache@v2 + with: + key: ${{runner.os}}-QtCache-${{env.QT_VERSION}}-${{matrix.arch}} + path: ${{runner.workspace}}/Qt + + - name: Install ${{matrix.arch}}-bit Qt + uses: jurplel/install-qt-action@v2 + with: + cached: ${{steps.cache-qt.outputs.cache-hit}} + version: ${{env.QT_VERSION}} + arch: win${{matrix.arch}}_${{env.QT_ARCH}} + + - name: Restore or setup vcpkg + uses: lukka/run-vcpkg@v6 + with: + vcpkgArguments: '@${{github.workspace}}/vcpkg.txt' + vcpkgDirectory: ${{github.workspace}}/vcpkg + appendedCacheKey: ${{hashFiles('**/vcpkg.txt')}} + vcpkgTriplet: ${{matrix.triplet}}-windows + + - name: Configure Cockatrice ${{matrix.arch}}-bit + shell: bash + run: | + mkdir -p build + cd build + export QTDIR="${{runner.workspace}}/Qt/$QT_VERSION/$QT_ARCH" + cmake .. -G "${{env.CMAKE_GENERATOR}}" -A "${{matrix.cmake}}" -DCMAKE_BUILD_TYPE="Release" -DWITH_SERVER=1 -DTEST=1 + + - name: Build Cockatrice ${{matrix.arch}}-bit + id: package + shell: bash + working-directory: build + run: | + cmake --build . --target package --config Release + ../.ci/name_build.sh "-win${{matrix.arch}}" + + - name: Run tests + shell: bash + working-directory: build + run: ctest -T Test -C Release + + - name: Upload artifact + uses: actions/upload-artifact@v2 + with: + name: Windows-${{matrix.arch}}bit-installer + path: ./build/${{steps.package.outputs.name}} + if-no-files-found: error + + - name: Upload to release + if: needs.configure.outputs.tag != null + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{github.token}} + with: + upload_url: ${{needs.configure.outputs.upload_url}} + asset_path: ./build/${{steps.package.outputs.name}} + asset_name: ${{steps.package.outputs.name}} + asset_content_type: application/octet-stream diff --git a/.github/workflows/clangify.yml b/.github/workflows/clangify.yml new file mode 100644 index 000000000..314534e7e --- /dev/null +++ b/.github/workflows/clangify.yml @@ -0,0 +1,28 @@ +name: Clangify + +on: + pull_request: + branches: + - master + paths-ignore: + - '**.md' + +jobs: + linter: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 20 # should be enough to find merge base + + - name: Install clang-format + shell: bash + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends clang-format + + - name: Run clangify + shell: bash + run: ./.ci/lint.sh diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..a0a57f3d7 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "vcpkg"] + path = vcpkg + url = https://github.com/microsoft/vcpkg.git diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index fbbbcbb38..000000000 --- a/.travis.yml +++ /dev/null @@ -1,276 +0,0 @@ -os: linux -dist: bionic -language: cpp -compiler: gcc - -git: - depth: 15 - -jobs: - include: - - #Static Code Analysis - - name: Check code style / Linting - if: tag IS NOT present - os: linux - group: stable - script: bash ./.ci/travis-lint.sh - - - #Ubuntu Bionic (on Docker) - - name: Ubuntu Bionic (Debug + Tests) - if: tag IS NOT present - os: linux - services: docker - language: minimal - env: NAME=UbuntuBionic CACHE=$HOME/$NAME - cache: - directories: - - $CACHE - before_install: source .ci/travis-docker.sh --build - script: RUN --server --debug --test - - - name: Ubuntu Bionic (Release) - if: (branch = master AND NOT type = pull_request) OR tag IS present - os: linux - services: docker - language: minimal - env: NAME=UbuntuBionic CACHE=$HOME/$NAME - cache: - directories: - - $CACHE - before_install: source .ci/travis-docker.sh --build - script: RUN --server --package $NAME --release - - #Ubuntu Focal (on Docker) - - name: Ubuntu Focal (Compile) - if: tag IS NOT present - os: linux - services: docker - language: minimal - env: NAME=UbuntuFocal CACHE=$HOME/$NAME - cache: - directories: - - $CACHE - before_install: source .ci/travis-docker.sh --build - script: RUN --server - - - name: Ubuntu Focal (Release) - if: (branch = master AND NOT type = pull_request) OR tag IS present - os: linux - services: docker - language: minimal - env: NAME=UbuntuFocal CACHE=$HOME/$NAME - cache: - directories: - - $CACHE - before_install: source .ci/travis-docker.sh --build - script: RUN --server --package $NAME --release - - #Debian Buster (on Docker) - - name: Debian Buster (Compile) - if: tag IS NOT present - os: linux - services: docker - language: minimal - env: NAME=DebianBuster CACHE=$HOME/$NAME - cache: - directories: - - $CACHE - before_install: source .ci/travis-docker.sh --build - script: RUN --server - - - name: Debian Buster (Release) - if: (branch = master AND NOT type = pull_request) OR tag IS present - os: linux - services: docker - language: minimal - env: NAME=DebianBuster CACHE=$HOME/$NAME - cache: - directories: - - $CACHE - before_install: source .ci/travis-docker.sh --build - script: RUN --server --package $NAME --release - - - #Fedora 32 (on Docker) - - name: Fedora 32 (Compile) - if: tag IS NOT present - services: docker - language: minimal - env: NAME=Fedora32 CACHE=$HOME/$NAME - cache: - directories: - - $CACHE - before_install: source .ci/travis-docker.sh --build - script: RUN --server - - - name: Fedora 32 (Release) - if: (branch = master AND NOT type = pull_request) OR tag IS present - services: docker - language: minimal - env: NAME=Fedora32 CACHE=$HOME/$NAME - cache: - directories: - - $CACHE - before_install: source .ci/travis-docker.sh --build - script: RUN --server --package $NAME RPM --release - - - #macOS High Sierra - - name: macOS High Sierra (Debug) - if: tag IS NOT present - os: osx - osx_image: xcode10.1 - cache: - - ccache - - directories: - - $HOME/Library/Caches/Homebrew - addons: - homebrew: - packages: - - ccache - - protobuf - - qt - - xz - update: true - script: bash ./.ci/travis-compile.sh --server --install --debug - before_cache: - - brew cleanup - - - name: macOS High Sierra (Release) - if: (branch = master AND NOT type = pull_request) OR tag IS present - os: osx - osx_image: xcode10.1 - env: OSX_VERSION=10.13 - cache: - - ccache - - directories: - - $HOME/Library/Caches/Homebrew - addons: - homebrew: - packages: - - ccache - - protobuf - - qt - - xz - update: true - script: bash ./.ci/travis-compile.sh --server --package "macos$OSX_VERSION" --release - before_cache: - - brew cleanup - - #macOS Catalina - - name: macOS Catalina (Debug) - if: tag IS NOT present - os: osx - osx_image: xcode11.6 - cache: - - ccache - - directories: - - $HOME/Library/Caches/Homebrew - addons: - homebrew: - packages: - - ccache - - protobuf - - qt - - xz - update: true - script: bash ./.ci/travis-compile.sh --server --install --debug - before_cache: - - brew cleanup - - - name: macOS Catalina (Release) - if: (branch = master AND NOT type = pull_request) OR tag IS present - os: osx - osx_image: xcode11.6 - env: OSX_VERSION=10.15 - cache: - - ccache - - directories: - - $HOME/Library/Caches/Homebrew - addons: - homebrew: - packages: - - ccache - - protobuf - - qt - - xz - update: true - script: bash ./.ci/travis-compile.sh --server --package "macos$OSX_VERSION" --release --zip - before_cache: - - brew cleanup - - - #Arch Linux (on Docker) (allow failures) - - name: Arch Linux (Debug) - if: tag IS NOT present - os: linux - services: docker - language: minimal - env: NAME=ArchLinux CACHE=$HOME/$NAME - cache: - directories: - - $CACHE - before_install: source .ci/travis-docker.sh --build - script: RUN --server --debug - - allow_failures: - # Arch linux is always the latest version and by definition not stable - - name: Arch Linux (Debug) - # Report build completion even if optional builds are not completed - fast_finish: true - - -# Builds for pull requests skip the deployment step altogether -deploy: -# Deploy configuration for "beta" releases - - provider: releases - token: - secure: mLMF41q7xgOR1sjczsilEy7HQis2PkZCzhfOGbn/8FoOQnmmPOZjrsdhn06ZSl3SFsbfCLuClDYXAbFscQmdgjcGN5AmHV+JYfW650QEuQa/f4/lQFsVRtEqUA1O3FQ0OuRxdpCfJubZBdFVH8SbZ93GLC5zXJbkWQNq+xCX1fU= - name: "Cockatrice $TRAVIS_TAG" - release_notes: "Beta release of Cockatrice" - skip_cleanup: true - file_glob: true - file: "build/Cockatrice-*" - overwrite: true - draft: false - prerelease: true - on: - tags: true - repo: Cockatrice/Cockatrice - condition: $TRAVIS_TAG =~ ([0-9]|[1-9][0-9])(\.([0-9]|[1-9][0-9])){2}-beta(\.([2-9]|[1-9][0-9]))?$ # regex to match semver naming convention for beta pre-releases - -# Deploy configuration for "stable" releases - - provider: releases - token: - secure: mLMF41q7xgOR1sjczsilEy7HQis2PkZCzhfOGbn/8FoOQnmmPOZjrsdhn06ZSl3SFsbfCLuClDYXAbFscQmdgjcGN5AmHV+JYfW650QEuQa/f4/lQFsVRtEqUA1O3FQ0OuRxdpCfJubZBdFVH8SbZ93GLC5zXJbkWQNq+xCX1fU= - skip_cleanup: true - file_glob: true - file: "build/Cockatrice-*" - overwrite: true - draft: false - prerelease: false - on: - tags: true - repo: Cockatrice/Cockatrice - condition: $TRAVIS_TAG =~ ([0-9]|[1-9][0-9])(\.([0-9]|[1-9][0-9])){2}$ # regex to match semver naming convention for stable full releases - - -notifications: - email: false - webhooks: - urls: - - https://webhooks.gitter.im/e/d94969c3b01b22cbdcb7 - on_success: change - on_failure: change - on_start: never - on_cancel: change - on_error: change - - -# Announcements of build image updates: https://docs.travis-ci.com/user/build-environment-updates/ -# For precise versions of preinstalled tools on the VM, check “Build system information” in the build log! -# Official validator for ".travis.yml" config file: https://yaml.travis-ci.org -# Official Travis CI Build Config Explorer: https://config.travis-ci.com/explore -# Travis CI config documentation: https://docs.travis-ci.com/user/customizing-the-build diff --git a/CMakeLists.txt b/CMakeLists.txt index f4a678d39..38759aade 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,15 +6,7 @@ # like the installation path, compilation flags etc.. # Cmake 3.1 is required to enable C++11 support correctly -cmake_minimum_required(VERSION 3.1) - -if(POLICY CMP0064) - cmake_policy(SET CMP0064 NEW) -endif() - -if(POLICY CMP0071) - cmake_policy(SET CMP0071 NEW) -endif() +cmake_minimum_required(VERSION 3.10) # Default to "Release" build type # User-provided value for CMAKE_BUILD_TYPE must be checked before the PROJECT() call @@ -35,9 +27,26 @@ if(USE_CCACHE) endif() endif() +if(WIN32) + # Use vcpkg toolchain on Windows + set(CMAKE_TOOLCHAIN_FILE ${CMAKE_CURRENT_SOURCE_DIR}/vcpkg/scripts/buildsystems/vcpkg.cmake + CACHE STRING "Vcpkg toolchain file") + # Qt path set by user or env var + if (QTDIR OR DEFINED ENV{QTDIR} OR DEFINED ENV{QTDIR32} OR DEFINED ENV{QTDIR64}) + else() + set(QTDIR "" CACHE PATH "Path to Qt (e.g. C:/Qt/5.7/msvc2015_64)") + message(WARNING "QTDIR variable is missing. Please set this variable to specify path to Qt (e.g. C:/Qt/5.7/msvc2015_64)") + endif() +endif() + # A project name is needed for CPack # Version can be overriden by git tags, see cmake/getversion.cmake -PROJECT("Cockatrice" VERSION 2.7.6) +PROJECT("Cockatrice" VERSION 2.8.1) + +# Set release name if not provided via env/cmake var +if(NOT DEFINED GIT_TAG_RELEASENAME) + set(GIT_TAG_RELEASENAME "Prismatic Bridge") +endif() # Use c++11 for all targets set(CMAKE_CXX_STANDARD 11 CACHE STRING "C++ ISO Standard") @@ -49,7 +58,6 @@ set(CMAKE_ALLOW_LOOSE_LOOP_CONSTRUCTS true) # Search path for cmake modules SET(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake) -# Retrieve git version hash include(getversion) # Create a header and a cpp file containing the version hash @@ -92,7 +100,7 @@ if(UNIX) endif() endif() elseif(WIN32) - set(CMAKE_INSTALL_PREFIX ${CMAKE_BINARY_DIR}/release) + set(CMAKE_INSTALL_PREFIX ${CMAKE_BINARY_DIR}/rundir/${CMAKE_BUILD_TYPE}) endif() # Treat warnings as errors (Debug builds only) @@ -100,10 +108,8 @@ option(WARNING_AS_ERROR "Treat warnings as errors in debug builds" ON) # Define proper compilation flags IF(MSVC) - # Visual Studio: - # Maximum optimization - # Disable warning C4251 - set(CMAKE_CXX_FLAGS_RELEASE "/Ox /MD /wd4251") + # Visual Studio: Maximum optimization, disable warning C4251 + set(CMAKE_CXX_FLAGS_RELEASE "/Ox /MD /wd4251 ") # Generate complete debugging information #set(CMAKE_CXX_FLAGS_DEBUG "/Zi") ELSEIF (CMAKE_COMPILER_IS_GNUCXX) @@ -128,7 +134,11 @@ ELSEIF (CMAKE_COMPILER_IS_GNUCXX) ELSE() # other: osx/llvm, bsd/llvm set(CMAKE_CXX_FLAGS_RELEASE "-O2") - set(CMAKE_CXX_FLAGS_DEBUG "-g -O0") + if(WARNING_AS_ERROR) + set(CMAKE_CXX_FLAGS_DEBUG "-g -O0 -Wall -Wextra -Werror") + else() + set(CMAKE_CXX_FLAGS_DEBUG "-g -O0 -Wall -Wextra") + endif() ENDIF() # GNU systems need to define the Mersenne exponent for the RNG to compile w/o warning @@ -139,8 +149,21 @@ ENDIF() FIND_PACKAGE(Threads REQUIRED) # Find Qt5 -OPTION(UPDATE_TRANSLATIONS "Update translations on compile" OFF) -MESSAGE(STATUS "UPDATE TRANSLATIONS: ${UPDATE_TRANSLATIONS}") +if(CMAKE_SIZEOF_VOID_P EQUAL 8) + set(_lib_suffix 64) +else() + set(_lib_suffix 32) +endif() + +if(DEFINED QTDIR${_lib_suffix}) + list(APPEND CMAKE_PREFIX_PATH "${QTDIR${_lib_suffix}}") +elseif(DEFINED QTDIR) + list(APPEND CMAKE_PREFIX_PATH "${QTDIR}") +elseif(DEFINED ENV{QTDIR${_lib_suffix}}) + list(APPEND CMAKE_PREFIX_PATH "$ENV{QTDIR${_lib_suffix}}") +elseif(DEFINED ENV{QTDIR}) + list(APPEND CMAKE_PREFIX_PATH "$ENV{QTDIR}") +endif() FIND_PACKAGE(Qt5Core 5.5.0 REQUIRED) @@ -161,12 +184,18 @@ ELSE() MESSAGE(FATAL_ERROR "No Qt5 found!") ENDIF() +# Check for translation updates +OPTION(UPDATE_TRANSLATIONS "Update translations on compile" OFF) +MESSAGE(STATUS "UPDATE TRANSLATIONS: ${UPDATE_TRANSLATIONS}") + set(CMAKE_AUTOMOC TRUE) # Find other needed libraries FIND_PACKAGE(Protobuf REQUIRED) IF(NOT EXISTS "${Protobuf_PROTOC_EXECUTABLE}") - MESSAGE(FATAL_ERROR "No protoc command found!") + MESSAGE(FATAL_ERROR "No protoc command found!") +ELSE() + MESSAGE(STATUS "Protoc version ${Protobuf_VERSION} found!") ENDIF() #Find OpenSSL @@ -215,7 +244,7 @@ if(UNIX) endif() elseif(WIN32) set(CPACK_GENERATOR NSIS ${CPACK_GENERATOR}) - if("${CMAKE_GENERATOR}" MATCHES "(Win64|IA64)") + if("${CMAKE_GENERATOR_PLATFORM}" MATCHES "(x64)") set(TRICE_IS_64_BIT 1) else() set(TRICE_IS_64_BIT 0) @@ -258,7 +287,7 @@ if(WITH_ORACLE) endif() # Compile dbconverter (default on) -option(WITH_DBCONVERTER "build oracle" ON) +option(WITH_DBCONVERTER "build dbconverter" ON) if(WITH_DBCONVERTER) add_subdirectory(dbconverter) SET(CPACK_INSTALL_CMAKE_PROJECTS "Dbconverter;Dbconverter;ALL;/" ${CPACK_INSTALL_CMAKE_PROJECTS}) diff --git a/README.md b/README.md index df26f5e00..94fa916bb 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,10 @@

Cockatrice | Download | - Get Involved | + Get Involved | Community | Translations | - Build | + Build | Run | License

@@ -29,21 +29,21 @@ Cockatrice is an open-source, multiplatform program for playing tabletop card ga # Download [![Cockatrice Eternal Download Count](https://img.shields.io/github/downloads/cockatrice/cockatrice/total.svg)](https://tooomm.github.io/github-release-stats/?username=Cockatrice&repository=Cockatrice) -Downloads are available for full releases and the current beta version in development.
-Full releases are checkpoints featuring major feature or UI enhancements - we recommend to use those. There is no strict schedule for new full releases. +Downloads are available for full releases and the current beta version in development. There is no strict release schedule for either of them. -The beta release contains the most recently added features and bugfixes, but can be unstable. They are released as we feel need. - -- Latest `stable` release (**recommended**): [![Download from GitHub Releases](https://img.shields.io/github/release/cockatrice/cockatrice.svg)](https://github.com/cockatrice/cockatrice/releases/latest) [![DL Count on Latest Release](https://img.shields.io/github/downloads/cockatrice/cockatrice/latest/total.svg?label=downloads)](https://tooomm.github.io/github-release-stats/?username=Cockatrice&repository=Cockatrice)
+- Latest `stable` release: [![Download from GitHub Releases](https://img.shields.io/github/release/cockatrice/cockatrice.svg)](https://github.com/cockatrice/cockatrice/releases/latest) [![DL Count on Latest Release](https://img.shields.io/github/downloads/cockatrice/cockatrice/latest/total.svg?label=downloads)](https://tooomm.github.io/github-release-stats/?username=Cockatrice&repository=Cockatrice)
+ - Stable versions are checkpoints featuring major feature and UI enhancements. + - **Recommended for most users!** - Latest `beta` release: [![Download from GitHub Pre-Releases](https://img.shields.io/github/release/cockatrice/cockatrice/all.svg)](https://github.com/cockatrice/cockatrice/releases) [![DL Count on Latest Pre-Release](https://img.shields.io/github/downloads-pre/cockatrice/cockatrice/latest/total.svg?label=downloads)](https://tooomm.github.io/github-release-stats/?username=Cockatrice&repository=Cockatrice) - - Beta versions may be unstable and contain bugs. + - Beta versions include the most recently added features and bugfixes, but can be unstable. - To be a Cockatrice Beta Tester, use this version. Find more information [here](https://github.com/Cockatrice/Cockatrice/wiki/Release-Channels)! + +# Get Involved [![Discord](https://img.shields.io/discord/314987288398659595?label=Discord&logo=discord&logoColor=white)](https://discord.gg/3Z9yzmA) [![Gitter Chat](https://img.shields.io/gitter/room/Cockatrice/Cockatrice)](https://gitter.im/Cockatrice/Cockatrice) -# Get Involved [![Gitter Chat](https://img.shields.io/gitter/room/Cockatrice/Cockatrice.svg)](https://gitter.im/Cockatrice/Cockatrice) - -[Chat](https://gitter.im/Cockatrice/Cockatrice) with the Cockatrice developers on Gitter. Come here to talk about the application, features, or just to hang out. For support regarding specific servers, please contact that server's admin or forum for support rather than asking here.
+Join our [Discord community](https://discord.gg/3Z9yzmA) to connect with the project or fellow users of the app. The Cockatrice developers are also available on [Gitter](https://gitter.im/Cockatrice/Cockatrice). Come here to talk about the application, features, or just to hang out.
+For support regarding specific servers, please contact that server's admin or forum for support rather than asking here.
To contribute code to the project, please review [the guidelines](https://github.com/Cockatrice/Cockatrice/blob/master/.github/CONTRIBUTING.md). We maintain two tags for contributors to find issues to work on: @@ -67,7 +67,7 @@ Cockatrice uses the [Google Developer Documentation Style Guide](https://develop - [reddit r/Cockatrice](https://reddit.com/r/cockatrice) -# Translations [![Cockatrice on Transiflex](https://tx-assets.scdn5.secure.raxcdn.com/static/charts/images/tx-logo-micro.c5603f91c780.png)](https://www.transifex.com/projects/p/cockatrice/) +# Translations [![Transifex Project](https://img.shields.io/badge/translate-on%20transifex-brightgreen)](https://www.transifex.com/projects/p/cockatrice/) Cockatrice uses Transifex for translations. You can help us bring Cockatrice and Oracle to your language or just edit single wordings right from within your browser by visiting our [Transifex project page](https://www.transifex.com/projects/p/cockatrice/).
@@ -78,9 +78,9 @@ Cockatrice uses Transifex for translations. You can help us bring Cockatrice and Check out our [Translator FAQ](https://github.com/Cockatrice/Cockatrice/wiki/Translation-FAQ) for more information about contributing!
-# Build [![Travis Build Status - master](https://travis-ci.org/Cockatrice/Cockatrice.svg?branch=master)](https://travis-ci.org/Cockatrice/Cockatrice) [![Appveyor Build Status - master](https://ci.appveyor.com/api/projects/status/oauxf5a0sj689rcg/branch/master?svg=true)](https://ci.appveyor.com/project/ZeldaZach/cockatrice/branch/master) +# Build [![CI Builds](https://github.com/Cockatrice/Cockatrice/actions/workflows/ci-builds.yml/badge.svg?branch=master&event=push)](https://github.com/Cockatrice/Cockatrice/actions/workflows/ci-builds.yml?query=branch%3Amaster+event%3Apush) -**Detailed compiling instructions are on the Cockatrice wiki under [Compiling Cockatrice](https://github.com/Cockatrice/Cockatrice/wiki/Compiling-Cockatrice)** +**Detailed compiling instructions can be found on the Cockatrice wiki under [Compiling Cockatrice](https://github.com/Cockatrice/Cockatrice/wiki/Compiling-Cockatrice)** Dependencies: *(for minimum requirements search our [CMake file](https://github.com/Cockatrice/Cockatrice/blob/master/CMakeLists.txt))* - [Qt](https://www.qt.io/developers/) @@ -125,7 +125,6 @@ The following flags can be passed to `cmake`: `Oracle` fetches card data
`Servatrice` is the server
- **Servatrice Docker container** You can run an instance of Servatrice (the Cockatrice server) using [Docker](https://www.docker.com/what-docker) and the Cockatrice Dockerfile.
@@ -160,6 +159,7 @@ A out of box working docker-compose file has been added to help setup in Windows Docker in Windows requires additional steps in form of using Docker Desktop to allow resource sharing from the drive the volumes are mapped from, as well as potential workarounds needed to get file sharing working in Windows. This [StackOverflow discussion sheds some light on it](https://stackoverflow.com/questions/42203488/settings-to-windows-firewall-to-allow-docker-for-windows-to-share-drive) + # License [![GPLv2 License](https://img.shields.io/github/license/Cockatrice/Cockatrice.svg)](https://github.com/Cockatrice/Cockatrice/blob/master/LICENSE) Cockatrice is free software, licensed under the [GPLv2](https://github.com/Cockatrice/Cockatrice/blob/master/LICENSE). diff --git a/clangify.sh b/clangify.sh index f2494cf50..ac1ab1be7 100755 --- a/clangify.sh +++ b/clangify.sh @@ -11,14 +11,15 @@ cd "${BASH_SOURCE%/*}/" || exit 2 # could not find path, this could happen with include=("common" \ "cockatrice/src" \ "oracle/src" \ -"servatrice/src") +"servatrice/src" \ +"tests") exclude=("servatrice/src/smtp" \ "common/sfmt" \ "common/lib" \ "oracle/src/zip" \ "oracle/src/lzma" \ "oracle/src/qt-json") -exts=("cpp" "h") +exts=("cpp" "h" "proto") cf_cmd="clang-format" branch="origin/master" diff --git a/cmake/getversion.cmake b/cmake/getversion.cmake index bc8476d23..e01cf4fa7 100644 --- a/cmake/getversion.cmake +++ b/cmake/getversion.cmake @@ -38,23 +38,6 @@ function(get_commit_date) set(GIT_COMMIT_DATE "${GIT_COM_DATE}" PARENT_SCOPE) endfunction() -function(clean_release_name name) - # Release Cockatrice 2.7.3: Dawn of Hope · Cockatrice/Cockatrice · GitHub - # Remove prefix - STRING(REGEX REPLACE "Release Cockatrice [0-9\.]+: " "" name "${name}") - # Remove suffix - STRING(REPLACE " · Cockatrice/Cockatrice · GitHub" "" name "${name}") - # Remove all unwanted chars - STRING(REGEX REPLACE "[^A-Za-z0-9_ ]" "" name "${name}") - # Strip (trim) whitespaces - STRING(STRIP "${name}" name) - # Replace all spaces with underscores - STRING(REPLACE " " "_" name "${name}") - - MESSAGE(STATUS "Friendly tag name: ${name}") - set(GIT_TAG_RELEASENAME "${name}" PARENT_SCOPE) -endfunction() - function(get_tag_name commit) if(${commit} STREQUAL "unknown") return() @@ -163,23 +146,8 @@ function(get_tag_name commit) set(PROJECT_VERSION_LABEL ${GIT_TAG_LABEL} PARENT_SCOPE) elseif(${GIT_TAG_TYPE} STREQUAL "Release") set(PROJECT_VERSION_LABEL "" PARENT_SCOPE) - - # get version name from github - set(GIT_TAG_TEMP_FILE "${PROJECT_BINARY_DIR}/tag_informations.txt") - set(GIT_TAG_TEMP_URL "https://github.com/Cockatrice/Cockatrice/releases/tag/${GIT_TAG}") - message(STATUS "Fetching tag informations from ${GIT_TAG_TEMP_URL}") - file(REMOVE "${GIT_TAG_TEMP_FILE}") - file(DOWNLOAD "${GIT_TAG_TEMP_URL}" "${GIT_TAG_TEMP_FILE}" STATUS status LOG log INACTIVITY_TIMEOUT 30 TIMEOUT 300 SHOW_PROGRESS) - list(GET status 0 err) - list(GET status 1 msg) - if(err) - message(WARNING "Download failed with error ${msg}: ${log}") - return() - endif() - file(STRINGS "${GIT_TAG_TEMP_FILE}" GIT_TAG_RAW_RELEASENAME REGEX "Release Cockatrice .*" LIMIT_COUNT 1 ENCODING UTF-8) - - clean_release_name("${GIT_TAG_RAW_RELEASENAME}") - set(PROJECT_VERSION_RELEASENAME "${GIT_TAG_RELEASENAME}" PARENT_SCOPE) + # set release name from env var + set(PROJECT_VERSION_RELEASENAME "${GIT_TAG_RELEASENAME}" PARENT_SCOPE) endif() endfunction() @@ -188,9 +156,9 @@ endfunction() # fallback defaults set(GIT_COMMIT_ID "unknown") -set(GIT_COMMIT_DATE "unknown") -set(GIT_COMMIT_DATE_FRIENDLY "unknown") -set(PROJECT_VERSION_LABEL "custom(unknown)") +set(GIT_COMMIT_DATE "") +set(GIT_COMMIT_DATE_FRIENDLY "") +set(PROJECT_VERSION_LABEL "") set(PROJECT_VERSION_RELEASENAME "") find_package(Git) diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index e97cde147..b1d213786 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -18,6 +18,7 @@ SET(cockatrice_SOURCES src/dlg_forgotpasswordrequest.cpp src/dlg_forgotpasswordreset.cpp src/dlg_forgotpasswordchallenge.cpp + src/dlg_manage_sets.cpp src/dlg_register.cpp src/dlg_tip_of_the_day.cpp src/tip_of_the_day.cpp @@ -62,7 +63,6 @@ SET(cockatrice_SOURCES src/carddragitem.cpp src/carddatabasemodel.cpp src/setsmodel.cpp - src/window_sets.cpp src/abstractgraphicsitem.cpp src/abstractcarddragitem.cpp src/dlg_settings.cpp @@ -79,7 +79,7 @@ SET(cockatrice_SOURCES src/tab_replays.cpp src/tab_supervisor.cpp src/tab_admin.cpp - src/tab_userlists.cpp + src/tab_account.cpp src/tab_deck_editor.cpp src/tab_logs.cpp src/replay_timeline_widget.cpp diff --git a/cockatrice/resources/help/search.md b/cockatrice/resources/help/search.md index 31b5f2b4f..481789cb9 100644 --- a/cockatrice/resources/help/search.md +++ b/cockatrice/resources/help/search.md @@ -49,7 +49,7 @@ The search bar recognizes a set of special commands similar to some other card d
[e:lea,leb](#e:lea,leb) (Cards that appear in Alpha or Beta)
e:lea,leb -(e:lea e:leb) (Cards that appear in Alpha or Beta but not in both editions)
-
Inverse:
+
Negate:
[c:wu -c:m](#c:wu -c:m) (Any card that is white or blue, but not multicolored)
Branching:
diff --git a/cockatrice/src/abstractcarditem.cpp b/cockatrice/src/abstractcarditem.cpp index 0f04fd6f9..5325409ef 100644 --- a/cockatrice/src/abstractcarditem.cpp +++ b/cockatrice/src/abstractcarditem.cpp @@ -287,14 +287,14 @@ void AbstractCardItem::mousePressEvent(QGraphicsSceneMouseEvent *event) } if (event->button() == Qt::LeftButton) setCursor(Qt::ClosedHandCursor); - else if (event->button() == Qt::MidButton) + else if (event->button() == Qt::MiddleButton) emit showCardInfoPopup(event->screenPos(), name); event->accept(); } void AbstractCardItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { - if (event->button() == Qt::MidButton) + if (event->button() == Qt::MiddleButton) emit deleteCardInfoPopup(name); // This function ensures the parent function doesn't mess around with our selection. diff --git a/cockatrice/src/abstractcounter.cpp b/cockatrice/src/abstractcounter.cpp index 683de8dca..17057c969 100644 --- a/cockatrice/src/abstractcounter.cpp +++ b/cockatrice/src/abstractcounter.cpp @@ -127,7 +127,7 @@ void AbstractCounter::setValue(int _value) void AbstractCounter::mousePressEvent(QGraphicsSceneMouseEvent *event) { if (isUnderMouse() && player->getLocalOrJudge()) { - if (event->button() == Qt::MidButton || (QApplication::keyboardModifiers() & Qt::ShiftModifier)) { + if (event->button() == Qt::MiddleButton || (QApplication::keyboardModifiers() & Qt::ShiftModifier)) { if (menu) menu->exec(event->screenPos()); event->accept(); diff --git a/cockatrice/src/carddatabase.cpp b/cockatrice/src/carddatabase.cpp index 3ae2b98ab..22f25fc75 100644 --- a/cockatrice/src/carddatabase.cpp +++ b/cockatrice/src/carddatabase.cpp @@ -10,8 +10,10 @@ #include #include #include +#include #include #include +#include #include #include @@ -291,22 +293,23 @@ void CardInfo::refreshCachedSetNames() QString CardInfo::simplifyName(const QString &name) { - QString simpleName(name); + static const QRegularExpression spaceOrSplit("(\\s+|\\/\\/.*)"); + static const QRegularExpression nonAlnum("[^a-z0-9]"); + + QString simpleName = name.toLower(); + + // remove spaces and right halves of split cards + simpleName.remove(spaceOrSplit); // So Aetherling would work, but not Ætherling since 'Æ' would get replaced // with nothing. simpleName.replace("æ", "ae"); - simpleName.replace("Æ", "AE"); // Replace Jötun Grunt with Jotun Grunt. simpleName = simpleName.normalized(QString::NormalizationForm_KD); - // Replace dashes with spaces so that we can say "garruk the veil cursed" - // instead of the unintuitive "garruk the veilcursed". - simpleName = simpleName.replace("-", " "); - - simpleName.remove(QRegExp("[^a-zA-Z0-9 ]")); - simpleName = simpleName.toLower(); + // remove all non alphanumeric characters from the name + simpleName.remove(nonAlnum); return simpleName; } @@ -437,6 +440,19 @@ CardInfoPtr CardDatabase::getCardBySimpleName(const QString &cardName) const return getCardFromMap(simpleNameCards, CardInfo::simplifyName(cardName)); } +CardInfoPtr CardDatabase::guessCard(const QString &cardName) const +{ + CardInfoPtr temp = getCard(cardName); + if (temp == nullptr) { // get card by simple name instead + temp = getCardBySimpleName(cardName); + if (temp == nullptr) { // still could not find the card, so simplify the cardName too + QString simpleCardName = CardInfo::simplifyName(cardName); + temp = getCardBySimpleName(simpleCardName); + } + } + return temp; // returns nullptr if not found +} + CardSetPtr CardDatabase::getSet(const QString &setName) { if (sets.contains(setName)) { @@ -494,6 +510,7 @@ LoadStatus CardDatabase::loadFromFile(const QString &fileName) LoadStatus CardDatabase::loadCardDatabase(const QString &path) { + auto startTime = QTime::currentTime(); LoadStatus tempLoadStatus = NotLoaded; if (!path.isEmpty()) { loadFromFileMutex->lock(); @@ -501,8 +518,9 @@ LoadStatus CardDatabase::loadCardDatabase(const QString &path) loadFromFileMutex->unlock(); } + int msecs = startTime.msecsTo(QTime::currentTime()); qDebug() << "[CardDatabase] loadCardDatabase(): Path =" << path << "Status =" << tempLoadStatus - << "Cards =" << cards.size() << "Sets=" << sets.size(); + << "Cards =" << cards.size() << "Sets =" << sets.size() << QString("%1ms").arg(msecs); return tempLoadStatus; } @@ -519,11 +537,21 @@ LoadStatus CardDatabase::loadCardDatabases() loadCardDatabase(SettingsCache::instance().getTokenDatabasePath()); // load tokens database loadCardDatabase(SettingsCache::instance().getSpoilerCardDatabasePath()); // load spoilers database - // load custom card databases - QDir dir(SettingsCache::instance().getCustomCardDatabasePath()); - for (const QString &fileName : - dir.entryList(QStringList("*.xml"), QDir::Files | QDir::Readable, QDir::Name | QDir::IgnoreCase)) { - loadCardDatabase(dir.absoluteFilePath(fileName)); + // find all custom card databases, recursively & following symlinks + // then load them alphabetically + QDirIterator customDatabaseIterator(SettingsCache::instance().getCustomCardDatabasePath(), QStringList() << "*.xml", + QDir::Files, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks); + QStringList databasePaths; + while (customDatabaseIterator.hasNext()) { + customDatabaseIterator.next(); + databasePaths.push_back(customDatabaseIterator.filePath()); + } + databasePaths.sort(); + + for (auto i = 0; i < databasePaths.size(); ++i) { + const auto &databasePath = databasePaths.at(i); + qDebug() << "Loading Custom Set" << i << "(" << databasePath << ")"; + loadCardDatabase(databasePath); } // AFTER all the cards have been loaded diff --git a/cockatrice/src/carddatabase.h b/cockatrice/src/carddatabase.h index b18909147..fe884c51b 100644 --- a/cockatrice/src/carddatabase.h +++ b/cockatrice/src/carddatabase.h @@ -409,6 +409,7 @@ public: void removeCard(CardInfoPtr card); CardInfoPtr getCard(const QString &cardName) const; QList getCards(const QStringList &cardNames) const; + CardInfoPtr guessCard(const QString &cardName) const; /* * Get a card by its simple name. The name will be simplified in this diff --git a/cockatrice/src/carddbparser/cockatricexml4.cpp b/cockatrice/src/carddbparser/cockatricexml4.cpp index a4b89d6c2..e9bb4adb5 100644 --- a/cockatrice/src/carddbparser/cockatricexml4.cpp +++ b/cockatrice/src/carddbparser/cockatricexml4.cpp @@ -160,14 +160,17 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml) // NOTE: attributes but be read before readElementText() QXmlStreamAttributes attrs = xml.attributes(); QString setName = xml.readElementText(QXmlStreamReader::IncludeChildElements); - CardInfoPerSet setInfo(internalAddSet(setName)); - for (QXmlStreamAttribute attr : attrs) { - QString attrName = attr.name().toString(); - if (attrName == "picURL") - attrName = "picurl"; - setInfo.setProperty(attrName, attr.value().toString()); + auto set = internalAddSet(setName); + if (set->getEnabled()) { + CardInfoPerSet setInfo(set); + for (QXmlStreamAttribute attr : attrs) { + QString attrName = attr.name().toString(); + if (attrName == "picURL") + attrName = "picurl"; + setInfo.setProperty(attrName, attr.value().toString()); + } + sets.insert(setName, setInfo); } - sets.insert(setName, setInfo); // relatd cards } else if (xml.name() == "related" || xml.name() == "reverse-related") { bool attach = false; @@ -380,4 +383,4 @@ bool CockatriceXml4Parser::saveToFile(SetNameMap sets, xml.writeEndDocument(); return true; -} \ No newline at end of file +} diff --git a/cockatrice/src/cardframe.cpp b/cockatrice/src/cardframe.cpp index eeec5bc22..a9044652f 100644 --- a/cockatrice/src/cardframe.cpp +++ b/cockatrice/src/cardframe.cpp @@ -107,7 +107,7 @@ void CardFrame::setCard(CardInfoPtr card) void CardFrame::setCard(const QString &cardName) { - setCard(db->getCardBySimpleName(cardName)); + setCard(db->guessCard(cardName)); } void CardFrame::setCard(AbstractCardItem *card) diff --git a/cockatrice/src/cardinfowidget.cpp b/cockatrice/src/cardinfowidget.cpp index cb864c4f7..4c0d16717 100644 --- a/cockatrice/src/cardinfowidget.cpp +++ b/cockatrice/src/cardinfowidget.cpp @@ -65,9 +65,10 @@ void CardInfoWidget::setCard(CardInfoPtr card) void CardInfoWidget::setCard(const QString &cardName) { - setCard(db->getCardBySimpleName(cardName)); - if (!info) + setCard(db->guessCard(cardName)); + if (info == nullptr) { text->setInvalidCardName(cardName); + } } void CardInfoWidget::setCard(AbstractCardItem *card) diff --git a/cockatrice/src/carditem.cpp b/cockatrice/src/carditem.cpp index 19b3b691d..84d09df16 100644 --- a/cockatrice/src/carditem.cpp +++ b/cockatrice/src/carditem.cpp @@ -18,9 +18,15 @@ #include #include -CardItem::CardItem(Player *_owner, const QString &_name, int _cardid, bool _revealedCard, QGraphicsItem *parent) - : AbstractCardItem(_name, _owner, _cardid, parent), zone(0), revealedCard(_revealedCard), attacking(false), - destroyOnZoneChange(false), doesntUntap(false), dragItem(0), attachedTo(0) +CardItem::CardItem(Player *_owner, + const QString &_name, + int _cardid, + bool _revealedCard, + QGraphicsItem *parent, + CardZone *_zone, + bool updateMenu) + : AbstractCardItem(_name, _owner, _cardid, parent), zone(_zone), revealedCard(_revealedCard), attacking(false), + destroyOnZoneChange(false), doesntUntap(false), dragItem(nullptr), attachedTo(nullptr) { owner->addCard(this); @@ -29,7 +35,9 @@ CardItem::CardItem(Player *_owner, const QString &_name, int _cardid, bool _reve moveMenu = new QMenu; retranslateUi(); - emit updateCardMenu(this); + if (updateMenu) { // avoid updating card menu too often + emit updateCardMenu(this); + } } CardItem::~CardItem() @@ -48,22 +56,22 @@ CardItem::~CardItem() void CardItem::prepareDelete() { - if (owner) { + if (owner != nullptr) { if (owner->getCardMenu() == cardMenu) { - owner->setCardMenu(0); - owner->getGame()->setActiveCard(0); + owner->setCardMenu(nullptr); + owner->getGame()->setActiveCard(nullptr); } - owner = 0; + owner = nullptr; } while (!attachedCards.isEmpty()) { - attachedCards.first()->setZone(0); // so that it won't try to call reorganizeCards() - attachedCards.first()->setAttachedTo(0); + attachedCards.first()->setZone(nullptr); // so that it won't try to call reorganizeCards() + attachedCards.first()->setAttachedTo(nullptr); } - if (attachedTo) { + if (attachedTo != nullptr) { attachedTo->removeAttachedCard(this); - attachedTo = 0; + attachedTo = nullptr; } } @@ -265,9 +273,10 @@ CardDragItem *CardItem::createDragItem(int _id, const QPointF &_pos, const QPoin void CardItem::deleteDragItem() { - if (dragItem) + if (dragItem) { dragItem->deleteLater(); - dragItem = NULL; + } + dragItem = nullptr; } void CardItem::drawArrow(const QColor &arrowColor) @@ -362,7 +371,7 @@ void CardItem::playCard(bool faceDown) void CardItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { if (event->button() == Qt::RightButton) { - if (cardMenu && !cardMenu->isEmpty() && owner) { + if (cardMenu && !cardMenu->isEmpty() && owner != nullptr) { owner->updateCardMenu(this); cardMenu->exec(event->screenPos()); } @@ -370,7 +379,7 @@ void CardItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) (!SettingsCache::instance().getDoubleClickToPlay())) { bool hideCard = false; if (zone && zone->getIsView()) { - ZoneViewZone *view = static_cast(zone); + auto *view = static_cast(zone); if (view->getRevealZone() && !view->getWriteableRevealZone()) hideCard = true; } @@ -381,7 +390,9 @@ void CardItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) } } - setCursor(Qt::OpenHandCursor); + if (owner != nullptr) { // cards without owner will be deleted + setCursor(Qt::OpenHandCursor); + } AbstractCardItem::mouseReleaseEvent(event); } diff --git a/cockatrice/src/carditem.h b/cockatrice/src/carditem.h index 579929585..5c04d97fc 100644 --- a/cockatrice/src/carditem.h +++ b/cockatrice/src/carditem.h @@ -52,7 +52,9 @@ public: const QString &_name = QString(), int _cardid = -1, bool revealedCard = false, - QGraphicsItem *parent = nullptr); + QGraphicsItem *parent = nullptr, + CardZone *_zone = nullptr, + bool updateMenu = false); ~CardItem(); void retranslateUi(); CardZone *getZone() const diff --git a/cockatrice/src/cardzone.cpp b/cockatrice/src/cardzone.cpp index 7010e56a9..438c5de2b 100644 --- a/cockatrice/src/cardzone.cpp +++ b/cockatrice/src/cardzone.cpp @@ -18,7 +18,7 @@ CardZone::CardZone(Player *_p, bool _contentsKnown, QGraphicsItem *parent, bool _isView) - : AbstractGraphicsItem(parent), player(_p), name(_name), cards(_contentsKnown), view(NULL), menu(NULL), + : AbstractGraphicsItem(parent), player(_p), name(_name), cards(_contentsKnown), views{}, menu(nullptr), doubleClickAction(0), hasCardAttr(_hasCardAttr), isShufflable(_isShufflable), isView(_isView) { if (!isView) @@ -28,7 +28,11 @@ CardZone::CardZone(Player *_p, CardZone::~CardZone() { qDebug() << "CardZone destructor: " << name; - delete view; + for (auto *view : views) { + if (view != nullptr) { + view->deleteLater(); + } + } clearContents(); } @@ -120,9 +124,11 @@ void CardZone::mousePressEvent(QGraphicsSceneMouseEvent *event) void CardZone::addCard(CardItem *card, bool reorganize, int x, int y) { - if (view) - if ((x <= view->getCards().size()) || (view->getNumberCards() == -1)) + for (auto *view : views) { + if ((x <= view->getCards().size()) || (view->getNumberCards() == -1)) { view->addCard(new CardItem(player, card->getName(), card->getId()), reorganize, x, y); + } + } card->setZone(this); addCardImpl(card, x, y); @@ -168,8 +174,9 @@ CardItem *CardZone::takeCard(int position, int cardId, bool /*canResize*/) CardItem *c = cards.takeAt(position); - if (view) + for (auto *view : views) { view->removeCard(position); + } c->setId(cardId); diff --git a/cockatrice/src/cardzone.h b/cockatrice/src/cardzone.h index e64fef025..5585663a8 100644 --- a/cockatrice/src/cardzone.h +++ b/cockatrice/src/cardzone.h @@ -21,7 +21,7 @@ protected: Player *player; QString name; CardList cards; - ZoneViewZone *view; + QList views; QMenu *menu; QAction *doubleClickAction; bool hasCardAttr; @@ -98,13 +98,9 @@ public: // takeCard() finds a card by position and removes it from the zone and from all of its views. virtual CardItem *takeCard(int position, int cardId, bool canResize = true); void removeCard(CardItem *card); - ZoneViewZone *getView() const + QList &getViews() { - return view; - } - void setView(ZoneViewZone *_view) - { - view = _view; + return views; } virtual void reorganizeCards() = 0; virtual QPointF closestGridPoint(const QPointF &point); diff --git a/cockatrice/src/chatview/chatview.cpp b/cockatrice/src/chatview/chatview.cpp index 858166f77..3dc88061d 100644 --- a/cockatrice/src/chatview/chatview.cpp +++ b/cockatrice/src/chatview/chatview.cpp @@ -3,7 +3,7 @@ #include "../pixmapgenerator.h" #include "../settingscache.h" #include "../soundengine.h" -#include "../tab_userlists.h" +#include "../tab_account.h" #include "../user_context_menu.h" #include "user_level.h" @@ -529,12 +529,12 @@ void ChatView::mousePressEvent(QMouseEvent *event) { switch (hoveredItemType) { case HoveredCard: { - if ((event->button() == Qt::MidButton) || (event->button() == Qt::LeftButton)) + if ((event->button() == Qt::MiddleButton) || (event->button() == Qt::LeftButton)) emit showCardInfoPopup(event->globalPos(), hoveredContent); break; } case HoveredUser: { - if (event->button() != Qt::MidButton) { + if (event->button() != Qt::MiddleButton) { const int delimiterIndex = hoveredContent.indexOf("_"); const QString userName = hoveredContent.mid(delimiterIndex + 1); switch (event->button()) { @@ -564,7 +564,7 @@ void ChatView::mousePressEvent(QMouseEvent *event) void ChatView::mouseReleaseEvent(QMouseEvent *event) { - if ((event->button() == Qt::MidButton) || (event->button() == Qt::LeftButton)) + if ((event->button() == Qt::MiddleButton) || (event->button() == Qt::LeftButton)) emit deleteCardInfoPopup(QString("_")); QTextBrowser::mouseReleaseEvent(event); diff --git a/cockatrice/src/deck_loader.cpp b/cockatrice/src/deck_loader.cpp index 8f8cd38c3..2967e0181 100644 --- a/cockatrice/src/deck_loader.cpp +++ b/cockatrice/src/deck_loader.cpp @@ -291,7 +291,7 @@ QString DeckLoader::getCardZoneFromName(QString cardName, QString currentZoneNam QString DeckLoader::getCompleteCardName(const QString cardName) const { if (db) { - CardInfoPtr temp = db->getCardBySimpleName(cardName); + CardInfoPtr temp = db->guessCard(cardName); if (temp) { return temp->getName(); } diff --git a/cockatrice/src/dlg_connect.cpp b/cockatrice/src/dlg_connect.cpp index bf34c68f5..4f29b390a 100644 --- a/cockatrice/src/dlg_connect.cpp +++ b/cockatrice/src/dlg_connect.cpp @@ -63,11 +63,12 @@ DlgConnect::DlgConnect(QWidget *parent) : QDialog(parent) autoConnectCheckBox = new QCheckBox(tr("A&uto connect")); autoConnectCheckBox->setToolTip(tr("Automatically connect to the most recent login when Cockatrice opens")); - if (SettingsCache::instance().servers().getSavePassword()) { - autoConnectCheckBox->setChecked(static_cast(SettingsCache::instance().servers().getAutoConnect())); + auto &servers = SettingsCache::instance().servers(); + if (servers.getSavePassword()) { + autoConnectCheckBox->setChecked(servers.getAutoConnect() > 0); autoConnectCheckBox->setEnabled(true); } else { - SettingsCache::instance().servers().setAutoConnect(0); + servers.setAutoConnect(0); autoConnectCheckBox->setChecked(false); autoConnectCheckBox->setEnabled(false); } @@ -87,7 +88,7 @@ DlgConnect::DlgConnect(QWidget *parent) : QDialog(parent) btnForgotPassword = new QPushButton(this); btnForgotPassword->setIcon(QPixmap("theme:icons/forgot_password")); - btnForgotPassword->setToolTip(tr("Forgot Password")); + btnForgotPassword->setToolTip(tr("Reset Password")); btnForgotPassword->setFixedWidth(30); connect(btnForgotPassword, SIGNAL(released()), this, SLOT(actForgotPassword())); @@ -192,13 +193,15 @@ void DlgConnect::rebuildComboBoxList(int failure) UserConnection_Information uci; savedHostList = uci.getServerInfo(); - bool autoConnectEnabled = static_cast(SettingsCache::instance().servers().getAutoConnect()); - QString autoConnectSaveName = SettingsCache::instance().servers().getSaveName(); + auto &servers = SettingsCache::instance().servers(); + bool autoConnectEnabled = servers.getAutoConnect() > 0; + QString previousHostName = servers.getPrevioushostName(); + QString autoConnectSaveName = servers.getSaveName(); int index = 0; for (const auto &pair : savedHostList) { - auto tmp = pair.second; + const auto &tmp = pair.second; QString saveName = tmp.getSaveName(); if (saveName.size()) { previousHosts->addItem(saveName); @@ -207,7 +210,7 @@ void DlgConnect::rebuildComboBoxList(int failure) if (saveName.compare(autoConnectSaveName) == 0) { previousHosts->setCurrentIndex(index); } - } else if (saveName.compare("Rooster Ranges") == 0) { + } else if (saveName.compare(previousHostName) == 0) { previousHosts->setCurrentIndex(index); } @@ -346,6 +349,11 @@ bool DeleteHighlightedItemWhenShiftDelPressedEventFilter::eventFilter(QObject *o void DlgConnect::actForgotPassword() { + ServersSettings &servers = SettingsCache::instance().servers(); + servers.setFPHostName(hostEdit->text()); + servers.setFPPort(portEdit->text()); + servers.setFPPlayerName(playernameEdit->text().trimmed()); + emit sigStartForgotPasswordRequest(); reject(); } diff --git a/cockatrice/src/dlg_creategame.cpp b/cockatrice/src/dlg_creategame.cpp index ad5742116..654cf97e4 100644 --- a/cockatrice/src/dlg_creategame.cpp +++ b/cockatrice/src/dlg_creategame.cpp @@ -83,11 +83,13 @@ void DlgCreateGame::sharedCtor() spectatorsNeedPasswordCheckBox = new QCheckBox(tr("Spectators &need a password to watch")); spectatorsCanTalkCheckBox = new QCheckBox(tr("Spectators can &chat")); spectatorsSeeEverythingCheckBox = new QCheckBox(tr("Spectators can see &hands")); + createGameAsSpectatorCheckBox = new QCheckBox(tr("Create game as spectator")); QVBoxLayout *spectatorsLayout = new QVBoxLayout; spectatorsLayout->addWidget(spectatorsAllowedCheckBox); spectatorsLayout->addWidget(spectatorsNeedPasswordCheckBox); spectatorsLayout->addWidget(spectatorsCanTalkCheckBox); spectatorsLayout->addWidget(spectatorsSeeEverythingCheckBox); + spectatorsLayout->addWidget(createGameAsSpectatorCheckBox); spectatorsGroupBox = new QGroupBox(tr("Spectators")); spectatorsGroupBox->setLayout(spectatorsLayout); @@ -95,7 +97,7 @@ void DlgCreateGame::sharedCtor() grid->addWidget(generalGroupBox, 0, 0); grid->addWidget(joinRestrictionsGroupBox, 0, 1); grid->addWidget(gameTypeGroupBox, 1, 0); - grid->addWidget(spectatorsGroupBox, 1, 1); + grid->addWidget(spectatorsGroupBox, 1, 1, Qt::AlignTop); grid->addWidget(rememberGameSettings, 2, 0); buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok); @@ -129,6 +131,7 @@ DlgCreateGame::DlgCreateGame(TabRoom *_room, const QMap &_gameType spectatorsNeedPasswordCheckBox->setChecked(SettingsCache::instance().getSpectatorsNeedPassword()); spectatorsCanTalkCheckBox->setChecked(SettingsCache::instance().getSpectatorsCanTalk()); spectatorsSeeEverythingCheckBox->setChecked(SettingsCache::instance().getSpectatorsCanSeeEverything()); + createGameAsSpectatorCheckBox->setChecked(SettingsCache::instance().getCreateGameAsSpectator()); if (!rememberGameSettings->isChecked()) { actReset(); @@ -159,6 +162,7 @@ DlgCreateGame::DlgCreateGame(const ServerInfo_Game &gameInfo, const QMapsetEnabled(false); spectatorsCanTalkCheckBox->setEnabled(false); spectatorsSeeEverythingCheckBox->setEnabled(false); + createGameAsSpectatorCheckBox->setEnabled(false); descriptionEdit->setText(QString::fromStdString(gameInfo.description())); maxPlayersEdit->setValue(gameInfo.max_players()); @@ -200,6 +204,7 @@ void DlgCreateGame::actReset() spectatorsNeedPasswordCheckBox->setChecked(false); spectatorsCanTalkCheckBox->setChecked(false); spectatorsSeeEverythingCheckBox->setChecked(false); + createGameAsSpectatorCheckBox->setChecked(false); QMapIterator gameTypeCheckBoxIterator(gameTypeCheckBoxes); while (gameTypeCheckBoxIterator.hasNext()) { @@ -226,6 +231,7 @@ void DlgCreateGame::actOK() cmd.set_spectators_can_talk(spectatorsCanTalkCheckBox->isChecked()); cmd.set_spectators_see_everything(spectatorsSeeEverythingCheckBox->isChecked()); cmd.set_join_as_judge(QApplication::keyboardModifiers() & Qt::ShiftModifier); + cmd.set_join_as_spectator(createGameAsSpectatorCheckBox->isChecked()); QString gameTypes = QString(); QMapIterator gameTypeCheckBoxIterator(gameTypeCheckBoxes); @@ -247,6 +253,7 @@ void DlgCreateGame::actOK() SettingsCache::instance().setSpectatorsNeedPassword(spectatorsNeedPasswordCheckBox->isChecked()); SettingsCache::instance().setSpectatorsCanTalk(spectatorsCanTalkCheckBox->isChecked()); SettingsCache::instance().setSpectatorsCanSeeEverything(spectatorsSeeEverythingCheckBox->isChecked()); + SettingsCache::instance().setCreateGameAsSpectator(createGameAsSpectatorCheckBox->isChecked()); SettingsCache::instance().setGameTypes(gameTypes); } PendingCommand *pend = room->prepareRoomCommand(cmd); diff --git a/cockatrice/src/dlg_creategame.h b/cockatrice/src/dlg_creategame.h index 08b9c8dfc..90e3ba2b0 100644 --- a/cockatrice/src/dlg_creategame.h +++ b/cockatrice/src/dlg_creategame.h @@ -39,7 +39,7 @@ private: QSpinBox *maxPlayersEdit; QCheckBox *onlyBuddiesCheckBox, *onlyRegisteredCheckBox; QCheckBox *spectatorsAllowedCheckBox, *spectatorsNeedPasswordCheckBox, *spectatorsCanTalkCheckBox, - *spectatorsSeeEverythingCheckBox; + *spectatorsSeeEverythingCheckBox, *createGameAsSpectatorCheckBox; QDialogButtonBox *buttonBox; QPushButton *clearButton; QCheckBox *rememberGameSettings; diff --git a/cockatrice/src/dlg_edit_password.cpp b/cockatrice/src/dlg_edit_password.cpp index bf43d5ed6..6bc1f382d 100644 --- a/cockatrice/src/dlg_edit_password.cpp +++ b/cockatrice/src/dlg_edit_password.cpp @@ -10,12 +10,13 @@ DlgEditPassword::DlgEditPassword(QWidget *parent) : QDialog(parent) { - oldPasswordLabel = new QLabel(tr("Old password:")); oldPasswordEdit = new QLineEdit(); - if (SettingsCache::instance().servers().getSavePassword()) - oldPasswordEdit->setText(SettingsCache::instance().servers().getPassword()); + auto &servers = SettingsCache::instance().servers(); + if (servers.getSavePassword()) { + oldPasswordEdit->setText(servers.getPassword()); + } oldPasswordLabel->setBuddy(oldPasswordEdit); oldPasswordEdit->setEchoMode(QLineEdit::Password); @@ -59,7 +60,5 @@ void DlgEditPassword::actOk() return; } - // always save the password so it will be picked up by the connect dialog - SettingsCache::instance().servers().setPassword(newPasswordEdit->text()); accept(); } diff --git a/cockatrice/src/dlg_filter_games.cpp b/cockatrice/src/dlg_filter_games.cpp index c76497053..6b99d0046 100644 --- a/cockatrice/src/dlg_filter_games.cpp +++ b/cockatrice/src/dlg_filter_games.cpp @@ -1,6 +1,7 @@ #include "dlg_filter_games.h" #include +#include #include #include #include @@ -15,13 +16,22 @@ DlgFilterGames::DlgFilterGames(const QMap &_allGameTypes, const GamesProxyModel *_gamesProxyModel, QWidget *parent) - : QDialog(parent), allGameTypes(_allGameTypes), gamesProxyModel(_gamesProxyModel) + : QDialog(parent), allGameTypes(_allGameTypes), gamesProxyModel(_gamesProxyModel), + gameAgeMap({{QTime(), tr("no limit")}, + {QTime(0, 5), tr("5 minutes")}, + {QTime(0, 10), tr("10 minutes")}, + {QTime(0, 30), tr("30 minutes")}, + {QTime(1, 0), tr("1 hour")}, + {QTime(2, 0), tr("2 hours")}}) { showBuddiesOnlyGames = new QCheckBox(tr("Show '&buddies only' games")); showBuddiesOnlyGames->setChecked(gamesProxyModel->getShowBuddiesOnlyGames()); - unavailableGamesVisibleCheckBox = new QCheckBox(tr("Show &unavailable games")); - unavailableGamesVisibleCheckBox->setChecked(gamesProxyModel->getUnavailableGamesVisible()); + showFullGames = new QCheckBox(tr("Show &full games")); + showFullGames->setChecked(gamesProxyModel->getShowFullGames()); + + showGamesThatStarted = new QCheckBox(tr("Show games &that have started")); + showGamesThatStarted->setChecked(gamesProxyModel->getShowGamesThatStarted()); showPasswordProtectedGames = new QCheckBox(tr("Show &password protected games")); showPasswordProtectedGames->setChecked(gamesProxyModel->getShowPasswordProtectedGames()); @@ -29,29 +39,39 @@ DlgFilterGames::DlgFilterGames(const QMap &_allGameTypes, hideIgnoredUserGames = new QCheckBox(tr("Hide '&ignored user' games")); hideIgnoredUserGames->setChecked(gamesProxyModel->getHideIgnoredUserGames()); + maxGameAgeComboBox = new QComboBox(); + maxGameAgeComboBox->setEditable(false); + maxGameAgeComboBox->addItems(gameAgeMap.values()); + QTime gameAge = gamesProxyModel->getMaxGameAge(); + maxGameAgeComboBox->setCurrentIndex(gameAgeMap.keys().indexOf(gameAge)); // index is -1 if unknown + auto *maxGameAgeLabel = new QLabel(tr("&Newer than:")); + maxGameAgeLabel->setBuddy(maxGameAgeComboBox); + gameNameFilterEdit = new QLineEdit; gameNameFilterEdit->setText(gamesProxyModel->getGameNameFilter()); - QLabel *gameNameFilterLabel = new QLabel(tr("Game &description:")); + auto *gameNameFilterLabel = new QLabel(tr("Game &description:")); gameNameFilterLabel->setBuddy(gameNameFilterEdit); creatorNameFilterEdit = new QLineEdit; creatorNameFilterEdit->setText(gamesProxyModel->getCreatorNameFilter()); - QLabel *creatorNameFilterLabel = new QLabel(tr("&Creator name:")); + auto *creatorNameFilterLabel = new QLabel(tr("&Creator name:")); creatorNameFilterLabel->setBuddy(creatorNameFilterEdit); - QGridLayout *generalGrid = new QGridLayout; + auto *generalGrid = new QGridLayout; generalGrid->addWidget(gameNameFilterLabel, 0, 0); generalGrid->addWidget(gameNameFilterEdit, 0, 1); generalGrid->addWidget(creatorNameFilterLabel, 1, 0); generalGrid->addWidget(creatorNameFilterEdit, 1, 1); + generalGrid->addWidget(maxGameAgeLabel, 2, 0); + generalGrid->addWidget(maxGameAgeComboBox, 2, 1); generalGroupBox = new QGroupBox(tr("General")); generalGroupBox->setLayout(generalGrid); - QVBoxLayout *gameTypeFilterLayout = new QVBoxLayout; + auto *gameTypeFilterLayout = new QVBoxLayout; QMapIterator gameTypesIterator(allGameTypes); while (gameTypesIterator.hasNext()) { gameTypesIterator.next(); - QCheckBox *temp = new QCheckBox(gameTypesIterator.value()); + auto *temp = new QCheckBox(gameTypesIterator.value()); temp->setChecked(gamesProxyModel->getGameTypeFilter().contains(gameTypesIterator.key())); gameTypeFilterCheckBoxes.insert(gameTypesIterator.key(), temp); @@ -62,66 +82,95 @@ DlgFilterGames::DlgFilterGames(const QMap &_allGameTypes, gameTypeFilterGroupBox = new QGroupBox(tr("&Game types")); gameTypeFilterGroupBox->setLayout(gameTypeFilterLayout); } else - gameTypeFilterGroupBox = 0; + gameTypeFilterGroupBox = nullptr; - QLabel *maxPlayersFilterMinLabel = new QLabel(tr("at &least:")); + auto *maxPlayersFilterMinLabel = new QLabel(tr("at &least:")); maxPlayersFilterMinSpinBox = new QSpinBox; - maxPlayersFilterMinSpinBox->setMinimum(1); + maxPlayersFilterMinSpinBox->setMinimum(0); maxPlayersFilterMinSpinBox->setMaximum(99); maxPlayersFilterMinSpinBox->setValue(gamesProxyModel->getMaxPlayersFilterMin()); maxPlayersFilterMinLabel->setBuddy(maxPlayersFilterMinSpinBox); - QLabel *maxPlayersFilterMaxLabel = new QLabel(tr("at &most:")); + auto *maxPlayersFilterMaxLabel = new QLabel(tr("at &most:")); maxPlayersFilterMaxSpinBox = new QSpinBox; - maxPlayersFilterMaxSpinBox->setMinimum(1); + maxPlayersFilterMaxSpinBox->setMinimum(0); maxPlayersFilterMaxSpinBox->setMaximum(99); maxPlayersFilterMaxSpinBox->setValue(gamesProxyModel->getMaxPlayersFilterMax()); maxPlayersFilterMaxLabel->setBuddy(maxPlayersFilterMaxSpinBox); - QGridLayout *maxPlayersFilterLayout = new QGridLayout; + auto *maxPlayersFilterLayout = new QGridLayout; maxPlayersFilterLayout->addWidget(maxPlayersFilterMinLabel, 0, 0); maxPlayersFilterLayout->addWidget(maxPlayersFilterMinSpinBox, 0, 1); maxPlayersFilterLayout->addWidget(maxPlayersFilterMaxLabel, 1, 0); maxPlayersFilterLayout->addWidget(maxPlayersFilterMaxSpinBox, 1, 1); - QGroupBox *maxPlayersGroupBox = new QGroupBox(tr("Maximum player count")); + auto *maxPlayersGroupBox = new QGroupBox(tr("Maximum player count")); maxPlayersGroupBox->setLayout(maxPlayersFilterLayout); - QGridLayout *restrictionsLayout = new QGridLayout; - restrictionsLayout->addWidget(unavailableGamesVisibleCheckBox, 0, 0); - restrictionsLayout->addWidget(showPasswordProtectedGames, 1, 0); - restrictionsLayout->addWidget(showBuddiesOnlyGames, 2, 0); - restrictionsLayout->addWidget(hideIgnoredUserGames, 3, 0); + auto *restrictionsLayout = new QGridLayout; + restrictionsLayout->addWidget(showFullGames, 0, 0); + restrictionsLayout->addWidget(showGamesThatStarted, 1, 0); + restrictionsLayout->addWidget(showPasswordProtectedGames, 2, 0); + restrictionsLayout->addWidget(showBuddiesOnlyGames, 3, 0); + restrictionsLayout->addWidget(hideIgnoredUserGames, 4, 0); - QGroupBox *restrictionsGroupBox = new QGroupBox(tr("Restrictions")); + auto *restrictionsGroupBox = new QGroupBox(tr("Restrictions")); restrictionsGroupBox->setLayout(restrictionsLayout); - QGridLayout *leftGrid = new QGridLayout; + showOnlyIfSpectatorsCanWatch = new QCheckBox(tr("Show games only if &spectators can watch")); + showOnlyIfSpectatorsCanWatch->setChecked(gamesProxyModel->getShowOnlyIfSpectatorsCanWatch()); + connect(showOnlyIfSpectatorsCanWatch, SIGNAL(toggled(bool)), this, SLOT(toggleSpectatorCheckboxEnabledness(bool))); + + showSpectatorPasswordProtected = new QCheckBox(tr("Show spectator password p&rotected games")); + showSpectatorPasswordProtected->setChecked(gamesProxyModel->getShowSpectatorPasswordProtected()); + showOnlyIfSpectatorsCanChat = new QCheckBox(tr("Show only if spectators can ch&at")); + showOnlyIfSpectatorsCanChat->setChecked(gamesProxyModel->getShowOnlyIfSpectatorsCanChat()); + showOnlyIfSpectatorsCanSeeHands = new QCheckBox(tr("Show only if spectators can see &hands")); + showOnlyIfSpectatorsCanSeeHands->setChecked(gamesProxyModel->getShowOnlyIfSpectatorsCanSeeHands()); + toggleSpectatorCheckboxEnabledness(getShowOnlyIfSpectatorsCanWatch()); + + auto *spectatorsLayout = new QGridLayout; + spectatorsLayout->addWidget(showOnlyIfSpectatorsCanWatch, 0, 0); + spectatorsLayout->addWidget(showSpectatorPasswordProtected, 1, 0); + spectatorsLayout->addWidget(showOnlyIfSpectatorsCanChat, 2, 0); + spectatorsLayout->addWidget(showOnlyIfSpectatorsCanSeeHands, 3, 0); + + auto *spectatorsGroupBox = new QGroupBox(tr("Spectators")); + spectatorsGroupBox->setLayout(spectatorsLayout); + + auto *leftGrid = new QGridLayout; leftGrid->addWidget(generalGroupBox, 0, 0, 1, 2); leftGrid->addWidget(maxPlayersGroupBox, 2, 0, 1, 2); leftGrid->addWidget(restrictionsGroupBox, 3, 0, 1, 2); - QVBoxLayout *leftColumn = new QVBoxLayout; + auto *leftColumn = new QVBoxLayout; leftColumn->addLayout(leftGrid); leftColumn->addStretch(); - QVBoxLayout *rightColumn = new QVBoxLayout; - rightColumn->addWidget(gameTypeFilterGroupBox); + auto *rightGrid = new QGridLayout; + rightGrid->addWidget(gameTypeFilterGroupBox, 0, 0, 1, 1); + rightGrid->addWidget(spectatorsGroupBox, 1, 0, 1, 1); - QHBoxLayout *hbox = new QHBoxLayout; + auto *rightColumn = new QVBoxLayout; + rightColumn->addLayout(rightGrid); + rightColumn->addStretch(); + + auto *hbox = new QHBoxLayout; hbox->addLayout(leftColumn); hbox->addLayout(rightColumn); - QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); connect(buttonBox, SIGNAL(accepted()), this, SLOT(actOk())); connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject())); - QVBoxLayout *mainLayout = new QVBoxLayout; + auto *mainLayout = new QVBoxLayout; mainLayout->addLayout(hbox); mainLayout->addWidget(buttonBox); setLayout(mainLayout); setWindowTitle(tr("Filter games")); + + setFixedHeight(sizeHint().height()); } void DlgFilterGames::actOk() @@ -129,14 +178,21 @@ void DlgFilterGames::actOk() accept(); } -bool DlgFilterGames::getUnavailableGamesVisible() const +void DlgFilterGames::toggleSpectatorCheckboxEnabledness(bool spectatorsEnabled) { - return unavailableGamesVisibleCheckBox->isChecked(); + showSpectatorPasswordProtected->setDisabled(!spectatorsEnabled); + showOnlyIfSpectatorsCanChat->setDisabled(!spectatorsEnabled); + showOnlyIfSpectatorsCanSeeHands->setDisabled(!spectatorsEnabled); } -void DlgFilterGames::setUnavailableGamesVisible(bool _unavailableGamesVisible) +bool DlgFilterGames::getShowFullGames() const { - unavailableGamesVisibleCheckBox->setChecked(_unavailableGamesVisible); + return showFullGames->isChecked(); +} + +bool DlgFilterGames::getShowGamesThatStarted() const +{ + return showGamesThatStarted->isChecked(); } bool DlgFilterGames::getShowBuddiesOnlyGames() const @@ -220,9 +276,38 @@ int DlgFilterGames::getMaxPlayersFilterMax() const return maxPlayersFilterMaxSpinBox->value(); } +const QTime &DlgFilterGames::getMaxGameAge() const +{ + int index = maxGameAgeComboBox->currentIndex(); + if (index < 0 || index >= gameAgeMap.size()) { // index is out of bounds + return gamesProxyModel->getMaxGameAge(); // leave the setting unchanged + } + return gameAgeMap.keys().at(index); +} + void DlgFilterGames::setMaxPlayersFilter(int _maxPlayersFilterMin, int _maxPlayersFilterMax) { maxPlayersFilterMinSpinBox->setValue(_maxPlayersFilterMin); maxPlayersFilterMaxSpinBox->setValue(_maxPlayersFilterMax == -1 ? maxPlayersFilterMaxSpinBox->maximum() : _maxPlayersFilterMax); } + +bool DlgFilterGames::getShowOnlyIfSpectatorsCanWatch() const +{ + return showOnlyIfSpectatorsCanWatch->isChecked(); +} + +bool DlgFilterGames::getShowSpectatorPasswordProtected() const +{ + return showSpectatorPasswordProtected->isEnabled() && showSpectatorPasswordProtected->isChecked(); +} + +bool DlgFilterGames::getShowOnlyIfSpectatorsCanChat() const +{ + return showOnlyIfSpectatorsCanChat->isEnabled() && showOnlyIfSpectatorsCanChat->isChecked(); +} + +bool DlgFilterGames::getShowOnlyIfSpectatorsCanSeeHands() const +{ + return showOnlyIfSpectatorsCanSeeHands->isEnabled() && showOnlyIfSpectatorsCanSeeHands->isChecked(); +} diff --git a/cockatrice/src/dlg_filter_games.h b/cockatrice/src/dlg_filter_games.h index 1f37bf31c..24c048ca8 100644 --- a/cockatrice/src/dlg_filter_games.h +++ b/cockatrice/src/dlg_filter_games.h @@ -4,11 +4,14 @@ #include "gamesmodel.h" #include +#include #include #include #include +#include class QCheckBox; +class QComboBox; class QGroupBox; class QLineEdit; class QSpinBox; @@ -19,7 +22,8 @@ class DlgFilterGames : public QDialog private: QGroupBox *generalGroupBox; QCheckBox *showBuddiesOnlyGames; - QCheckBox *unavailableGamesVisibleCheckBox; + QCheckBox *showFullGames; + QCheckBox *showGamesThatStarted; QCheckBox *showPasswordProtectedGames; QCheckBox *hideIgnoredUserGames; QLineEdit *gameNameFilterEdit; @@ -27,20 +31,27 @@ private: QMap gameTypeFilterCheckBoxes; QSpinBox *maxPlayersFilterMinSpinBox; QSpinBox *maxPlayersFilterMaxSpinBox; + QComboBox *maxGameAgeComboBox; + + QCheckBox *showOnlyIfSpectatorsCanWatch; + QCheckBox *showSpectatorPasswordProtected; + QCheckBox *showOnlyIfSpectatorsCanChat; + QCheckBox *showOnlyIfSpectatorsCanSeeHands; const QMap &allGameTypes; const GamesProxyModel *gamesProxyModel; private slots: void actOk(); + void toggleSpectatorCheckboxEnabledness(bool spectatorsEnabled); public: DlgFilterGames(const QMap &_allGameTypes, const GamesProxyModel *_gamesProxyModel, QWidget *parent = nullptr); - bool getUnavailableGamesVisible() const; - void setUnavailableGamesVisible(bool _unavailableGamesVisible); + bool getShowFullGames() const; + bool getShowGamesThatStarted() const; bool getShowPasswordProtectedGames() const; void setShowPasswordProtectedGames(bool _passwordProtectedGamesHidden); bool getShowBuddiesOnlyGames() const; @@ -56,6 +67,12 @@ public: int getMaxPlayersFilterMin() const; int getMaxPlayersFilterMax() const; void setMaxPlayersFilter(int _maxPlayersFilterMin, int _maxPlayersFilterMax); + const QTime &getMaxGameAge() const; + const QMap gameAgeMap; + bool getShowOnlyIfSpectatorsCanWatch() const; + bool getShowSpectatorPasswordProtected() const; + bool getShowOnlyIfSpectatorsCanChat() const; + bool getShowOnlyIfSpectatorsCanSeeHands() const; }; #endif diff --git a/cockatrice/src/dlg_forgotpasswordchallenge.cpp b/cockatrice/src/dlg_forgotpasswordchallenge.cpp index 7b85dd44e..44a182583 100644 --- a/cockatrice/src/dlg_forgotpasswordchallenge.cpp +++ b/cockatrice/src/dlg_forgotpasswordchallenge.cpp @@ -12,14 +12,13 @@ DlgForgotPasswordChallenge::DlgForgotPasswordChallenge(QWidget *parent) : QDialog(parent) { - QString lastfphost; QString lastfpport; QString lastfpplayername; ServersSettings &servers = SettingsCache::instance().servers(); - lastfphost = servers.getHostname("server.cockatrice.us"); - lastfpport = servers.getPort("4747"); - lastfpplayername = servers.getPlayerName("Player"); + lastfphost = servers.getHostname(); + lastfpport = servers.getPort(); + lastfpplayername = servers.getPlayerName(); if (!servers.getFPHostname().isEmpty() && !servers.getFPPort().isEmpty() && !servers.getFPPlayerName().isEmpty()) { lastfphost = servers.getFPHostname(); @@ -28,12 +27,15 @@ DlgForgotPasswordChallenge::DlgForgotPasswordChallenge(QWidget *parent) : QDialo } if (servers.getFPHostname().isEmpty() && servers.getFPPort().isEmpty() && servers.getFPPlayerName().isEmpty()) { - QMessageBox::warning(this, tr("Forgot Password Challenge Warning"), - tr("Oops, looks like something has gone wrong. Please restart the forgot password " - "process by using the forgot password button on the connection screen.")); + QMessageBox::warning(this, tr("Reset Password Challenge Warning"), + tr("A problem has occurred. Please try to request a new password again.")); reject(); } + infoLabel = + new QLabel(tr("Enter the information of the server and the account you'd like to request a new password for.")); + infoLabel->setWordWrap(true); + hostLabel = new QLabel(tr("&Host:")); hostEdit = new QLineEdit(lastfphost); hostLabel->setBuddy(hostEdit); @@ -60,14 +62,15 @@ DlgForgotPasswordChallenge::DlgForgotPasswordChallenge(QWidget *parent) : QDialo } QGridLayout *grid = new QGridLayout; - grid->addWidget(hostLabel, 0, 0); - grid->addWidget(hostEdit, 0, 1); - grid->addWidget(portLabel, 1, 0); - grid->addWidget(portEdit, 1, 1); - grid->addWidget(playernameLabel, 2, 0); - grid->addWidget(playernameEdit, 2, 1); - grid->addWidget(emailLabel, 3, 0); - grid->addWidget(emailEdit, 3, 1); + grid->addWidget(infoLabel, 0, 0, 1, 2); + grid->addWidget(hostLabel, 1, 0); + grid->addWidget(hostEdit, 1, 1); + grid->addWidget(portLabel, 2, 0); + grid->addWidget(portEdit, 2, 1); + grid->addWidget(playernameLabel, 3, 0); + grid->addWidget(playernameEdit, 3, 1); + grid->addWidget(emailLabel, 4, 0); + grid->addWidget(emailEdit, 4, 1); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); connect(buttonBox, SIGNAL(accepted()), this, SLOT(actOk())); @@ -78,7 +81,7 @@ DlgForgotPasswordChallenge::DlgForgotPasswordChallenge(QWidget *parent) : QDialo mainLayout->addWidget(buttonBox); setLayout(mainLayout); - setWindowTitle(tr("Forgot Password Challenge")); + setWindowTitle(tr("Reset Password Challenge")); setFixedHeight(sizeHint().height()); setMinimumWidth(300); } @@ -86,7 +89,7 @@ DlgForgotPasswordChallenge::DlgForgotPasswordChallenge(QWidget *parent) : QDialo void DlgForgotPasswordChallenge::actOk() { if (emailEdit->text().isEmpty()) { - QMessageBox::critical(this, tr("Forgot Password Challenge Warning"), tr("The email address can't be empty.")); + QMessageBox::critical(this, tr("Reset Password Challenge Error"), tr("The email address can't be empty.")); return; } diff --git a/cockatrice/src/dlg_forgotpasswordchallenge.h b/cockatrice/src/dlg_forgotpasswordchallenge.h index ca8867ce2..34aa237bd 100644 --- a/cockatrice/src/dlg_forgotpasswordchallenge.h +++ b/cockatrice/src/dlg_forgotpasswordchallenge.h @@ -34,7 +34,7 @@ private slots: void actOk(); private: - QLabel *hostLabel, *portLabel, *playernameLabel, *emailLabel; + QLabel *infoLabel, *hostLabel, *portLabel, *playernameLabel, *emailLabel; QLineEdit *hostEdit, *portEdit, *playernameEdit, *emailEdit; }; diff --git a/cockatrice/src/dlg_forgotpasswordrequest.cpp b/cockatrice/src/dlg_forgotpasswordrequest.cpp index cb5f620cf..e12bd1f0a 100644 --- a/cockatrice/src/dlg_forgotpasswordrequest.cpp +++ b/cockatrice/src/dlg_forgotpasswordrequest.cpp @@ -12,14 +12,13 @@ DlgForgotPasswordRequest::DlgForgotPasswordRequest(QWidget *parent) : QDialog(parent) { - QString lastfphost; QString lastfpport; QString lastfpplayername; ServersSettings &servers = SettingsCache::instance().servers(); - lastfphost = servers.getHostname("server.cockatrice.us"); - lastfpport = servers.getPort("4747"); - lastfpplayername = servers.getPlayerName("Player"); + lastfphost = servers.getHostname(); + lastfpport = servers.getPort(); + lastfpplayername = servers.getPlayerName(); if (!servers.getFPHostname().isEmpty() && !servers.getFPPort().isEmpty() && !servers.getFPPlayerName().isEmpty()) { lastfphost = servers.getFPHostname(); @@ -27,6 +26,9 @@ DlgForgotPasswordRequest::DlgForgotPasswordRequest(QWidget *parent) : QDialog(pa lastfpplayername = servers.getFPPlayerName(); } + infoLabel = new QLabel(tr("Enter the information of the server you'd like to request a new password for.")); + infoLabel->setWordWrap(true); + hostLabel = new QLabel(tr("&Host:")); hostEdit = new QLineEdit(lastfphost); hostLabel->setBuddy(hostEdit); @@ -40,12 +42,13 @@ DlgForgotPasswordRequest::DlgForgotPasswordRequest(QWidget *parent) : QDialog(pa playernameLabel->setBuddy(playernameEdit); QGridLayout *grid = new QGridLayout; - grid->addWidget(hostLabel, 0, 0); - grid->addWidget(hostEdit, 0, 1); - grid->addWidget(portLabel, 1, 0); - grid->addWidget(portEdit, 1, 1); - grid->addWidget(playernameLabel, 2, 0); - grid->addWidget(playernameEdit, 2, 1); + grid->addWidget(infoLabel, 0, 0, 1, 2); + grid->addWidget(hostLabel, 1, 0); + grid->addWidget(hostEdit, 1, 1); + grid->addWidget(portLabel, 2, 0); + grid->addWidget(portEdit, 2, 1); + grid->addWidget(playernameLabel, 3, 0); + grid->addWidget(playernameEdit, 3, 1); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); connect(buttonBox, SIGNAL(accepted()), this, SLOT(actOk())); @@ -56,7 +59,7 @@ DlgForgotPasswordRequest::DlgForgotPasswordRequest(QWidget *parent) : QDialog(pa mainLayout->addWidget(buttonBox); setLayout(mainLayout); - setWindowTitle(tr("Forgot Password Request")); + setWindowTitle(tr("Reset Password Request")); setFixedHeight(sizeHint().height()); setMinimumWidth(300); } @@ -64,7 +67,7 @@ DlgForgotPasswordRequest::DlgForgotPasswordRequest(QWidget *parent) : QDialog(pa void DlgForgotPasswordRequest::actOk() { if (playernameEdit->text().isEmpty()) { - QMessageBox::critical(this, tr("Forgot Password Request Warning"), tr("The player name can't be empty.")); + QMessageBox::critical(this, tr("Reset Password Error"), tr("The player name can't be empty.")); return; } diff --git a/cockatrice/src/dlg_forgotpasswordrequest.h b/cockatrice/src/dlg_forgotpasswordrequest.h index e796ae2f6..c423444c0 100644 --- a/cockatrice/src/dlg_forgotpasswordrequest.h +++ b/cockatrice/src/dlg_forgotpasswordrequest.h @@ -30,7 +30,7 @@ private slots: void actOk(); private: - QLabel *hostLabel, *portLabel, *playernameLabel; + QLabel *infoLabel, *hostLabel, *portLabel, *playernameLabel; QLineEdit *hostEdit, *portEdit, *playernameEdit; }; diff --git a/cockatrice/src/dlg_forgotpasswordreset.cpp b/cockatrice/src/dlg_forgotpasswordreset.cpp index 8d16a346d..6945eee08 100644 --- a/cockatrice/src/dlg_forgotpasswordreset.cpp +++ b/cockatrice/src/dlg_forgotpasswordreset.cpp @@ -12,14 +12,13 @@ DlgForgotPasswordReset::DlgForgotPasswordReset(QWidget *parent) : QDialog(parent) { - QString lastfphost; QString lastfpport; QString lastfpplayername; ServersSettings &servers = SettingsCache::instance().servers(); - lastfphost = servers.getHostname("server.cockatrice.us"); - lastfpport = servers.getPort("4747"); - lastfpplayername = servers.getPlayerName("Player"); + lastfphost = servers.getHostname(); + lastfpport = servers.getPort(); + lastfpplayername = servers.getPlayerName(); if (!servers.getFPHostname().isEmpty() && !servers.getFPPort().isEmpty() && !servers.getFPPlayerName().isEmpty()) { lastfphost = servers.getFPHostname(); @@ -28,12 +27,14 @@ DlgForgotPasswordReset::DlgForgotPasswordReset(QWidget *parent) : QDialog(parent } if (servers.getFPHostname().isEmpty() && servers.getFPPort().isEmpty() && servers.getFPPlayerName().isEmpty()) { - QMessageBox::warning(this, tr("Forgot Password Reset Warning"), - tr("Oops, looks like something has gone wrong. Please re-start the forgot password " - "process by using the forgot password button on the connection screen.")); + QMessageBox::warning(this, tr("Reset Password Warning"), + tr("A problem has occurred. Please try to request a new password again.")); reject(); } + infoLabel = new QLabel(tr("Enter the received token and the new password in order to set your new password.")); + infoLabel->setWordWrap(true); + hostLabel = new QLabel(tr("&Host:")); hostEdit = new QLineEdit(lastfphost); hostLabel->setBuddy(hostEdit); @@ -70,18 +71,19 @@ DlgForgotPasswordReset::DlgForgotPasswordReset(QWidget *parent) : QDialog(parent } QGridLayout *grid = new QGridLayout; - grid->addWidget(hostLabel, 0, 0); - grid->addWidget(hostEdit, 0, 1); - grid->addWidget(portLabel, 1, 0); - grid->addWidget(portEdit, 1, 1); - grid->addWidget(playernameLabel, 2, 0); - grid->addWidget(playernameEdit, 2, 1); - grid->addWidget(tokenLabel, 3, 0); - grid->addWidget(tokenEdit, 3, 1); - grid->addWidget(newpasswordLabel, 4, 0); - grid->addWidget(newpasswordEdit, 4, 1); - grid->addWidget(newpasswordverifyLabel, 5, 0); - grid->addWidget(newpasswordverifyEdit, 5, 1); + grid->addWidget(infoLabel, 0, 0, 1, 2); + grid->addWidget(hostLabel, 1, 0); + grid->addWidget(hostEdit, 1, 1); + grid->addWidget(portLabel, 2, 0); + grid->addWidget(portEdit, 2, 1); + grid->addWidget(playernameLabel, 3, 0); + grid->addWidget(playernameEdit, 3, 1); + grid->addWidget(tokenLabel, 4, 0); + grid->addWidget(tokenEdit, 4, 1); + grid->addWidget(newpasswordLabel, 5, 0); + grid->addWidget(newpasswordEdit, 5, 1); + grid->addWidget(newpasswordverifyLabel, 6, 0); + grid->addWidget(newpasswordverifyEdit, 6, 1); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); connect(buttonBox, SIGNAL(accepted()), this, SLOT(actOk())); @@ -92,7 +94,7 @@ DlgForgotPasswordReset::DlgForgotPasswordReset(QWidget *parent) : QDialog(parent mainLayout->addWidget(buttonBox); setLayout(mainLayout); - setWindowTitle(tr("Forgot Password Reset")); + setWindowTitle(tr("Reset Password")); setFixedHeight(sizeHint().height()); setMinimumWidth(300); } @@ -100,22 +102,22 @@ DlgForgotPasswordReset::DlgForgotPasswordReset(QWidget *parent) : QDialog(parent void DlgForgotPasswordReset::actOk() { if (playernameEdit->text().isEmpty()) { - QMessageBox::critical(this, tr("Forgot Password Reset Warning"), tr("The player name can't be empty.")); + QMessageBox::critical(this, tr("Reset Password Error"), tr("The player name can't be empty.")); return; } if (tokenEdit->text().isEmpty()) { - QMessageBox::critical(this, tr("Forgot Password Reset Warning"), tr("The token can't be empty.")); + QMessageBox::critical(this, tr("Reset Password Error"), tr("The token can't be empty.")); return; } if (newpasswordEdit->text().isEmpty()) { - QMessageBox::critical(this, tr("Forgot Password Reset Warning"), tr("The new password can't be empty.")); + QMessageBox::critical(this, tr("Reset Password Error"), tr("The new password can't be empty.")); return; } if (newpasswordEdit->text() != newpasswordverifyEdit->text()) { - QMessageBox::critical(this, tr("Forgot Password Reset Warning"), tr("The passwords do not match.")); + QMessageBox::critical(this, tr("Reset Password Error"), tr("The passwords do not match.")); return; } diff --git a/cockatrice/src/dlg_forgotpasswordreset.h b/cockatrice/src/dlg_forgotpasswordreset.h index 4963e08a5..ed018ee7f 100644 --- a/cockatrice/src/dlg_forgotpasswordreset.h +++ b/cockatrice/src/dlg_forgotpasswordreset.h @@ -38,7 +38,8 @@ private slots: void actOk(); private: - QLabel *hostLabel, *portLabel, *playernameLabel, *tokenLabel, *newpasswordLabel, *newpasswordverifyLabel; + QLabel *infoLabel, *hostLabel, *portLabel, *playernameLabel, *tokenLabel, *newpasswordLabel, + *newpasswordverifyLabel; QLineEdit *hostEdit, *portEdit, *playernameEdit, *tokenEdit, *newpasswordEdit, *newpasswordverifyEdit; }; diff --git a/cockatrice/src/window_sets.cpp b/cockatrice/src/dlg_manage_sets.cpp similarity index 94% rename from cockatrice/src/window_sets.cpp rename to cockatrice/src/dlg_manage_sets.cpp index 02898c8b4..496431c1e 100644 --- a/cockatrice/src/window_sets.cpp +++ b/cockatrice/src/dlg_manage_sets.cpp @@ -1,4 +1,4 @@ -#include "window_sets.h" +#include "dlg_manage_sets.h" #include "customlineedit.h" #include "main.h" @@ -84,6 +84,7 @@ WndSets::WndSets(QWidget *parent) : QMainWindow(parent) displayModel->setDynamicSortFilter(false); view = new QTreeView; view->setModel(displayModel); + view->setMinimumSize(QSize(500, 250)); view->setAlternatingRowColors(true); view->setUniformRowHeights(true); @@ -128,16 +129,17 @@ WndSets::WndSets(QWidget *parent) : QMainWindow(parent) labNotes->setWordWrap(true); labNotes->setTextInteractionFlags(Qt::TextBrowserInteraction); labNotes->setOpenExternalLinks(true); - labNotes->setText("" + tr("Deck Editor") + ": " + - tr("Only cards in enabled sets will appear in the card list of the deck editor") + "

" + - "" + tr("Card Art") + ": " + tr("Image priority is decided in the following order") + - "
  1. " + tr("CUSTOM Folder") + - " (" + - tr("How to use custom card art") + ")
  2. " + tr("Enabled Sets (Top to Bottom)") + - "
  3. " + tr("Disabled Sets (Top to Bottom)") + "
"); + labNotes->setText(tr("Use CTRL+A to select all sets in the view.") + "
" + tr("Deck Editor") + ": " + + tr("Only cards in enabled sets will appear in the card list of the deck editor.") + "
" + + tr("Card Art") + ": " + tr("Image priority is decided in the following order:") + "
" + + tr("first the CUSTOM Folder (%1), then the Enabled Sets in this dialog (Top to Bottom)", + "%1 is a link to the wiki") + .arg("" + + tr("How to use custom card art") + "")); QGridLayout *hintsGrid = new QGridLayout; + hintsGrid->setMargin(2); hintsGrid->addWidget(labNotes, 0, 0); hintsGroupBox = new QGroupBox(tr("Hints")); hintsGroupBox->setLayout(hintsGrid); @@ -163,8 +165,8 @@ WndSets::WndSets(QWidget *parent) : QMainWindow(parent) connect(buttonBox, SIGNAL(rejected()), this, SLOT(actRestore())); mainLayout = new QGridLayout; - mainLayout->addWidget(setsEditToolBar, 1, 0, 2, 1); mainLayout->addLayout(filterBox, 0, 1, 1, 2); + mainLayout->addWidget(setsEditToolBar, 1, 0, 2, 1); mainLayout->addWidget(view, 1, 1, 1, 2); mainLayout->addWidget(enableAllButton, 2, 1); mainLayout->addWidget(disableAllButton, 2, 2); @@ -184,7 +186,7 @@ WndSets::WndSets(QWidget *parent) : QMainWindow(parent) setCentralWidget(centralWidget); setWindowTitle(tr("Manage sets")); - resize(700, 400); + resize(800, 500); } WndSets::~WndSets() diff --git a/cockatrice/src/window_sets.h b/cockatrice/src/dlg_manage_sets.h similarity index 97% rename from cockatrice/src/window_sets.h rename to cockatrice/src/dlg_manage_sets.h index c9dd7f5ec..6e932b30b 100644 --- a/cockatrice/src/window_sets.h +++ b/cockatrice/src/dlg_manage_sets.h @@ -1,5 +1,5 @@ -#ifndef WINDOW_SETS_H -#define WINDOW_SETS_H +#ifndef DLG_MANAGE_SETS_H +#define DLG_MANAGE_SETS_H #include #include diff --git a/cockatrice/src/dlg_register.cpp b/cockatrice/src/dlg_register.cpp index d0703eb21..2af05af6a 100644 --- a/cockatrice/src/dlg_register.cpp +++ b/cockatrice/src/dlg_register.cpp @@ -14,20 +14,24 @@ DlgRegister::DlgRegister(QWidget *parent) : QDialog(parent) { ServersSettings &servers = SettingsCache::instance().servers(); + infoLabel = new QLabel(tr("Enter your information and the information of the server you'd like to register to.\n" + "Your email will be used to verify your account.")); + infoLabel->setWordWrap(true); + hostLabel = new QLabel(tr("&Host:")); - hostEdit = new QLineEdit(servers.getHostname("server.cockatrice.us")); + hostEdit = new QLineEdit(servers.getHostname()); hostLabel->setBuddy(hostEdit); portLabel = new QLabel(tr("&Port:")); - portEdit = new QLineEdit(servers.getPort("4747")); + portEdit = new QLineEdit(servers.getPort()); portLabel->setBuddy(portEdit); playernameLabel = new QLabel(tr("Player &name:")); - playernameEdit = new QLineEdit(servers.getPlayerName("Player")); + playernameEdit = new QLineEdit(servers.getPlayerName()); playernameLabel->setBuddy(playernameEdit); passwordLabel = new QLabel(tr("P&assword:")); - passwordEdit = new QLineEdit(servers.getPassword()); + passwordEdit = new QLineEdit(); passwordLabel->setBuddy(passwordEdit); passwordEdit->setEchoMode(QLineEdit::Password); @@ -307,24 +311,25 @@ DlgRegister::DlgRegister(QWidget *parent) : QDialog(parent) realnameLabel->setBuddy(realnameEdit); QGridLayout *grid = new QGridLayout; - grid->addWidget(hostLabel, 0, 0); - grid->addWidget(hostEdit, 0, 1); - grid->addWidget(portLabel, 1, 0); - grid->addWidget(portEdit, 1, 1); - grid->addWidget(playernameLabel, 2, 0); - grid->addWidget(playernameEdit, 2, 1); - grid->addWidget(passwordLabel, 3, 0); - grid->addWidget(passwordEdit, 3, 1); - grid->addWidget(passwordConfirmationLabel, 4, 0); - grid->addWidget(passwordConfirmationEdit, 4, 1); - grid->addWidget(emailLabel, 5, 0); - grid->addWidget(emailEdit, 5, 1); - grid->addWidget(emailConfirmationLabel, 6, 0); - grid->addWidget(emailConfirmationEdit, 6, 1); - grid->addWidget(countryLabel, 8, 0); - grid->addWidget(countryEdit, 8, 1); - grid->addWidget(realnameLabel, 9, 0); - grid->addWidget(realnameEdit, 9, 1); + grid->addWidget(infoLabel, 0, 0, 1, 2); + grid->addWidget(hostLabel, 1, 0); + grid->addWidget(hostEdit, 1, 1); + grid->addWidget(portLabel, 2, 0); + grid->addWidget(portEdit, 2, 1); + grid->addWidget(playernameLabel, 3, 0); + grid->addWidget(playernameEdit, 3, 1); + grid->addWidget(passwordLabel, 4, 0); + grid->addWidget(passwordEdit, 4, 1); + grid->addWidget(passwordConfirmationLabel, 5, 0); + grid->addWidget(passwordConfirmationEdit, 5, 1); + grid->addWidget(emailLabel, 6, 0); + grid->addWidget(emailEdit, 6, 1); + grid->addWidget(emailConfirmationLabel, 7, 0); + grid->addWidget(emailConfirmationEdit, 7, 1); + grid->addWidget(countryLabel, 9, 0); + grid->addWidget(countryEdit, 9, 1); + grid->addWidget(realnameLabel, 10, 0); + grid->addWidget(realnameEdit, 10, 1); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); connect(buttonBox, SIGNAL(accepted()), this, SLOT(actOk())); @@ -355,12 +360,5 @@ void DlgRegister::actOk() return; } - ServersSettings &servers = SettingsCache::instance().servers(); - servers.setHostName(hostEdit->text()); - servers.setPort(portEdit->text()); - servers.setPlayerName(playernameEdit->text()); - // always save the password so it will be picked up by the connect dialog - servers.setPassword(passwordEdit->text()); - accept(); } diff --git a/cockatrice/src/dlg_register.h b/cockatrice/src/dlg_register.h index 59bf7c6f7..a18fe95c6 100644 --- a/cockatrice/src/dlg_register.h +++ b/cockatrice/src/dlg_register.h @@ -50,8 +50,8 @@ private slots: void actOk(); private: - QLabel *hostLabel, *portLabel, *playernameLabel, *passwordLabel, *passwordConfirmationLabel, *emailLabel, - *emailConfirmationLabel, *countryLabel, *realnameLabel; + QLabel *infoLabel, *hostLabel, *portLabel, *playernameLabel, *passwordLabel, *passwordConfirmationLabel, + *emailLabel, *emailConfirmationLabel, *countryLabel, *realnameLabel; QLineEdit *hostEdit, *portEdit, *playernameEdit, *passwordEdit, *passwordConfirmationEdit, *emailEdit, *emailConfirmationEdit, *realnameEdit; QComboBox *countryEdit; diff --git a/cockatrice/src/dlg_settings.cpp b/cockatrice/src/dlg_settings.cpp index 5a0c32985..85b0f8a50 100644 --- a/cockatrice/src/dlg_settings.cpp +++ b/cockatrice/src/dlg_settings.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -291,10 +292,12 @@ AppearanceSettingsPage::AppearanceSettingsPage() } connect(&themeBox, SIGNAL(currentIndexChanged(int)), this, SLOT(themeBoxChanged(int))); + connect(&openThemeButton, SIGNAL(clicked()), this, SLOT(openThemeLocation())); auto *themeGrid = new QGridLayout; themeGrid->addWidget(&themeLabel, 0, 0); themeGrid->addWidget(&themeBox, 0, 1); + themeGrid->addWidget(&openThemeButton, 1, 1); themeGroupBox = new QGroupBox; themeGroupBox->setLayout(themeGrid); @@ -370,10 +373,24 @@ void AppearanceSettingsPage::themeBoxChanged(int index) SettingsCache::instance().setThemeName(themeDirs.at(index)); } +void AppearanceSettingsPage::openThemeLocation() +{ + QString dir = SettingsCache::instance().getThemesPath(); + QDir dirDir = dir; + dirDir.cdUp(); + // open if dir exists, create if parent dir does exist + if (dirDir.exists() && dirDir.mkpath(dir)) { + QDesktopServices::openUrl(QUrl::fromLocalFile(dir)); + } else { + QMessageBox::critical(this, tr("Error"), tr("Could not create themes directory at '%1'.").arg(dir)); + } +} + void AppearanceSettingsPage::retranslateUi() { themeGroupBox->setTitle(tr("Theme settings")); themeLabel.setText(tr("Current theme:")); + openThemeButton.setText(tr("Open themes folder")); cardsGroupBox->setTitle(tr("Card rendering")); displayCardNamesCheckBox.setText(tr("Display card names on cards having a picture")); @@ -394,7 +411,7 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() notificationsEnabledCheckBox.setChecked(SettingsCache::instance().getNotificationsEnabled()); connect(¬ificationsEnabledCheckBox, SIGNAL(stateChanged(int)), &SettingsCache::instance(), SLOT(setNotificationsEnabled(int))); - connect(¬ificationsEnabledCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setSpecNotificationEnabled(int))); + connect(¬ificationsEnabledCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setNotificationEnabled(int))); specNotificationsEnabledCheckBox.setChecked(SettingsCache::instance().getSpectatorNotificationsEnabled()); specNotificationsEnabledCheckBox.setEnabled(SettingsCache::instance().getNotificationsEnabled()); @@ -456,9 +473,14 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() setLayout(mainLayout); } -void UserInterfaceSettingsPage::setSpecNotificationEnabled(int i) +void UserInterfaceSettingsPage::setNotificationEnabled(int i) { specNotificationsEnabledCheckBox.setEnabled(i != 0); + buddyConnectNotificationsEnabledCheckBox.setEnabled(i != 0); + if (i == 0) { + specNotificationsEnabledCheckBox.setChecked(false); + buddyConnectNotificationsEnabledCheckBox.setChecked(false); + } } void UserInterfaceSettingsPage::retranslateUi() diff --git a/cockatrice/src/dlg_settings.h b/cockatrice/src/dlg_settings.h index bf80db422..c23a29692 100644 --- a/cockatrice/src/dlg_settings.h +++ b/cockatrice/src/dlg_settings.h @@ -79,10 +79,12 @@ class AppearanceSettingsPage : public AbstractSettingsPage Q_OBJECT private slots: void themeBoxChanged(int index); + void openThemeLocation(); private: QLabel themeLabel; QComboBox themeBox; + QPushButton openThemeButton; QLabel minPlayersForMultiColumnLayoutLabel; QLabel maxFontSizeForCardsLabel; QCheckBox displayCardNamesCheckBox; @@ -106,7 +108,7 @@ class UserInterfaceSettingsPage : public AbstractSettingsPage { Q_OBJECT private slots: - void setSpecNotificationEnabled(int); + void setNotificationEnabled(int); private: QCheckBox notificationsEnabledCheckBox; diff --git a/cockatrice/src/dlg_update.cpp b/cockatrice/src/dlg_update.cpp index c679bd5c9..98d318adc 100644 --- a/cockatrice/src/dlg_update.cpp +++ b/cockatrice/src/dlg_update.cpp @@ -45,13 +45,17 @@ DlgUpdate::DlgUpdate(QWidget *parent) : QDialog(parent) connect(stopDownload, SIGNAL(clicked()), this, SLOT(cancelDownload())); connect(ok, SIGNAL(clicked()), this, SLOT(closeDialog())); - QVBoxLayout *parentLayout = new QVBoxLayout(this); + auto *parentLayout = new QVBoxLayout(this); parentLayout->addWidget(descriptionLabel); parentLayout->addWidget(statusLabel); parentLayout->addWidget(progress); parentLayout->addWidget(buttonBox); setLayout(parentLayout); + setWindowTitle(tr("Check for Client Updates")); + + setFixedHeight(this->sizeHint().height()); + setFixedWidth(this->sizeHint().width()); // Check for SSL (this probably isn't necessary) if (!QSslSocket::supportsSsl()) { @@ -140,7 +144,7 @@ void DlgUpdate::finishedUpdateCheck(bool needToUpdate, bool isCompatible, Releas return; } - publishDate = release->getPublishDate().toString(Qt::DefaultLocaleLongDate); + publishDate = release->getPublishDate().toString(QLocale().dateFormat(QLocale::LongFormat)); if (isCompatible) { int reply; reply = QMessageBox::question( @@ -190,19 +194,19 @@ void DlgUpdate::enableOkButton(bool enable) ok->setEnabled(enable); } -void DlgUpdate::setLabel(QString newText) +void DlgUpdate::setLabel(const QString &newText) { statusLabel->setText(newText); } -void DlgUpdate::updateCheckError(QString errorString) +void DlgUpdate::updateCheckError(const QString &errorString) { setLabel(tr("Error")); QMessageBox::critical(this, tr("Update Error"), tr("An error occurred while checking for updates:") + QString(" ") + errorString); } -void DlgUpdate::downloadError(QString errorString) +void DlgUpdate::downloadError(const QString &errorString) { setLabel(tr("Error")); enableUpdateButton(true); @@ -210,7 +214,7 @@ void DlgUpdate::downloadError(QString errorString) tr("An error occurred while downloading an update:") + QString(" ") + errorString); } -void DlgUpdate::downloadSuccessful(QUrl filepath) +void DlgUpdate::downloadSuccessful(const QUrl &filepath) { setLabel(tr("Installing...")); // Try to open the installer. If it opens, quit Cockatrice diff --git a/cockatrice/src/dlg_update.h b/cockatrice/src/dlg_update.h index 2c8f2bbb8..42cfc5f58 100644 --- a/cockatrice/src/dlg_update.h +++ b/cockatrice/src/dlg_update.h @@ -19,10 +19,10 @@ private slots: void gotoDownloadPage(); void downloadUpdate(); void cancelDownload(); - void updateCheckError(QString errorString); - void downloadSuccessful(QUrl filepath); + void updateCheckError(const QString &errorString); + void downloadSuccessful(const QUrl &filepath); void downloadProgressMade(qint64 bytesRead, qint64 totalBytes); - void downloadError(QString errorString); + void downloadError(const QString &errorString); void closeDialog(); private: @@ -31,7 +31,7 @@ private: void enableOkButton(bool enable); void addStopDownloadAndRemoveOthers(bool enable); void beginUpdateCheck(); - void setLabel(QString text); + void setLabel(const QString &text); QLabel *statusLabel, *descriptionLabel; QProgressBar *progress; QPushButton *manualDownload, *gotoDownload, *ok, *stopDownload; diff --git a/cockatrice/src/gamescene.cpp b/cockatrice/src/gamescene.cpp index 9a2e51aef..815e35c55 100644 --- a/cockatrice/src/gamescene.cpp +++ b/cockatrice/src/gamescene.cpp @@ -144,12 +144,10 @@ void GameScene::rearrange() void GameScene::toggleZoneView(Player *player, const QString &zoneName, int numberCards) { - for (int i = 0; i < zoneViews.size(); i++) { - ZoneViewZone *temp = zoneViews[i]->getZone(); - if ((temp->getName() == zoneName) && (temp->getPlayer() == player)) { // view is already open - zoneViews[i]->close(); - if (temp->getNumberCards() == numberCards) - return; + for (auto &view : zoneViews) { + ZoneViewZone *temp = view->getZone(); + if (temp->getName() == zoneName && temp->getPlayer() == player && temp->getNumberCards() == numberCards) { + view->close(); } } @@ -157,12 +155,13 @@ void GameScene::toggleZoneView(Player *player, const QString &zoneName, int numb zoneViews.append(item); connect(item, SIGNAL(closePressed(ZoneViewWidget *)), this, SLOT(removeZoneView(ZoneViewWidget *))); addItem(item); - if (zoneName == "grave") + if (zoneName == "grave") { item->setPos(360, 100); - else if (zoneName == "rfg") + } else if (zoneName == "rfg") { item->setPos(380, 120); - else + } else { item->setPos(340, 80); + } } void GameScene::addRevealedZoneView(Player *player, diff --git a/cockatrice/src/gameselector.cpp b/cockatrice/src/gameselector.cpp index 8cc570456..036a1817e 100644 --- a/cockatrice/src/gameselector.cpp +++ b/cockatrice/src/gameselector.cpp @@ -8,9 +8,9 @@ #include "pb/room_commands.pb.h" #include "pb/serverinfo_game.pb.h" #include "pending_command.h" +#include "tab_account.h" #include "tab_room.h" #include "tab_supervisor.h" -#include "tab_userlists.h" #include #include @@ -74,18 +74,16 @@ GameSelector::GameSelector(AbstractClient *_client, connect(filterButton, SIGNAL(clicked()), this, SLOT(actSetFilter())); clearFilterButton = new QPushButton; clearFilterButton->setIcon(QPixmap("theme:icons/clearsearch")); - if (showFilters && gameListProxyModel->areFilterParametersSetToDefaults()) { - clearFilterButton->setEnabled(false); - } else { - clearFilterButton->setEnabled(true); - } + bool filtersSetToDefault = showFilters && gameListProxyModel->areFilterParametersSetToDefaults(); + clearFilterButton->setEnabled(!filtersSetToDefault); connect(clearFilterButton, SIGNAL(clicked()), this, SLOT(actClearFilter())); if (room) { createButton = new QPushButton; connect(createButton, SIGNAL(clicked()), this, SLOT(actCreate())); - } else - createButton = 0; + } else { + createButton = nullptr; + } joinButton = new QPushButton; spectateButton = new QPushButton; @@ -154,13 +152,19 @@ void GameSelector::actSetFilter() return; gameListProxyModel->setShowBuddiesOnlyGames(dlg.getShowBuddiesOnlyGames()); - gameListProxyModel->setUnavailableGamesVisible(dlg.getUnavailableGamesVisible()); + gameListProxyModel->setShowFullGames(dlg.getShowFullGames()); + gameListProxyModel->setShowGamesThatStarted(dlg.getShowGamesThatStarted()); gameListProxyModel->setShowPasswordProtectedGames(dlg.getShowPasswordProtectedGames()); gameListProxyModel->setHideIgnoredUserGames(dlg.getHideIgnoredUserGames()); gameListProxyModel->setGameNameFilter(dlg.getGameNameFilter()); gameListProxyModel->setCreatorNameFilter(dlg.getCreatorNameFilter()); gameListProxyModel->setGameTypeFilter(dlg.getGameTypeFilter()); gameListProxyModel->setMaxPlayersFilter(dlg.getMaxPlayersFilterMin(), dlg.getMaxPlayersFilterMax()); + gameListProxyModel->setMaxGameAge(dlg.getMaxGameAge()); + gameListProxyModel->setShowOnlyIfSpectatorsCanWatch(dlg.getShowOnlyIfSpectatorsCanWatch()); + gameListProxyModel->setShowSpectatorPasswordProtected(dlg.getShowSpectatorPasswordProtected()); + gameListProxyModel->setShowOnlyIfSpectatorsCanChat(dlg.getShowOnlyIfSpectatorsCanChat()); + gameListProxyModel->setShowOnlyIfSpectatorsCanSeeHands(dlg.getShowOnlyIfSpectatorsCanSeeHands()); gameListProxyModel->saveFilterParameters(gameTypeMap); clearFilterButton->setEnabled(!gameListProxyModel->areFilterParametersSetToDefaults()); diff --git a/cockatrice/src/gamesmodel.cpp b/cockatrice/src/gamesmodel.cpp index 3a0441bbc..092bf18b3 100644 --- a/cockatrice/src/gamesmodel.cpp +++ b/cockatrice/src/gamesmodel.cpp @@ -3,13 +3,14 @@ #include "pb/serverinfo_game.pb.h" #include "pixmapgenerator.h" #include "settingscache.h" -#include "tab_userlists.h" +#include "tab_account.h" #include "userlist.h" #include #include #include #include +#include enum GameListColumn { @@ -23,25 +24,50 @@ enum GameListColumn SPECTATORS }; -const QString GamesModel::getGameCreatedString(const int secs) const -{ +const bool DEFAULT_SHOW_FULL_GAMES = false; +const bool DEFAULT_SHOW_GAMES_THAT_STARTED = false; +const bool DEFAULT_SHOW_PASSWORD_PROTECTED_GAMES = true; +const bool DEFAULT_SHOW_BUDDIES_ONLY_GAMES = true; +const bool DEFAULT_HIDE_IGNORED_USER_GAMES = false; +const bool DEFAULT_SHOW_ONLY_IF_SPECTATORS_CAN_WATCH = false; +const bool DEFAULT_SHOW_SPECTATOR_PASSWORD_PROTECTED = true; +const bool DEFAULT_SHOW_ONLY_IF_SPECTATORS_CAN_CHAT = false; +const bool DEFAULT_SHOW_ONLY_IF_SPECTATORS_CAN_SEE_HANDS = false; +const int DEFAULT_MAX_PLAYERS_MIN = 1; +const int DEFAULT_MAX_PLAYERS_MAX = 99; +constexpr QTime DEFAULT_MAX_GAME_AGE = QTime(); - QString ret; - if (secs < SECS_PER_MIN * 2) // for first min we display "New" - ret = tr("New"); - else if (secs < SECS_PER_MIN * 10) // from 2 - 10 mins we show the mins - ret = QString("%1 min").arg(QString::number(secs / SECS_PER_MIN)); - else if (secs < SECS_PER_MIN * 60) { // from 10 mins to 1h we aggregate every 10 mins - int unitOfTen = secs / SECS_PER_TEN_MIN; - QString str = "%1%2"; - ret = str.arg(QString::number(unitOfTen), "0+ min"); - } else { // from 1 hr onward we show hrs - int hours = secs / SECS_PER_HOUR; - if (secs % SECS_PER_HOUR >= SECS_PER_MIN * 30) // if the room is open for 1hr 30 mins, we round to 2hrs - ++hours; - ret = QString("%1+ h").arg(QString::number(hours)); +const QString GamesModel::getGameCreatedString(const int secs) +{ + static const QTime zeroTime{0, 0}; + static const int halfHourSecs = zeroTime.secsTo(QTime(1, 0)) / 2; + static const int halfMinSecs = zeroTime.secsTo(QTime(0, 1)) / 2; + static const int wrapSeconds = zeroTime.secsTo(zeroTime.addSecs(-halfHourSecs)); // round up + + if (secs >= wrapSeconds) { // QTime wraps after a day + return tr(">1 day"); } - return ret; + + QTime total = zeroTime.addSecs(secs); + QTime totalRounded = total.addSecs(halfMinSecs); // round up + QString form; + int amount; + if (totalRounded.hour()) { + amount = total.addSecs(halfHourSecs).hour(); // round up separately + form = tr("%1%2 hr", "short age in hours", amount); + } else if (total.minute() < 2) { // games are new during their first minute + return tr("new"); + } else { + amount = totalRounded.minute(); + form = tr("%1%2 min", "short age in minutes", amount); + } + + for (int aggregate : {40, 20, 10, 5}) { // floor to values in this list + if (amount >= aggregate) { + return form.arg(">").arg(aggregate); + } + } + return form.arg("").arg(amount); } GamesModel::GamesModel(const QMap &_rooms, const QMap &_gameTypes, QObject *parent) @@ -60,20 +86,23 @@ QVariant GamesModel::data(const QModelIndex &index, int role) const if ((index.row() >= gameList.size()) || (index.column() >= columnCount())) return QVariant(); - const ServerInfo_Game &g = gameList[index.row()]; + const ServerInfo_Game &gameentry = gameList[index.row()]; switch (index.column()) { case ROOM: - return rooms.value(g.room_id()); + return rooms.value(gameentry.room_id()); case CREATED: { - QDateTime then; - then.setTime_t(g.start_time()); - int secs = then.secsTo(QDateTime::currentDateTime()); - switch (role) { - case Qt::DisplayRole: + case Qt::DisplayRole: { +#if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0)) + QDateTime then = QDateTime::fromSecsSinceEpoch(gameentry.start_time(), Qt::UTC); +#else + QDateTime then = QDateTime::fromTime_t(gameentry.start_time(), Qt::UTC); +#endif + int secs = then.secsTo(QDateTime::currentDateTimeUtc()); return getGameCreatedString(secs); + } case SORT_ROLE: - return QVariant(secs); + return QVariant(-static_cast(gameentry.start_time())); case Qt::TextAlignmentRole: return Qt::AlignCenter; default: @@ -84,7 +113,7 @@ QVariant GamesModel::data(const QModelIndex &index, int role) const switch (role) { case SORT_ROLE: case Qt::DisplayRole: - return QString::fromStdString(g.description()); + return QString::fromStdString(gameentry.description()); case Qt::TextAlignmentRole: return Qt::AlignLeft; default: @@ -94,11 +123,11 @@ QVariant GamesModel::data(const QModelIndex &index, int role) const switch (role) { case SORT_ROLE: case Qt::DisplayRole: - return QString::fromStdString(g.creator_info().name()); + return QString::fromStdString(gameentry.creator_info().name()); case Qt::DecorationRole: { QPixmap avatarPixmap = UserLevelPixmapGenerator::generatePixmap( - 13, (UserLevelFlags)g.creator_info().user_level(), false, - QString::fromStdString(g.creator_info().privlevel())); + 13, (UserLevelFlags)gameentry.creator_info().user_level(), false, + QString::fromStdString(gameentry.creator_info().privlevel())); return QIcon(avatarPixmap); } default: @@ -110,9 +139,9 @@ QVariant GamesModel::data(const QModelIndex &index, int role) const case SORT_ROLE: case Qt::DisplayRole: { QStringList result; - GameTypeMap gameTypeMap = gameTypes.value(g.room_id()); - for (int i = g.game_types_size() - 1; i >= 0; --i) - result.append(gameTypeMap.value(g.game_types(i))); + GameTypeMap gameTypeMap = gameTypes.value(gameentry.room_id()); + for (int i = gameentry.game_types_size() - 1; i >= 0; --i) + result.append(gameTypeMap.value(gameentry.game_types(i))); return result.join(", "); } case Qt::TextAlignmentRole: @@ -125,16 +154,16 @@ QVariant GamesModel::data(const QModelIndex &index, int role) const case SORT_ROLE: case Qt::DisplayRole: { QStringList result; - if (g.with_password()) + if (gameentry.with_password()) result.append(tr("password")); - if (g.only_buddies()) + if (gameentry.only_buddies()) result.append(tr("buddies only")); - if (g.only_registered()) + if (gameentry.only_registered()) result.append(tr("reg. users only")); return result.join(", "); } case Qt::DecorationRole: { - return g.with_password() ? QIcon(LockPixmapGenerator::generatePixmap(13)) : QVariant(); + return gameentry.with_password() ? QIcon(LockPixmapGenerator::generatePixmap(13)) : QVariant(); case Qt::TextAlignmentRole: return Qt::AlignLeft; default: @@ -145,7 +174,7 @@ QVariant GamesModel::data(const QModelIndex &index, int role) const switch (role) { case SORT_ROLE: case Qt::DisplayRole: - return QString("%1/%2").arg(g.player_count()).arg(g.max_players()); + return QString("%1/%2").arg(gameentry.player_count()).arg(gameentry.max_players()); case Qt::TextAlignmentRole: return Qt::AlignCenter; default: @@ -156,19 +185,19 @@ QVariant GamesModel::data(const QModelIndex &index, int role) const switch (role) { case SORT_ROLE: case Qt::DisplayRole: { - if (g.spectators_allowed()) { + if (gameentry.spectators_allowed()) { QString result; - result.append(QString::number(g.spectators_count())); + result.append(QString::number(gameentry.spectators_count())); - if (g.spectators_can_chat() && g.spectators_omniscient()) { + if (gameentry.spectators_can_chat() && gameentry.spectators_omniscient()) { result.append(" (") .append(tr("can chat")) .append(" & ") .append(tr("see hands")) .append(")"); - } else if (g.spectators_can_chat()) { + } else if (gameentry.spectators_can_chat()) { result.append(" (").append(tr("can chat")).append(")"); - } else if (g.spectators_omniscient()) { + } else if (gameentry.spectators_omniscient()) { result.append(" (").append(tr("can see hands")).append(")"); } @@ -249,8 +278,6 @@ void GamesModel::updateGameList(const ServerInfo_Game &game) return; } } - if (game.player_count() <= 0) - return; beginInsertRows(QModelIndex(), gameList.size(), gameList.size()); gameList.append(game); endInsertRows(); @@ -277,9 +304,15 @@ void GamesProxyModel::setHideIgnoredUserGames(bool _hideIgnoredUserGames) invalidateFilter(); } -void GamesProxyModel::setUnavailableGamesVisible(bool _unavailableGamesVisible) +void GamesProxyModel::setShowFullGames(bool _showFullGames) { - unavailableGamesVisible = _unavailableGamesVisible; + showFullGames = _showFullGames; + invalidateFilter(); +} + +void GamesProxyModel::setShowGamesThatStarted(bool _showGamesThatStarted) +{ + showGamesThatStarted = _showGamesThatStarted; invalidateFilter(); } @@ -314,6 +347,36 @@ void GamesProxyModel::setMaxPlayersFilter(int _maxPlayersFilterMin, int _maxPlay invalidateFilter(); } +void GamesProxyModel::setMaxGameAge(const QTime &_maxGameAge) +{ + maxGameAge = _maxGameAge; + invalidateFilter(); +} + +void GamesProxyModel::setShowOnlyIfSpectatorsCanWatch(bool _showOnlyIfSpectatorsCanWatch) +{ + showOnlyIfSpectatorsCanWatch = _showOnlyIfSpectatorsCanWatch; + invalidateFilter(); +} + +void GamesProxyModel::setShowSpectatorPasswordProtected(bool _showSpectatorPasswordProtected) +{ + showSpectatorPasswordProtected = _showSpectatorPasswordProtected; + invalidateFilter(); +} + +void GamesProxyModel::setShowOnlyIfSpectatorsCanChat(bool _showOnlyIfSpectatorsCanChat) +{ + showOnlyIfSpectatorsCanChat = _showOnlyIfSpectatorsCanChat; + invalidateFilter(); +} + +void GamesProxyModel::setShowOnlyIfSpectatorsCanSeeHands(bool _showOnlyIfSpectatorsCanSeeHands) +{ + showOnlyIfSpectatorsCanSeeHands = _showOnlyIfSpectatorsCanSeeHands; + invalidateFilter(); +} + int GamesProxyModel::getNumFilteredGames() const { GamesModel *model = qobject_cast(sourceModel()); @@ -331,7 +394,8 @@ int GamesProxyModel::getNumFilteredGames() const void GamesProxyModel::resetFilterParameters() { - unavailableGamesVisible = DEFAULT_UNAVAILABLE_GAMES_VISIBLE; + showFullGames = DEFAULT_SHOW_FULL_GAMES; + showGamesThatStarted = DEFAULT_SHOW_GAMES_THAT_STARTED; showPasswordProtectedGames = DEFAULT_SHOW_PASSWORD_PROTECTED_GAMES; showBuddiesOnlyGames = DEFAULT_SHOW_BUDDIES_ONLY_GAMES; hideIgnoredUserGames = DEFAULT_HIDE_IGNORED_USER_GAMES; @@ -340,24 +404,34 @@ void GamesProxyModel::resetFilterParameters() gameTypeFilter.clear(); maxPlayersFilterMin = DEFAULT_MAX_PLAYERS_MIN; maxPlayersFilterMax = DEFAULT_MAX_PLAYERS_MAX; + maxGameAge = DEFAULT_MAX_GAME_AGE; + showOnlyIfSpectatorsCanWatch = DEFAULT_SHOW_ONLY_IF_SPECTATORS_CAN_WATCH; + showSpectatorPasswordProtected = DEFAULT_SHOW_SPECTATOR_PASSWORD_PROTECTED; + showOnlyIfSpectatorsCanChat = DEFAULT_SHOW_ONLY_IF_SPECTATORS_CAN_CHAT; + showOnlyIfSpectatorsCanSeeHands = DEFAULT_SHOW_ONLY_IF_SPECTATORS_CAN_SEE_HANDS; invalidateFilter(); } bool GamesProxyModel::areFilterParametersSetToDefaults() const { - return unavailableGamesVisible == DEFAULT_UNAVAILABLE_GAMES_VISIBLE && + return showFullGames == DEFAULT_SHOW_FULL_GAMES && showGamesThatStarted == DEFAULT_SHOW_GAMES_THAT_STARTED && showPasswordProtectedGames == DEFAULT_SHOW_PASSWORD_PROTECTED_GAMES && showBuddiesOnlyGames == DEFAULT_SHOW_BUDDIES_ONLY_GAMES && hideIgnoredUserGames == DEFAULT_HIDE_IGNORED_USER_GAMES && gameNameFilter.isEmpty() && creatorNameFilter.isEmpty() && gameTypeFilter.isEmpty() && maxPlayersFilterMin == DEFAULT_MAX_PLAYERS_MIN && - maxPlayersFilterMax == DEFAULT_MAX_PLAYERS_MAX; + maxPlayersFilterMax == DEFAULT_MAX_PLAYERS_MAX && maxGameAge == DEFAULT_MAX_GAME_AGE && + showOnlyIfSpectatorsCanWatch == DEFAULT_SHOW_ONLY_IF_SPECTATORS_CAN_WATCH && + showSpectatorPasswordProtected == DEFAULT_SHOW_SPECTATOR_PASSWORD_PROTECTED && + showOnlyIfSpectatorsCanChat == DEFAULT_SHOW_ONLY_IF_SPECTATORS_CAN_CHAT && + showOnlyIfSpectatorsCanSeeHands == DEFAULT_SHOW_ONLY_IF_SPECTATORS_CAN_SEE_HANDS; } void GamesProxyModel::loadFilterParameters(const QMap &allGameTypes) { GameFiltersSettings &gameFilters = SettingsCache::instance().gameFilters(); - unavailableGamesVisible = gameFilters.isUnavailableGamesVisible(); + showFullGames = gameFilters.isShowFullGames(); + showGamesThatStarted = gameFilters.isShowGamesThatStarted(); showPasswordProtectedGames = gameFilters.isShowPasswordProtectedGames(); hideIgnoredUserGames = gameFilters.isHideIgnoredUserGames(); showBuddiesOnlyGames = gameFilters.isShowBuddiesOnlyGames(); @@ -365,6 +439,11 @@ void GamesProxyModel::loadFilterParameters(const QMap &allGameType creatorNameFilter = gameFilters.getCreatorNameFilter(); maxPlayersFilterMin = gameFilters.getMinPlayers(); maxPlayersFilterMax = gameFilters.getMaxPlayers(); + maxGameAge = gameFilters.getMaxGameAge(); + showOnlyIfSpectatorsCanWatch = gameFilters.isShowOnlyIfSpectatorsCanWatch(); + showSpectatorPasswordProtected = gameFilters.isShowSpectatorPasswordProtected(); + showOnlyIfSpectatorsCanChat = gameFilters.isShowOnlyIfSpectatorsCanChat(); + showOnlyIfSpectatorsCanSeeHands = gameFilters.isShowOnlyIfSpectatorsCanSeeHands(); QMapIterator gameTypesIterator(allGameTypes); while (gameTypesIterator.hasNext()) { @@ -381,7 +460,8 @@ void GamesProxyModel::saveFilterParameters(const QMap &allGameType { GameFiltersSettings &gameFilters = SettingsCache::instance().gameFilters(); gameFilters.setShowBuddiesOnlyGames(showBuddiesOnlyGames); - gameFilters.setUnavailableGamesVisible(unavailableGamesVisible); + gameFilters.setShowFullGames(showFullGames); + gameFilters.setShowGamesThatStarted(showGamesThatStarted); gameFilters.setShowPasswordProtectedGames(showPasswordProtectedGames); gameFilters.setHideIgnoredUserGames(hideIgnoredUserGames); gameFilters.setGameNameFilter(gameNameFilter); @@ -396,6 +476,12 @@ void GamesProxyModel::saveFilterParameters(const QMap &allGameType gameFilters.setMinPlayers(maxPlayersFilterMin); gameFilters.setMaxPlayers(maxPlayersFilterMax); + gameFilters.setMaxGameAge(maxGameAge); + + gameFilters.setShowOnlyIfSpectatorsCanWatch(showOnlyIfSpectatorsCanWatch); + gameFilters.setShowSpectatorPasswordProtected(showSpectatorPasswordProtected); + gameFilters.setShowOnlyIfSpectatorsCanChat(showOnlyIfSpectatorsCanChat); + gameFilters.setShowOnlyIfSpectatorsCanSeeHands(showOnlyIfSpectatorsCanSeeHands); } bool GamesProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex & /*sourceParent*/) const @@ -405,7 +491,12 @@ bool GamesProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex & /*sour bool GamesProxyModel::filterAcceptsRow(int sourceRow) const { - GamesModel *model = qobject_cast(sourceModel()); +#if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0)) + static const QDate epochDate = QDateTime::fromSecsSinceEpoch(0, Qt::UTC).date(); +#else + static const QDate epochDate = QDateTime::fromTime_t(0, Qt::UTC).date(); +#endif + auto *model = qobject_cast(sourceModel()); if (!model) return false; @@ -418,15 +509,13 @@ bool GamesProxyModel::filterAcceptsRow(int sourceRow) const QString::fromStdString(game.creator_info().name()))) { return false; } - if (!unavailableGamesVisible) { - if (game.player_count() == game.max_players()) + if (!showFullGames && game.player_count() == game.max_players()) + return false; + if (!showGamesThatStarted && game.started()) + return false; + if (!ownUserIsRegistered) + if (game.only_registered()) return false; - if (game.started()) - return false; - if (!ownUserIsRegistered) - if (game.only_registered()) - return false; - } if (!showPasswordProtectedGames && game.with_password()) return false; if (!gameNameFilter.isEmpty()) @@ -442,11 +531,32 @@ bool GamesProxyModel::filterAcceptsRow(int sourceRow) const if (!gameTypeFilter.isEmpty() && gameTypes.intersect(gameTypeFilter).isEmpty()) return false; - if ((int)game.max_players() < maxPlayersFilterMin) + if (game.max_players() < maxPlayersFilterMin) return false; - if ((int)game.max_players() > maxPlayersFilterMax) + if (game.max_players() > maxPlayersFilterMax) return false; + if (maxGameAge.isValid()) { + QDateTime now = QDateTime::currentDateTimeUtc(); + qint64 signed_start_time = game.start_time(); // cast to 64 bit value to allow signed value + QDateTime total = now.addSecs(-signed_start_time); // a 32 bit value would wrap at 2038-1-19 + // games shouldn't have negative ages but we'll not filter them + // because qtime wraps after a day we consider all games older than a day to be too old + if (total.isValid() && total.date() >= epochDate && (total.date() > epochDate || total.time() > maxGameAge)) { + return false; + } + } + + if (showOnlyIfSpectatorsCanWatch) { + if (!game.spectators_allowed()) + return false; + if (!showSpectatorPasswordProtected && game.spectators_need_password()) + return false; + if (showOnlyIfSpectatorsCanChat && !game.spectators_can_chat()) + return false; + if (showOnlyIfSpectatorsCanSeeHands && !game.spectators_omniscient()) + return false; + } return true; } diff --git a/cockatrice/src/gamesmodel.h b/cockatrice/src/gamesmodel.h index e114a1106..eb14fcbda 100644 --- a/cockatrice/src/gamesmodel.h +++ b/cockatrice/src/gamesmodel.h @@ -9,6 +9,8 @@ #include #include #include +#include +#include class GamesModel : public QAbstractTableModel { @@ -19,9 +21,6 @@ private: QMap gameTypes; static const int NUM_COLS = 8; - static const int SECS_PER_MIN = 60; - static const int SECS_PER_TEN_MIN = 600; - static const int SECS_PER_HOUR = 3600; public: static const int SORT_ROLE = Qt::UserRole + 1; @@ -37,7 +36,7 @@ public: } QVariant data(const QModelIndex &index, int role) const; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; - const QString getGameCreatedString(const int secs) const; + static const QString getGameCreatedString(const int secs); const ServerInfo_Game &getGame(int row); /** @@ -68,20 +67,25 @@ class GamesProxyModel : public QSortFilterProxyModel private: bool ownUserIsRegistered; const TabSupervisor *tabSupervisor; + + // If adding any additional filters, make sure to update: + // - GamesProxyModel() + // - resetFilterParameters() + // - areFilterParametersSetToDefaults() + // - loadFilterParameters() + // - saveFilterParameters() + // - filterAcceptsRow() bool showBuddiesOnlyGames; bool hideIgnoredUserGames; - bool unavailableGamesVisible; + bool showFullGames; + bool showGamesThatStarted; bool showPasswordProtectedGames; QString gameNameFilter, creatorNameFilter; QSet gameTypeFilter; - int maxPlayersFilterMin, maxPlayersFilterMax; - - static const bool DEFAULT_UNAVAILABLE_GAMES_VISIBLE = false; - static const bool DEFAULT_SHOW_PASSWORD_PROTECTED_GAMES = true; - static const bool DEFAULT_SHOW_BUDDIES_ONLY_GAMES = true; - static const bool DEFAULT_HIDE_IGNORED_USER_GAMES = false; - static const int DEFAULT_MAX_PLAYERS_MIN = 1; - static const int DEFAULT_MAX_PLAYERS_MAX = 99; + quint32 maxPlayersFilterMin, maxPlayersFilterMax; + QTime maxGameAge; + bool showOnlyIfSpectatorsCanWatch, showSpectatorPasswordProtected, showOnlyIfSpectatorsCanChat, + showOnlyIfSpectatorsCanSeeHands; public: GamesProxyModel(QObject *parent = nullptr, const TabSupervisor *_tabSupervisor = nullptr); @@ -96,11 +100,16 @@ public: return hideIgnoredUserGames; } void setHideIgnoredUserGames(bool _hideIgnoredUserGames); - bool getUnavailableGamesVisible() const + bool getShowFullGames() const { - return unavailableGamesVisible; + return showFullGames; } - void setUnavailableGamesVisible(bool _unavailableGamesVisible); + void setShowFullGames(bool _showFullGames); + bool getShowGamesThatStarted() const + { + return showGamesThatStarted; + } + void setShowGamesThatStarted(bool _showGamesThatStarted); bool getShowPasswordProtectedGames() const { return showPasswordProtectedGames; @@ -130,6 +139,32 @@ public: return maxPlayersFilterMax; } void setMaxPlayersFilter(int _maxPlayersFilterMin, int _maxPlayersFilterMax); + const QTime &getMaxGameAge() const + { + return maxGameAge; + } + void setMaxGameAge(const QTime &_maxGameAge); + bool getShowOnlyIfSpectatorsCanWatch() const + { + return showOnlyIfSpectatorsCanWatch; + } + void setShowOnlyIfSpectatorsCanWatch(bool _showOnlyIfSpectatorsCanWatch); + bool getShowSpectatorPasswordProtected() const + { + return showSpectatorPasswordProtected; + } + void setShowSpectatorPasswordProtected(bool _showSpectatorPasswordProtected); + bool getShowOnlyIfSpectatorsCanChat() const + { + return showOnlyIfSpectatorsCanChat; + } + void setShowOnlyIfSpectatorsCanChat(bool _showOnlyIfSpectatorsCanChat); + bool getShowOnlyIfSpectatorsCanSeeHands() const + { + return showOnlyIfSpectatorsCanSeeHands; + } + void setShowOnlyIfSpectatorsCanSeeHands(bool _showOnlyIfSpectatorsCanSeeHands); + int getNumFilteredGames() const; void resetFilterParameters(); bool areFilterParametersSetToDefaults() const; diff --git a/cockatrice/src/handzone.cpp b/cockatrice/src/handzone.cpp index bf6a44b8a..91d2e89e6 100644 --- a/cockatrice/src/handzone.cpp +++ b/cockatrice/src/handzone.cpp @@ -76,7 +76,13 @@ QRectF HandZone::boundingRect() const void HandZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) { - painter->fillRect(boundingRect(), themeManager->getHandBgBrush()); + QBrush brush = themeManager->getHandBgBrush(); + + if (player->getZoneId() > 0) { + // If the extra image is not found, load the default one + brush = themeManager->getExtraHandBgBrush(QString::number(player->getZoneId()), brush); + } + painter->fillRect(boundingRect(), brush); } void HandZone::reorganizeCards() diff --git a/cockatrice/src/main.cpp b/cockatrice/src/main.cpp index 89abee12e..69003a3da 100644 --- a/cockatrice/src/main.cpp +++ b/cockatrice/src/main.cpp @@ -151,6 +151,10 @@ int main(int argc, char *argv[]) qDebug("main(): MainWindow constructor finished"); ui.setWindowIcon(QPixmap("theme:cockatrice")); +#if QT_VERSION >= QT_VERSION_CHECK(5, 7, 0) + // set name of the app desktop file; used by wayland to load the window icon + QGuiApplication::setDesktopFileName("cockatrice"); +#endif SettingsCache::instance().setClientID(generateClientID()); diff --git a/cockatrice/src/messagelogwidget.cpp b/cockatrice/src/messagelogwidget.cpp index 65ed5e120..1b9b610ff 100644 --- a/cockatrice/src/messagelogwidget.cpp +++ b/cockatrice/src/messagelogwidget.cpp @@ -152,6 +152,14 @@ void MessageLogWidget::logAlwaysRevealTopCard(Player *player, CardZone *zone, bo .arg(zone->getTranslatedName(true, CaseTopCardsOfZone))); } +void MessageLogWidget::logAlwaysLookAtTopCard(Player *player, CardZone *zone, bool reveal) +{ + appendHtmlServerMessage((reveal ? tr("%1 can now look at top card %2 at any time.") + : tr("%1 no longer can look at top card %2 at any time.")) + .arg(sanitizeHtml(player->getName())) + .arg(zone->getTranslatedName(true, CaseTopCardsOfZone))); +} + void MessageLogWidget::logAttachCard(Player *player, QString cardName, Player *targetPlayer, QString targetCardName) { appendHtmlServerMessage(tr("%1 attaches %2 to %3's %4.") @@ -316,7 +324,11 @@ void MessageLogWidget::logMoveCard(Player *player, bool usesNewX = false; if (targetZoneName == tableConstant()) { soundEngine->playSound("play_card"); - finalStr = tr("%1 puts %2 into play%3."); + if (card->getFaceDown()) { + finalStr = tr("%1 puts %2 into play%3 face down."); + } else { + finalStr = tr("%1 puts %2 into play%3."); + } } else if (targetZoneName == graveyardConstant()) { finalStr = tr("%1 puts %2%3 into their graveyard."); } else if (targetZoneName == exileConstant()) { @@ -380,9 +392,11 @@ void MessageLogWidget::logDumpZone(Player *player, CardZone *zone, int numberCar void MessageLogWidget::logFlipCard(Player *player, QString cardName, bool faceDown) { if (faceDown) { - appendHtmlServerMessage(tr("%1 turns %2 face-down.").arg(sanitizeHtml(player->getName())).arg(cardName)); + appendHtmlServerMessage( + tr("%1 turns %2 face-down.").arg(sanitizeHtml(player->getName())).arg(cardLink(cardName))); } else { - appendHtmlServerMessage(tr("%1 turns %2 face-up.").arg(sanitizeHtml(player->getName())).arg(cardName)); + appendHtmlServerMessage( + tr("%1 turns %2 face-up.").arg(sanitizeHtml(player->getName())).arg(cardLink(cardName))); } } @@ -743,13 +757,6 @@ void MessageLogWidget::logSpectatorSay(QString spectatorName, appendMessage(std::move(message), {}, spectatorName, spectatorUserLevel, userPrivLevel, false); } -void MessageLogWidget::logStopDumpZone(Player *player, CardZone *zone) -{ - appendHtmlServerMessage(tr("%1 stops looking at %2.") - .arg(sanitizeHtml(player->getName())) - .arg(zone->getTranslatedName(zone->getPlayer() == player, CaseLookAtZone))); -} - void MessageLogWidget::logUnattachCard(Player *player, QString cardName) { appendHtmlServerMessage( @@ -810,13 +817,14 @@ void MessageLogWidget::connectToPlayer(Player *player) SLOT(logAttachCard(Player *, QString, Player *, QString))); connect(player, SIGNAL(logUnattachCard(Player *, QString)), this, SLOT(logUnattachCard(Player *, QString))); connect(player, SIGNAL(logDumpZone(Player *, CardZone *, int)), this, SLOT(logDumpZone(Player *, CardZone *, int))); - connect(player, SIGNAL(logStopDumpZone(Player *, CardZone *)), this, SLOT(logStopDumpZone(Player *, CardZone *))); connect(player, SIGNAL(logDrawCards(Player *, int)), this, SLOT(logDrawCards(Player *, int))); connect(player, SIGNAL(logUndoDraw(Player *, QString)), this, SLOT(logUndoDraw(Player *, QString))); connect(player, SIGNAL(logRevealCards(Player *, CardZone *, int, QString, Player *, bool, int)), this, SLOT(logRevealCards(Player *, CardZone *, int, QString, Player *, bool, int))); connect(player, SIGNAL(logAlwaysRevealTopCard(Player *, CardZone *, bool)), this, SLOT(logAlwaysRevealTopCard(Player *, CardZone *, bool))); + connect(player, SIGNAL(logAlwaysLookAtTopCard(Player *, CardZone *, bool)), this, + SLOT(logAlwaysLookAtTopCard(Player *, CardZone *, bool))); } MessageLogWidget::MessageLogWidget(const TabSupervisor *_tabSupervisor, diff --git a/cockatrice/src/messagelogwidget.h b/cockatrice/src/messagelogwidget.h index 94878fd33..7769e098f 100644 --- a/cockatrice/src/messagelogwidget.h +++ b/cockatrice/src/messagelogwidget.h @@ -42,6 +42,7 @@ public slots: void containerProcessingDone(); void containerProcessingStarted(const GameEventContext &context); void logAlwaysRevealTopCard(Player *player, CardZone *zone, bool reveal); + void logAlwaysLookAtTopCard(Player *player, CardZone *zone, bool reveal); void logAttachCard(Player *player, QString cardName, Player *targetPlayer, QString targetCardName); void logConcede(Player *player); void logUnconcede(Player *player); @@ -92,7 +93,6 @@ public slots: void logShuffle(Player *player, CardZone *zone, int start, int end); void logSpectatorSay(QString spectatorName, UserLevelFlags spectatorUserLevel, QString userPrivLevel, QString message); - void logStopDumpZone(Player *player, CardZone *zone); void logUnattachCard(Player *player, QString cardName); void logUndoDraw(Player *player, QString cardName); void setContextJudgeName(QString player); diff --git a/cockatrice/src/pictureloader.cpp b/cockatrice/src/pictureloader.cpp index 08b068054..bac849589 100644 --- a/cockatrice/src/pictureloader.cpp +++ b/cockatrice/src/pictureloader.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -195,8 +196,11 @@ bool PictureLoaderWorker::cardImageExistsOnDisk(QString &setName, QString &corre QString thisPath(it.next()); QFileInfo thisFileInfo(thisPath); - if (thisFileInfo.isFile() && thisFileInfo.baseName() == correctedCardname) + if (thisFileInfo.isFile() && + (thisFileInfo.fileName() == correctedCardname || thisFileInfo.completeBaseName() == correctedCardname || + thisFileInfo.baseName() == correctedCardname)) { picsPaths << thisPath; // Card found in the CUSTOM directory, somewhere + } } if (!setName.isEmpty()) { @@ -241,6 +245,10 @@ QString PictureToLoad::transformUrl(const QString &urlTemplate) const for downloading images. If information is requested by the template that is not populated for this specific card/set combination, an empty string is returned.*/ + static const QRegularExpression rxCardProp("!prop:([^!]+)!"); + static const QRegularExpression rxFillWith("^(.+)_fill_with_(.+)$"); + static const QRegularExpression rxSetProp("!set:([^!]+)!"); + QString transformedUrl = urlTemplate; CardSetPtr set = getCurrentSet(); @@ -252,18 +260,39 @@ QString PictureToLoad::transformUrl(const QString &urlTemplate) const transformMap["!corrected_name_lower!"] = card->getCorrectedName().toLower(); // card properties - QRegExp rxCardProp("!prop:([^!]+)!"); - int pos = 0; - while ((pos = rxCardProp.indexIn(transformedUrl, pos)) != -1) { - QString propertyName = rxCardProp.cap(1); - pos += rxCardProp.matchedLength(); - QString propertyValue = card->getProperty(propertyName); + auto matches = rxCardProp.globalMatch(transformedUrl); + while (matches.hasNext()) { + auto match = matches.next(); + QString propertyName = match.captured(1); + auto fillMatch = rxFillWith.match(propertyName); + QString fillPropertyName; + QString fillWith; + if (fillMatch.hasMatch()) { + fillPropertyName = fillMatch.captured(1); + fillWith = fillMatch.captured(2); + } else { + fillPropertyName = propertyName; + fillWith = QString(); + } + QString propertyValue = card->getProperty(fillPropertyName); if (propertyValue.isEmpty()) { qDebug() << "PictureLoader: [card: " << card->getName() << " set: " << getSetName() - << "]: Requested property (" << propertyName << ") for Url template (" << urlTemplate + << "]: Requested property (" << fillPropertyName << ") for Url template (" << urlTemplate << ") is not available"; return QString(); } else { + if (!fillWith.isEmpty()) { + int fillLength = fillWith.length(); + int propLength = propertyValue.length(); + if (fillLength < propLength) { + qDebug() << "PictureLoader: [card: " << card->getName() << " set: " << getSetName() + << "]: Requested property (" << fillPropertyName << ") for Url template (" << urlTemplate + << ") is longer than fill specification (" << fillWith << ")"; + return QString(); + } else { + propertyValue = fillWith.left(fillLength - propLength) + propertyValue; + } + } transformMap["!prop:" + propertyName + "!"] = propertyValue; } } @@ -274,18 +303,39 @@ QString PictureToLoad::transformUrl(const QString &urlTemplate) const transformMap["!setname!"] = set->getLongName(); transformMap["!setname_lower!"] = set->getLongName().toLower(); - QRegExp rxSetProp("!set:([^!]+)!"); - pos = 0; // Defined above - while ((pos = rxSetProp.indexIn(transformedUrl, pos)) != -1) { - QString propertyName = rxSetProp.cap(1); - pos += rxSetProp.matchedLength(); - QString propertyValue = card->getSetProperty(set->getShortName(), propertyName); + auto matches = rxSetProp.globalMatch(transformedUrl); + while (matches.hasNext()) { + auto match = matches.next(); + QString propertyName = match.captured(1); + auto fillMatch = rxFillWith.match(propertyName); + QString fillPropertyName; + QString fillWith; + if (fillMatch.hasMatch()) { + fillPropertyName = fillMatch.captured(1); + fillWith = fillMatch.captured(2); + } else { + fillPropertyName = propertyName; + fillWith = QString(); + } + QString propertyValue = card->getSetProperty(set->getShortName(), fillPropertyName); if (propertyValue.isEmpty()) { qDebug() << "PictureLoader: [card: " << card->getName() << " set: " << getSetName() - << "]: Requested set property (" << propertyName << ") for Url template (" << urlTemplate + << "]: Requested set property (" << fillPropertyName << ") for Url template (" << urlTemplate << ") is not available"; return QString(); } else { + if (!fillWith.isEmpty()) { + int fillLength = fillWith.length(); + int propLength = propertyValue.length(); + if (fillLength < propLength) { + qDebug() << "PictureLoader: [card: " << card->getName() << " set: " << getSetName() + << "]: Requested set property (" << fillPropertyName << ") for Url template (" + << urlTemplate << ") is longer than fill specification (" << fillWith << ")"; + return QString(); + } else { + propertyValue = fillWith.left(fillLength - propLength) + propertyValue; + } + } transformMap["!set:" + propertyName + "!"] = propertyValue; } } @@ -405,10 +455,9 @@ void PictureLoaderWorker::picDownloadFinished(QNetworkReply *reply) QImageReader imgReader; imgReader.setDecideFormatFromContent(true); imgReader.setDevice(reply); - QString extension = "." + imgReader.format(); // the format is determined - // prior to reading the - // QImageReader data - // into a QImage object, as that wipes the QImageReader buffer + // the format is determined prior to reading the QImageReader data into a QImage object, as that wipes the + // QImageReader buffer + QString extension = "." + imgReader.format(); if (extension == ".jpeg") { extension = ".jpg"; } diff --git a/cockatrice/src/player.cpp b/cockatrice/src/player.cpp index 3c5c0b3c6..a853e0e3a 100644 --- a/cockatrice/src/player.cpp +++ b/cockatrice/src/player.cpp @@ -49,7 +49,6 @@ #include "pb/event_set_card_counter.pb.h" #include "pb/event_set_counter.pb.h" #include "pb/event_shuffle.pb.h" -#include "pb/event_stop_dump_zone.pb.h" #include "pb/serverinfo_player.pb.h" #include "pb/serverinfo_user.pb.h" #include "pb/serverinfo_zone.pb.h" @@ -83,7 +82,13 @@ void PlayerArea::updateBg() void PlayerArea::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) { - painter->fillRect(bRect, themeManager->getPlayerBgBrush()); + QBrush brush = themeManager->getPlayerBgBrush(); + + if (playerZoneId > 0) { + // If the extra image is not found, load the default one + brush = themeManager->getExtraPlayerBgBrush(QString::number(playerZoneId), brush); + } + painter->fillRect(boundingRect(), brush); } void PlayerArea::setSize(qreal width, qreal height) @@ -92,10 +97,14 @@ void PlayerArea::setSize(qreal width, qreal height) bRect = QRectF(0, 0, width, height); } +void PlayerArea::setPlayerZoneId(int _playerZoneId) +{ + playerZoneId = _playerZoneId; +} + Player::Player(const ServerInfo_User &info, int _id, bool _local, bool _judge, TabGame *_parent) - : QObject(_parent), game(_parent), shortcutsActive(false), defaultNumberTopCards(1), - defaultNumberTopCardsToPlaceBelow(1), lastTokenDestroy(true), lastTokenTableRow(0), id(_id), active(false), - local(_local), judge(_judge), mirrored(false), handVisible(false), conceded(false), zoneId(0), + : QObject(_parent), game(_parent), shortcutsActive(false), lastTokenDestroy(true), lastTokenTableRow(0), id(_id), + active(false), local(_local), judge(_judge), mirrored(false), handVisible(false), conceded(false), zoneId(0), dialogSemaphore(false), deck(nullptr) { userInfo = new ServerInfo_User; @@ -200,6 +209,9 @@ Player::Player(const ServerInfo_User &info, int _id, bool _local, bool _judge, T aAlwaysRevealTopCard = new QAction(this); aAlwaysRevealTopCard->setCheckable(true); connect(aAlwaysRevealTopCard, SIGNAL(triggered()), this, SLOT(actAlwaysRevealTopCard())); + aAlwaysLookAtTopCard = new QAction(this); + aAlwaysLookAtTopCard->setCheckable(true); + connect(aAlwaysLookAtTopCard, SIGNAL(triggered()), this, SLOT(actAlwaysLookAtTopCard())); aOpenDeckInDeckEditor = new QAction(this); aOpenDeckInDeckEditor->setEnabled(false); connect(aOpenDeckInDeckEditor, SIGNAL(triggered()), this, SLOT(actOpenDeckInDeckEditor())); @@ -225,6 +237,7 @@ Player::Player(const ServerInfo_User &info, int _id, bool _local, bool _judge, T connect(aShuffle, SIGNAL(triggered()), this, SLOT(actShuffle())); aMulligan = new QAction(this); connect(aMulligan, SIGNAL(triggered()), this, SLOT(actMulligan())); + aMoveTopToPlay = new QAction(this); connect(aMoveTopToPlay, SIGNAL(triggered()), this, SLOT(actMoveTopCardToPlay())); aMoveTopToPlayFaceDown = new QAction(this); @@ -239,8 +252,25 @@ Player::Player(const ServerInfo_User &info, int _id, bool _local, bool _judge, T connect(aMoveTopCardsToExile, SIGNAL(triggered()), this, SLOT(actMoveTopCardsToExile())); aMoveTopCardToBottom = new QAction(this); connect(aMoveTopCardToBottom, SIGNAL(triggered()), this, SLOT(actMoveTopCardToBottom())); - aMoveBottomCardToGrave = new QAction(this); - connect(aMoveBottomCardToGrave, SIGNAL(triggered()), this, SLOT(actMoveBottomCardToGrave())); + + aDrawBottomCard = new QAction(this); + connect(aDrawBottomCard, SIGNAL(triggered()), this, SLOT(actDrawBottomCard())); + aDrawBottomCards = new QAction(this); + connect(aDrawBottomCards, SIGNAL(triggered()), this, SLOT(actDrawBottomCards())); + aMoveBottomToPlay = new QAction(this); + connect(aMoveBottomToPlay, SIGNAL(triggered()), this, SLOT(actMoveBottomCardToPlay())); + aMoveBottomToPlayFaceDown = new QAction(this); + connect(aMoveBottomToPlayFaceDown, SIGNAL(triggered()), this, SLOT(actMoveBottomCardToPlayFaceDown())); + aMoveBottomCardToGraveyard = new QAction(this); + connect(aMoveBottomCardToGraveyard, SIGNAL(triggered()), this, SLOT(actMoveBottomCardToGrave())); + aMoveBottomCardToExile = new QAction(this); + connect(aMoveBottomCardToExile, SIGNAL(triggered()), this, SLOT(actMoveBottomCardToExile())); + aMoveBottomCardsToGraveyard = new QAction(this); + connect(aMoveBottomCardsToGraveyard, SIGNAL(triggered()), this, SLOT(actMoveBottomCardsToGrave())); + aMoveBottomCardsToExile = new QAction(this); + connect(aMoveBottomCardsToExile, SIGNAL(triggered()), this, SLOT(actMoveBottomCardsToExile())); + aMoveBottomCardToTop = new QAction(this); + connect(aMoveBottomCardToTop, SIGNAL(triggered()), this, SLOT(actMoveBottomCardToTop())); } playerMenu = new TearOffMenu(); @@ -276,22 +306,34 @@ Player::Player(const ServerInfo_User &info, int _id, bool _local, bool _judge, T playerLists.append(mRevealLibrary = libraryMenu->addMenu(QString())); playerLists.append(mRevealTopCard = libraryMenu->addMenu(QString())); libraryMenu->addAction(aAlwaysRevealTopCard); + libraryMenu->addAction(aAlwaysLookAtTopCard); libraryMenu->addSeparator(); - libraryMenu->addAction(aMoveTopToPlay); - libraryMenu->addAction(aMoveTopToPlayFaceDown); - libraryMenu->addAction(aMoveTopCardToBottom); - libraryMenu->addAction(aMoveBottomCardToGrave); - libraryMenu->addSeparator(); - libraryMenu->addAction(aMoveTopCardToGraveyard); - libraryMenu->addAction(aMoveTopCardToExile); - libraryMenu->addAction(aMoveTopCardsToGraveyard); - libraryMenu->addAction(aMoveTopCardsToExile); + topLibraryMenu = libraryMenu->addTearOffMenu(QString()); + bottomLibraryMenu = libraryMenu->addTearOffMenu(QString()); libraryMenu->addSeparator(); libraryMenu->addAction(aOpenDeckInDeckEditor); deck->setMenu(libraryMenu, aDrawCard); - } else { - handMenu = nullptr; - libraryMenu = nullptr; + + topLibraryMenu->addAction(aMoveTopToPlay); + topLibraryMenu->addAction(aMoveTopToPlayFaceDown); + topLibraryMenu->addAction(aMoveTopCardToBottom); + topLibraryMenu->addSeparator(); + topLibraryMenu->addAction(aMoveTopCardToGraveyard); + topLibraryMenu->addAction(aMoveTopCardsToGraveyard); + topLibraryMenu->addAction(aMoveTopCardToExile); + topLibraryMenu->addAction(aMoveTopCardsToExile); + + bottomLibraryMenu->addAction(aDrawBottomCard); + bottomLibraryMenu->addAction(aDrawBottomCards); + bottomLibraryMenu->addSeparator(); + bottomLibraryMenu->addAction(aMoveBottomToPlay); + bottomLibraryMenu->addAction(aMoveBottomToPlayFaceDown); + bottomLibraryMenu->addAction(aMoveBottomCardToTop); + bottomLibraryMenu->addSeparator(); + bottomLibraryMenu->addAction(aMoveBottomCardToGraveyard); + bottomLibraryMenu->addAction(aMoveBottomCardsToGraveyard); + bottomLibraryMenu->addAction(aMoveBottomCardToExile); + bottomLibraryMenu->addAction(aMoveBottomCardsToExile); } graveMenu = playerMenu->addTearOffMenu(QString()); @@ -348,6 +390,7 @@ Player::Player(const ServerInfo_User &info, int _id, bool _local, bool _judge, T aCreateAnotherToken->setEnabled(false); createPredefinedTokenMenu = new QMenu(QString()); + createPredefinedTokenMenu->setEnabled(false); playerMenu->addSeparator(); countersMenu = playerMenu->addMenu(QString()); @@ -369,6 +412,7 @@ Player::Player(const ServerInfo_User &info, int _id, bool _local, bool _judge, T if (local || judge) { aCardMenu = new QAction(this); + aCardMenu->setEnabled(false); playerMenu->addSeparator(); playerMenu->addAction(aCardMenu); } else { @@ -558,11 +602,11 @@ void Player::playerListActionTriggered() if (menu == mRevealLibrary) { cmd.set_zone_name("deck"); } else if (menu == mRevealTopCard) { - int decksize = zones.value("deck")->getCards().size(); + int deckSize = zones.value("deck")->getCards().size(); bool ok; int number = QInputDialog::getInt(game, tr("Reveal top cards of library"), - tr("Number of cards: (max. %1)").arg(decksize), defaultNumberTopCards, 1, - decksize, 1, &ok); + tr("Number of cards: (max. %1)").arg(deckSize), defaultNumberTopCards, 1, + deckSize, 1, &ok); if (ok) { cmd.set_zone_name("deck"); cmd.set_top_cards(number); @@ -682,23 +726,36 @@ void Player::retranslateUi() aViewHand->setText(tr("&View hand")); aViewTopCards->setText(tr("View &top cards of library...")); mRevealLibrary->setTitle(tr("Reveal &library to...")); - mRevealTopCard->setTitle(tr("Reveal t&op cards to...")); + mRevealTopCard->setTitle(tr("Reveal &top cards to...")); + topLibraryMenu->setTitle(tr("&Top of library...")); + bottomLibraryMenu->setTitle(tr("&Bottom of library...")); aAlwaysRevealTopCard->setText(tr("&Always reveal top card")); - aOpenDeckInDeckEditor->setText(tr("O&pen deck in deck editor")); + aAlwaysLookAtTopCard->setText(tr("&Always look at top card")); + aOpenDeckInDeckEditor->setText(tr("&Open deck in deck editor")); aViewSideboard->setText(tr("&View sideboard")); aDrawCard->setText(tr("&Draw card")); aDrawCards->setText(tr("D&raw cards...")); aUndoDraw->setText(tr("&Undo last draw")); aMulligan->setText(tr("Take &mulligan")); aShuffle->setText(tr("&Shuffle")); - aMoveTopToPlay->setText(tr("Play top card")); + + aMoveTopToPlay->setText(tr("&Play top card")); aMoveTopToPlayFaceDown->setText(tr("Play top card &face down")); aMoveTopCardToGraveyard->setText(tr("Move top card to grave&yard")); aMoveTopCardToExile->setText(tr("Move top card to e&xile")); aMoveTopCardsToGraveyard->setText(tr("Move top cards to &graveyard...")); aMoveTopCardsToExile->setText(tr("Move top cards to &exile...")); aMoveTopCardToBottom->setText(tr("Put top card on &bottom")); - aMoveBottomCardToGrave->setText(tr("Put bottom card &in graveyard")); + + aDrawBottomCard->setText(tr("&Draw bottom card")); + aDrawBottomCards->setText(tr("D&raw bottom cards...")); + aMoveBottomToPlay->setText(tr("&Play bottom card")); + aMoveBottomToPlayFaceDown->setText(tr("Play bottom card &face down")); + aMoveBottomCardToGraveyard->setText(tr("Move bottom card to grave&yard")); + aMoveBottomCardToExile->setText(tr("Move bottom card to e&xile")); + aMoveBottomCardsToGraveyard->setText(tr("Move bottom cards to &graveyard...")); + aMoveBottomCardsToExile->setText(tr("Move bottom cards to &exile...")); + aMoveBottomCardToTop->setText(tr("Put bottom card on &top")); handMenu->setTitle(tr("&Hand")); mRevealHand->setTitle(tr("&Reveal hand to...")); @@ -719,7 +776,7 @@ void Player::retranslateUi() counterIterator.next().value()->retranslateUi(); } - aCardMenu->setText(tr("C&ard")); + aCardMenu->setText(tr("Selec&ted cards")); for (auto &allPlayersAction : allPlayersActions) { allPlayersAction->setText(tr("&All players")); @@ -861,6 +918,7 @@ void Player::setShortcutsActive() aCreateToken->setShortcut(shortcuts.getSingleShortcut("Player/aCreateToken")); aCreateAnotherToken->setShortcut(shortcuts.getSingleShortcut("Player/aCreateAnotherToken")); aAlwaysRevealTopCard->setShortcut(shortcuts.getSingleShortcut("Player/aAlwaysRevealTopCard")); + aAlwaysLookAtTopCard->setShortcut(shortcuts.getSingleShortcut("Player/aAlwaysLookAtTopCard")); aMoveTopToPlay->setShortcut(shortcuts.getSingleShortcut("Player/aMoveTopToPlay")); aMoveTopToPlayFaceDown->setShortcut(shortcuts.getSingleShortcut("Player/aMoveTopToPlayFaceDown")); aMoveTopCardToGraveyard->setShortcut(shortcuts.getSingleShortcut("Player/aMoveTopCardToGraveyard")); @@ -868,7 +926,15 @@ void Player::setShortcutsActive() aMoveTopCardToExile->setShortcut(shortcuts.getSingleShortcut("Player/aMoveTopCardToExile")); aMoveTopCardsToExile->setShortcut(shortcuts.getSingleShortcut("Player/aMoveTopCardsToExile")); aMoveTopCardToBottom->setShortcut(shortcuts.getSingleShortcut("Player/aMoveTopCardToBottom")); - aMoveBottomCardToGrave->setShortcut(shortcuts.getSingleShortcut("Player/aMoveBottomCardToGrave")); + aDrawBottomCard->setShortcut(shortcuts.getSingleShortcut("Player/aDrawBottomCard")); + aDrawBottomCards->setShortcut(shortcuts.getSingleShortcut("Player/aDrawBottomCards")); + aMoveBottomToPlay->setShortcut(shortcuts.getSingleShortcut("Player/aMoveBottomToPlay")); + aMoveBottomToPlayFaceDown->setShortcut(shortcuts.getSingleShortcut("Player/aMoveBottomToPlayFaceDown")); + aMoveBottomCardToGraveyard->setShortcut(shortcuts.getSingleShortcut("Player/aMoveBottomCardToGrave")); + aMoveBottomCardsToGraveyard->setShortcut(shortcuts.getSingleShortcut("Player/aMoveBottomCardsToGrave")); + aMoveBottomCardToExile->setShortcut(shortcuts.getSingleShortcut("Player/aMoveBottomCardToExile")); + aMoveBottomCardsToExile->setShortcut(shortcuts.getSingleShortcut("Player/aMoveBottomCardsToExile")); + aMoveBottomCardToTop->setShortcut(shortcuts.getSingleShortcut("Player/aMoveBottomCardToTop")); aPlayFacedown->setShortcut(shortcuts.getSingleShortcut("Player/aPlayFacedown")); aPlay->setShortcut(shortcuts.getSingleShortcut("Player/aPlay")); } @@ -892,12 +958,21 @@ void Player::setShortcutsInactive() aCreateToken->setShortcut(QKeySequence()); aCreateAnotherToken->setShortcut(QKeySequence()); aAlwaysRevealTopCard->setShortcut(QKeySequence()); + aAlwaysLookAtTopCard->setShortcut(QKeySequence()); aMoveTopToPlay->setShortcut(QKeySequence()); aMoveTopToPlayFaceDown->setShortcut(QKeySequence()); aMoveTopCardToGraveyard->setShortcut(QKeySequence()); aMoveTopCardsToGraveyard->setShortcut(QKeySequence()); aMoveTopCardToExile->setShortcut(QKeySequence()); aMoveTopCardsToExile->setShortcut(QKeySequence()); + aDrawBottomCard->setShortcut(QKeySequence()); + aDrawBottomCards->setShortcut(QKeySequence()); + aMoveBottomToPlay->setShortcut(QKeySequence()); + aMoveBottomToPlayFaceDown->setShortcut(QKeySequence()); + aMoveBottomCardToGraveyard->setShortcut(QKeySequence()); + aMoveBottomCardsToGraveyard->setShortcut(QKeySequence()); + aMoveBottomCardToExile->setShortcut(QKeySequence()); + aMoveBottomCardsToExile->setShortcut(QKeySequence()); QMapIterator counterIterator(counters); while (counterIterator.hasNext()) { @@ -910,6 +985,7 @@ void Player::initSayMenu() sayMenu->clear(); int count = SettingsCache::instance().messages().getCount(); + sayMenu->setEnabled(count > 0); for (int i = 0; i < count; ++i) { auto *newAction = new QAction(SettingsCache::instance().messages().getMessageAt(i), this); @@ -927,10 +1003,14 @@ void Player::setDeck(const DeckLoader &_deck) aOpenDeckInDeckEditor->setEnabled(deck); createPredefinedTokenMenu->clear(); + createPredefinedTokenMenu->setEnabled(false); predefinedTokens.clear(); InnerDecklistNode *tokenZone = dynamic_cast(deck->getRoot()->findChild(DECK_ZONE_TOKENS)); - if (tokenZone) + if (tokenZone) { + if (tokenZone->size() > 0) + createPredefinedTokenMenu->setEnabled(true); + for (int i = 0; i < tokenZone->size(); ++i) { const QString tokenName = tokenZone->at(i)->getName(); predefinedTokens.append(tokenName); @@ -940,6 +1020,7 @@ void Player::setDeck(const DeckLoader &_deck) } connect(a, SIGNAL(triggered()), this, SLOT(actCreatePredefinedToken())); } + } } void Player::actViewLibrary() @@ -954,9 +1035,11 @@ void Player::actViewHand() void Player::actViewTopCards() { + int deckSize = zones.value("deck")->getCards().size(); bool ok; - int number = QInputDialog::getInt(game, tr("View top cards of library"), tr("Number of cards:"), - defaultNumberTopCards, 1, 2000000000, 1, &ok); + int number = + QInputDialog::getInt(game, tr("View top cards of library"), tr("Number of cards: (max. %1)").arg(deckSize), + defaultNumberTopCards, 1, deckSize, 1, &ok); if (ok) { defaultNumberTopCards = number; static_cast(scene())->toggleZoneView(this, "deck", number); @@ -972,6 +1055,15 @@ void Player::actAlwaysRevealTopCard() sendGameCommand(cmd); } +void Player::actAlwaysLookAtTopCard() +{ + Command_ChangeZoneProperties cmd; + cmd.set_zone_name("deck"); + cmd.set_always_look_at_top_card(aAlwaysLookAtTopCard->isChecked()); + + sendGameCommand(cmd); +} + void Player::actOpenDeckInDeckEditor() { emit openDeckEditor(deck); @@ -1021,11 +1113,12 @@ void Player::actMulligan() { int startSize = SettingsCache::instance().getStartingHandSize(); int handSize = zones.value("hand")->getCards().size(); - int deckSize = zones.value("deck")->getCards().size() + handSize; + int deckSize = zones.value("deck")->getCards().size() + handSize; // hand is shuffled back into the deck bool ok; - int number = QInputDialog::getInt( - game, tr("Draw hand"), tr("Number of cards:") + '\n' + tr("0 and lower are in comparison to current hand size"), - startSize, -handSize, deckSize, 1, &ok); + int number = QInputDialog::getInt(game, tr("Draw hand"), + tr("Number of cards: (max. %1)").arg(deckSize) + '\n' + + tr("0 and lower are in comparison to current hand size"), + startSize, -handSize, deckSize, 1, &ok); if (!ok) { return; } @@ -1046,8 +1139,12 @@ void Player::actMulligan() void Player::actDrawCards() { - int number = QInputDialog::getInt(game, tr("Draw cards"), tr("Number:")); - if (number) { + int deckSize = zones.value("deck")->getCards().size(); + bool ok; + int number = QInputDialog::getInt(game, tr("Draw cards"), tr("Number of cards: (max. %1)").arg(deckSize), + defaultNumberTopCards, 1, deckSize, 1, &ok); + if (ok) { + defaultNumberTopCards = number; Command_DrawCards cmd; cmd.set_number(static_cast(number)); sendGameCommand(cmd); @@ -1059,6 +1156,24 @@ void Player::actUndoDraw() sendGameCommand(Command_UndoDraw()); } +void Player::cmdSetTopCard(Command_MoveCard &cmd) +{ + cmd.set_start_zone("deck"); + auto *cardToMove = cmd.mutable_cards_to_move()->add_card(); + cardToMove->set_card_id(0); + cmd.set_target_player_id(getId()); +} + +void Player::cmdSetBottomCard(Command_MoveCard &cmd) +{ + CardZone *zone = zones.value("deck"); + int lastCard = zone->getCards().size() - 1; + cmd.set_start_zone("deck"); + auto *cardToMove = cmd.mutable_cards_to_move()->add_card(); + cardToMove->set_card_id(lastCard); + cmd.set_target_player_id(getId()); +} + void Player::actMoveTopCardToGrave() { if (zones.value("deck")->getCards().empty()) { @@ -1066,9 +1181,7 @@ void Player::actMoveTopCardToGrave() } Command_MoveCard cmd; - cmd.set_start_zone("deck"); - cmd.mutable_cards_to_move()->add_card()->set_card_id(0); - cmd.set_target_player_id(getId()); + cmdSetTopCard(cmd); cmd.set_target_zone("grave"); cmd.set_x(0); cmd.set_y(0); @@ -1083,9 +1196,7 @@ void Player::actMoveTopCardToExile() } Command_MoveCard cmd; - cmd.set_start_zone("deck"); - cmd.mutable_cards_to_move()->add_card()->set_card_id(0); - cmd.set_target_player_id(getId()); + cmdSetTopCard(cmd); cmd.set_target_zone("rfg"); cmd.set_x(0); cmd.set_y(0); @@ -1095,15 +1206,21 @@ void Player::actMoveTopCardToExile() void Player::actMoveTopCardsToGrave() { - int number = QInputDialog::getInt(game, tr("Move top cards to grave"), tr("Number:")); - if (!number) { + const int maxCards = zones.value("deck")->getCards().size(); + if (maxCards == 0) { return; } - const int maxCards = zones.value("deck")->getCards().size(); - if (number > maxCards) { + bool ok; + int number = + QInputDialog::getInt(game, tr("Move top cards to grave"), tr("Number of cards: (max. %1)").arg(maxCards), + defaultNumberTopCards, 1, maxCards, 1, &ok); + if (!ok) { + return; + } else if (number > maxCards) { number = maxCards; } + defaultNumberTopCards = number; Command_MoveCard cmd; cmd.set_start_zone("deck"); @@ -1121,15 +1238,21 @@ void Player::actMoveTopCardsToGrave() void Player::actMoveTopCardsToExile() { - int number = QInputDialog::getInt(game, tr("Move top cards to exile"), tr("Number:")); - if (!number) { + const int maxCards = zones.value("deck")->getCards().size(); + if (maxCards == 0) { return; } - const int maxCards = zones.value("deck")->getCards().size(); - if (number > maxCards) { + bool ok; + int number = + QInputDialog::getInt(game, tr("Move top cards to exile"), tr("Number of cards: (max. %1)").arg(maxCards), + defaultNumberTopCards, 1, maxCards, 1, &ok); + if (!ok) { + return; + } else if (number > maxCards) { number = maxCards; } + defaultNumberTopCards = number; Command_MoveCard cmd; cmd.set_start_zone("deck"); @@ -1147,12 +1270,14 @@ void Player::actMoveTopCardsToExile() void Player::actMoveTopCardToBottom() { + if (zones.value("deck")->getCards().empty()) { + return; + } + Command_MoveCard cmd; - cmd.set_start_zone("deck"); - cmd.mutable_cards_to_move()->add_card()->set_card_id(0); - cmd.set_target_player_id(getId()); + cmdSetTopCard(cmd); cmd.set_target_zone("deck"); - cmd.set_x(-1); + cmd.set_x(-1); // bottom of deck cmd.set_y(0); sendGameCommand(cmd); @@ -1160,11 +1285,12 @@ void Player::actMoveTopCardToBottom() void Player::actMoveTopCardToPlay() { + if (zones.value("deck")->getCards().empty()) { + return; + } + Command_MoveCard cmd; - cmd.set_start_zone("deck"); - CardToMove *cardToMove = cmd.mutable_cards_to_move()->add_card(); - cardToMove->set_card_id(0); - cmd.set_target_player_id(getId()); + cmdSetTopCard(cmd); cmd.set_target_zone("stack"); cmd.set_x(-1); cmd.set_y(0); @@ -1174,6 +1300,10 @@ void Player::actMoveTopCardToPlay() void Player::actMoveTopCardToPlayFaceDown() { + if (zones.value("deck")->getCards().empty()) { + return; + } + Command_MoveCard cmd; cmd.set_start_zone("deck"); CardToMove *cardToMove = cmd.mutable_cards_to_move()->add_card(); @@ -1189,15 +1319,194 @@ void Player::actMoveTopCardToPlayFaceDown() void Player::actMoveBottomCardToGrave() { - CardZone *zone = zones.value("deck"); + if (zones.value("deck")->getCards().empty()) { + return; + } + + Command_MoveCard cmd; + cmdSetBottomCard(cmd); + cmd.set_target_zone("grave"); + cmd.set_x(0); + cmd.set_y(0); + + sendGameCommand(cmd); +} + +void Player::actMoveBottomCardToExile() +{ + if (zones.value("deck")->getCards().empty()) { + return; + } + + Command_MoveCard cmd; + cmdSetBottomCard(cmd); + cmd.set_target_zone("rfg"); + cmd.set_x(0); + cmd.set_y(0); + + sendGameCommand(cmd); +} + +void Player::actMoveBottomCardsToGrave() +{ + const int maxCards = zones.value("deck")->getCards().size(); + if (maxCards == 0) { + return; + } + + bool ok; + int number = + QInputDialog::getInt(game, tr("Move bottom cards to grave"), tr("Number of cards: (max. %1)").arg(maxCards), + defaultNumberBottomCards, 1, maxCards, 1, &ok); + if (!ok) { + return; + } else if (number > maxCards) { + number = maxCards; + } + defaultNumberBottomCards = number; + Command_MoveCard cmd; cmd.set_start_zone("deck"); - cmd.mutable_cards_to_move()->add_card()->set_card_id(zone->getCards().size() - 1); cmd.set_target_player_id(getId()); cmd.set_target_zone("grave"); cmd.set_x(0); cmd.set_y(0); + for (int i = maxCards - number; i < maxCards; ++i) { + cmd.mutable_cards_to_move()->add_card()->set_card_id(i); + } + + sendGameCommand(cmd); +} + +void Player::actMoveBottomCardsToExile() +{ + const int maxCards = zones.value("deck")->getCards().size(); + if (maxCards == 0) { + return; + } + + bool ok; + int number = + QInputDialog::getInt(game, tr("Move bottom cards to exile"), tr("Number of cards: (max. %1)").arg(maxCards), + defaultNumberBottomCards, 1, maxCards, 1, &ok); + if (!ok) { + return; + } else if (number > maxCards) { + number = maxCards; + } + defaultNumberBottomCards = number; + + Command_MoveCard cmd; + cmd.set_start_zone("deck"); + cmd.set_target_player_id(getId()); + cmd.set_target_zone("rfg"); + cmd.set_x(0); + cmd.set_y(0); + + for (int i = maxCards - number; i < maxCards; ++i) { + cmd.mutable_cards_to_move()->add_card()->set_card_id(i); + } + + sendGameCommand(cmd); +} + +void Player::actMoveBottomCardToTop() +{ + if (zones.value("deck")->getCards().empty()) { + return; + } + + Command_MoveCard cmd; + cmdSetBottomCard(cmd); + cmd.set_target_zone("deck"); + cmd.set_x(0); // top of deck + cmd.set_y(0); + + sendGameCommand(cmd); +} + +void Player::actDrawBottomCard() +{ + if (zones.value("deck")->getCards().empty()) { + return; + } + + Command_MoveCard cmd; + cmdSetBottomCard(cmd); + cmd.set_target_zone("hand"); + cmd.set_x(0); + cmd.set_y(0); + + sendGameCommand(cmd); +} + +void Player::actDrawBottomCards() +{ + const int maxCards = zones.value("deck")->getCards().size(); + if (maxCards == 0) { + return; + } + + bool ok; + int number = QInputDialog::getInt(game, tr("Draw bottom cards"), tr("Number of cards: (max. %1)").arg(maxCards), + defaultNumberBottomCards, 1, maxCards, 1, &ok); + if (!ok) { + return; + } else if (number > maxCards) { + number = maxCards; + } + defaultNumberBottomCards = number; + + Command_MoveCard cmd; + cmd.set_start_zone("deck"); + cmd.set_target_player_id(getId()); + cmd.set_target_zone("hand"); + cmd.set_x(0); + cmd.set_y(0); + + for (int i = maxCards - number; i < maxCards; ++i) { + cmd.mutable_cards_to_move()->add_card()->set_card_id(i); + } + + sendGameCommand(cmd); +} + +void Player::actMoveBottomCardToPlay() +{ + if (zones.value("deck")->getCards().empty()) { + return; + } + + Command_MoveCard cmd; + cmdSetBottomCard(cmd); + cmd.set_target_zone("stack"); + cmd.set_x(-1); + cmd.set_y(0); + + sendGameCommand(cmd); +} + +void Player::actMoveBottomCardToPlayFaceDown() +{ + if (zones.value("deck")->getCards().empty()) { + return; + } + + CardZone *zone = zones.value("deck"); + int lastCard = zone->getCards().size() - 1; + + Command_MoveCard cmd; + cmd.set_start_zone("deck"); + auto *cardToMove = cmd.mutable_cards_to_move()->add_card(); + cardToMove->set_card_id(lastCard); + cardToMove->set_face_down(true); + + cmd.set_target_player_id(getId()); + cmd.set_target_zone("table"); + cmd.set_x(-1); + cmd.set_y(0); + sendGameCommand(cmd); } @@ -1214,8 +1523,10 @@ void Player::actUntapAll() void Player::actRollDie() { bool ok; - int sides = QInputDialog::getInt(game, tr("Roll die"), tr("Number of sides:"), 20, 2, 1000, 1, &ok); + int sides = QInputDialog::getInt(game, tr("Roll die"), tr("Number of sides:"), defaultNumberDieRoll, minDieRoll, + maxDieRoll, 1, &ok); if (ok) { + defaultNumberDieRoll = sides; Command_RollDie cmd; cmd.set_sides(static_cast(sides)); sendGameCommand(cmd); @@ -1231,7 +1542,7 @@ void Player::actCreateToken() lastTokenName = dlg.getName(); lastTokenPT = dlg.getPT(); - CardInfoPtr correctedCard = db->getCardBySimpleName(lastTokenName); + CardInfoPtr correctedCard = db->guessCard(lastTokenName); if (correctedCard) { lastTokenName = correctedCard->getName(); lastTokenTableRow = TableZone::clampValidTableRow(2 - correctedCard->getTableRow()); @@ -1533,8 +1844,10 @@ void Player::eventShuffle(const Event_Shuffle &event) if (!zone) { return; } - if (zone->getView() && zone->getView()->getRevealZone()) { - zone->getView()->setWriteableRevealZone(false); + for (auto *view : zone->getViews()) { + if (view != nullptr) { + emit view->beingDeleted(); + } } emit logShuffle(this, zone, event.start(), event.end()); } @@ -1670,19 +1983,6 @@ void Player::eventDumpZone(const Event_DumpZone &event) emit logDumpZone(this, zone, event.number_cards()); } -void Player::eventStopDumpZone(const Event_StopDumpZone &event) -{ - Player *zoneOwner = game->getPlayers().value(event.zone_owner_id(), 0); - if (!zoneOwner) { - return; - } - CardZone *zone = zoneOwner->getZones().value(QString::fromStdString(event.zone_name()), 0); - if (!zone) { - return; - } - emit logStopDumpZone(this, zone); -} - void Player::eventMoveCard(const Event_MoveCard &event, const GameEventContext &context) { Player *startPlayer = game->getPlayers().value(event.start_player_id()); @@ -1955,6 +2255,10 @@ void Player::eventChangeZoneProperties(const Event_ChangeZoneProperties &event) zone->setAlwaysRevealTopCard(event.always_reveal_top_card()); emit logAlwaysRevealTopCard(this, zone, event.always_reveal_top_card()); } + if (event.has_always_look_at_top_card()) { + zone->setAlwaysRevealTopCard(event.always_look_at_top_card()); + emit logAlwaysLookAtTopCard(this, zone, event.always_look_at_top_card()); + } } void Player::processGameEvent(GameEvent::GameEventType type, const GameEvent &event, const GameEventContext &context) @@ -1996,9 +2300,6 @@ void Player::processGameEvent(GameEvent::GameEventType type, const GameEvent &ev case GameEvent::DUMP_ZONE: eventDumpZone(event.GetExtension(Event_DumpZone::ext)); break; - case GameEvent::STOP_DUMP_ZONE: - eventStopDumpZone(event.GetExtension(Event_StopDumpZone::ext)); - break; case GameEvent::MOVE_CARD: eventMoveCard(event.GetExtension(Event_MoveCard::ext), context); break; @@ -2404,11 +2705,13 @@ bool Player::clearCardsToDelete() void Player::actMoveCardXCardsFromTop() { + int deckSize = zones.value("deck")->getCards().size() + 1; // add the card to move to the deck bool ok; - int number = QInputDialog::getInt(game, tr("Place card X cards from top of library"), - tr("How many cards from the top of the deck should this card be placed:"), - defaultNumberTopCardsToPlaceBelow, 1, 2000000000, 1, &ok); - number--; + int number = + QInputDialog::getInt(game, tr("Place card X cards from top of library"), + tr("Which position should this card be placed:") + "\n" + tr("(max. %1)").arg(deckSize), + defaultNumberTopCardsToPlaceBelow, 1, deckSize, 1, &ok); + number -= 1; // indexes start at 0 if (!ok) { return; @@ -3219,6 +3522,7 @@ void Player::addRelatedCardActions(const CardItem *card, QMenu *cardMenu) void Player::setCardMenu(QMenu *menu) { if (aCardMenu) { + aCardMenu->setEnabled(menu != nullptr); aCardMenu->setMenu(menu); } } @@ -3249,6 +3553,7 @@ void Player::setGameStarted() { if (local) { aAlwaysRevealTopCard->setChecked(false); + aAlwaysLookAtTopCard->setChecked(false); } setConceded(false); } @@ -3266,6 +3571,7 @@ void Player::setConceded(bool _conceded) void Player::setZoneId(int _zoneId) { zoneId = _zoneId; + playerArea->setPlayerZoneId(_zoneId); } void Player::setMirrored(bool _mirrored) diff --git a/cockatrice/src/player.h b/cockatrice/src/player.h index 4dbdf5943..363fbe6c8 100644 --- a/cockatrice/src/player.h +++ b/cockatrice/src/player.h @@ -18,52 +18,51 @@ namespace protobuf class Message; } } // namespace google -class CardDatabase; -class DeckLoader; -class QMenu; -class QAction; -class ZoneViewZone; -class TabGame; -class AbstractCounter; class AbstractCardItem; -class CardItem; -class ArrowTarget; +class AbstractCounter; class ArrowItem; +class ArrowTarget; +class CardDatabase; +class CardItem; class CardZone; -class StackZone; -class TableZone; -class HandZone; -class PlayerTarget; -class ServerInfo_User; -class ServerInfo_Player; -class ServerInfo_Arrow; -class ServerInfo_Counter; class CommandContainer; +class Command_MoveCard; +class DeckLoader; +class Event_AttachCard; +class Event_ChangeZoneProperties; +class Event_CreateArrow; +class Event_CreateCounter; +class Event_CreateToken; +class Event_DelCounter; +class Event_DeleteArrow; +class Event_DestroyCard; +class Event_DrawCards; +class Event_DumpZone; +class Event_FlipCard; +class Event_GameSay; +class Event_MoveCard; +class Event_RevealCards; +class Event_RollDie; +class Event_SetCardAttr; +class Event_SetCardCounter; +class Event_SetCounter; +class Event_Shuffle; class GameCommand; class GameEvent; class GameEventContext; -// class Event_ConnectionStateChanged; -class Event_GameSay; -class Event_Shuffle; -class Event_RollDie; -class Event_CreateArrow; -class Event_DeleteArrow; -class Event_CreateToken; -class Event_SetCardAttr; -class Event_SetCardCounter; -class Event_CreateCounter; -class Event_SetCounter; -class Event_DelCounter; -class Event_DumpZone; -class Event_StopDumpZone; -class Event_MoveCard; -class Event_FlipCard; -class Event_DestroyCard; -class Event_AttachCard; -class Event_DrawCards; -class Event_RevealCards; -class Event_ChangeZoneProperties; +class HandZone; class PendingCommand; +class PlayerTarget; +class QAction; +class QMenu; +class ServerInfo_Arrow; +class ServerInfo_Counter; +class ServerInfo_Player; +class ServerInfo_User; +class StackZone; +class TabGame; +class TableZone; +class ZoneViewZone; const int MAX_TOKENS_PER_DIALOG = 99; @@ -73,6 +72,7 @@ class PlayerArea : public QObject, public QGraphicsItem Q_INTERFACES(QGraphicsItem) private: QRectF bRect; + int playerZoneId; private slots: void updateBg(); @@ -94,6 +94,12 @@ public: void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; void setSize(qreal width, qreal height); + + void setPlayerZoneId(int _playerZoneId); + int getPlayerZoneId() const + { + return playerZoneId; + } }; class Player : public QObject, public QGraphicsItem @@ -128,7 +134,6 @@ signals: void logSetPT(Player *player, CardItem *card, QString newPT); void logSetAnnotation(Player *player, CardItem *card, QString newAnnotation); void logDumpZone(Player *player, CardZone *zone, int numberCards); - void logStopDumpZone(Player *player, CardZone *zone); void logRevealCards(Player *player, CardZone *zone, int cardId, @@ -137,6 +142,7 @@ signals: bool faceDown, int amount); void logAlwaysRevealTopCard(Player *player, CardZone *zone, bool reveal); + void logAlwaysLookAtTopCard(Player *player, CardZone *zone, bool reveal); void sizeChanged(); void playerCountChanged(); @@ -157,12 +163,21 @@ public slots: void actMoveTopCardsToGrave(); void actMoveTopCardsToExile(); void actMoveTopCardToBottom(); + void actDrawBottomCard(); + void actDrawBottomCards(); + void actMoveBottomCardToPlay(); + void actMoveBottomCardToPlayFaceDown(); void actMoveBottomCardToGrave(); + void actMoveBottomCardToExile(); + void actMoveBottomCardsToGrave(); + void actMoveBottomCardsToExile(); + void actMoveBottomCardToTop(); void actViewLibrary(); void actViewHand(); void actViewTopCards(); void actAlwaysRevealTopCard(); + void actAlwaysLookAtTopCard(); void actViewGraveyard(); void actRevealRandomGraveyardCard(); void actViewRfg(); @@ -208,17 +223,20 @@ private: TabGame *game; QMenu *sbMenu, *countersMenu, *sayMenu, *createPredefinedTokenMenu, *mRevealLibrary, *mRevealTopCard, *mRevealHand, *mRevealRandomHandCard, *mRevealRandomGraveyardCard; - TearOffMenu *moveGraveMenu, *moveRfgMenu, *graveMenu, *moveHandMenu, *handMenu, *libraryMenu, *rfgMenu, *playerMenu; + TearOffMenu *moveGraveMenu, *moveRfgMenu, *graveMenu, *moveHandMenu, *handMenu, *libraryMenu, *topLibraryMenu, + *bottomLibraryMenu, *rfgMenu, *playerMenu; QList playerLists; QList allPlayersActions; QAction *aMoveHandToTopLibrary, *aMoveHandToBottomLibrary, *aMoveHandToGrave, *aMoveHandToRfg, *aMoveGraveToTopLibrary, *aMoveGraveToBottomLibrary, *aMoveGraveToHand, *aMoveGraveToRfg, *aMoveRfgToTopLibrary, *aMoveRfgToBottomLibrary, *aMoveRfgToHand, *aMoveRfgToGrave, *aViewHand, *aViewLibrary, *aViewTopCards, - *aAlwaysRevealTopCard, *aOpenDeckInDeckEditor, *aMoveTopCardToGraveyard, *aMoveTopCardToExile, - *aMoveTopCardsToGraveyard, *aMoveTopCardsToExile, *aMoveTopCardToBottom, *aViewGraveyard, *aViewRfg, - *aViewSideboard, *aDrawCard, *aDrawCards, *aUndoDraw, *aMulligan, *aShuffle, *aMoveTopToPlay, + *aAlwaysRevealTopCard, *aAlwaysLookAtTopCard, *aOpenDeckInDeckEditor, *aMoveTopCardToGraveyard, + *aMoveTopCardToExile, *aMoveTopCardsToGraveyard, *aMoveTopCardsToExile, *aMoveTopCardToBottom, *aViewGraveyard, + *aViewRfg, *aViewSideboard, *aDrawCard, *aDrawCards, *aUndoDraw, *aMulligan, *aShuffle, *aMoveTopToPlay, *aMoveTopToPlayFaceDown, *aUntapAll, *aRollDie, *aCreateToken, *aCreateAnotherToken, *aCardMenu, - *aMoveBottomCardToGrave; + *aMoveBottomToPlay, *aMoveBottomToPlayFaceDown, *aMoveBottomCardToTop, *aMoveBottomCardToGraveyard, + *aMoveBottomCardToExile, *aMoveBottomCardsToGraveyard, *aMoveBottomCardsToExile, *aDrawBottomCard, + *aDrawBottomCards; QList aAddCounter, aSetCounter, aRemoveCounter; QAction *aPlay, *aPlayFacedown, *aHide, *aTap, *aDoesntUntap, *aAttach, *aUnattach, *aDrawArrow, *aSetPT, *aResetPT, @@ -227,8 +245,12 @@ private: *aMoveToXfromTopOfLibrary; bool shortcutsActive; - int defaultNumberTopCards; - int defaultNumberTopCardsToPlaceBelow; + int defaultNumberTopCards = 1; + int defaultNumberTopCardsToPlaceBelow = 1; + int defaultNumberBottomCards = 1; + int defaultNumberDieRoll = 20; + static constexpr int minDieRoll = 2; + static constexpr int maxDieRoll = 1000000; QString lastTokenName, lastTokenColor, lastTokenPT, lastTokenAnnotation; bool lastTokenDestroy; int lastTokenTableRow; @@ -288,7 +310,6 @@ private: void eventSetCounter(const Event_SetCounter &event); void eventDelCounter(const Event_DelCounter &event); void eventDumpZone(const Event_DumpZone &event); - void eventStopDumpZone(const Event_StopDumpZone &event); void eventMoveCard(const Event_MoveCard &event, const GameEventContext &context); void eventFlipCard(const Event_FlipCard &event); void eventDestroyCard(const Event_DestroyCard &event); @@ -296,6 +317,8 @@ private: void eventDrawCards(const Event_DrawCards &event); void eventRevealCards(const Event_RevealCards &event); void eventChangeZoneProperties(const Event_ChangeZoneProperties &event); + void cmdSetTopCard(Command_MoveCard &cmd); + void cmdSetBottomCard(Command_MoveCard &cmd); QVariantList parsePT(const QString &pt); diff --git a/cockatrice/src/playerlistwidget.cpp b/cockatrice/src/playerlistwidget.cpp index ebdf2ff5d..c4c706d99 100644 --- a/cockatrice/src/playerlistwidget.cpp +++ b/cockatrice/src/playerlistwidget.cpp @@ -5,9 +5,9 @@ #include "pb/serverinfo_playerproperties.pb.h" #include "pb/session_commands.pb.h" #include "pixmapgenerator.h" +#include "tab_account.h" #include "tab_game.h" #include "tab_supervisor.h" -#include "tab_userlists.h" #include "user_context_menu.h" #include "userlist.h" diff --git a/cockatrice/src/playertarget.cpp b/cockatrice/src/playertarget.cpp index fb4c9f3d6..194bfcda1 100644 --- a/cockatrice/src/playertarget.cpp +++ b/cockatrice/src/playertarget.cpp @@ -24,7 +24,7 @@ PlayerCounter::PlayerCounter(Player *_player, QRectF PlayerCounter::boundingRect() const { - return QRectF(0, 0, 50, 30); + return {0, 0, 50, 30}; } void PlayerCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) @@ -56,13 +56,14 @@ void PlayerCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * /* } PlayerTarget::PlayerTarget(Player *_owner, QGraphicsItem *parentItem, QWidget *_game) - : ArrowTarget(_owner, parentItem), playerCounter(0), game(_game) + : ArrowTarget(_owner, parentItem), playerCounter(nullptr), game(_game) { setCacheMode(DeviceCoordinateCache); const std::string &bmp = _owner->getUserInfo()->avatar_bmp(); - if (!fullPixmap.loadFromData((const uchar *)bmp.data(), bmp.size())) + if (!fullPixmap.loadFromData((const uchar *)bmp.data(), static_cast(bmp.size()))) { fullPixmap = QPixmap(); + } } PlayerTarget::~PlayerTarget() @@ -74,7 +75,7 @@ PlayerTarget::~PlayerTarget() QRectF PlayerTarget::boundingRect() const { - return QRectF(0, 0, 160, 64); + return {0, 0, 160, 64}; } void PlayerTarget::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) @@ -153,7 +154,7 @@ void PlayerTarget::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*o AbstractCounter *PlayerTarget::addCounter(int _counterId, const QString &_name, int _value) { if (playerCounter) { - disconnect(playerCounter, 0, this, 0); + disconnect(playerCounter, nullptr, this, nullptr); playerCounter->delCounter(); } @@ -167,5 +168,5 @@ AbstractCounter *PlayerTarget::addCounter(int _counterId, const QString &_name, void PlayerTarget::counterDeleted() { - playerCounter = 0; + playerCounter = nullptr; } diff --git a/cockatrice/src/releasechannel.cpp b/cockatrice/src/releasechannel.cpp index 9bd6efae3..0ce28839a 100644 --- a/cockatrice/src/releasechannel.cpp +++ b/cockatrice/src/releasechannel.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -45,24 +46,19 @@ void ReleaseChannel::checkForUpdates() #if defined(Q_OS_OSX) bool ReleaseChannel::downloadMatchesCurrentOS(const QString &fileName) { - const int mac_os_version = QSysInfo::productVersion().split(".")[1].toInt(); - - // TODO: If we change macOS builds, this must be updated - if (mac_os_version <= 12) { - // We no longer compile files for macOS 10.12 Sierra or older + static QRegularExpression version_regex("macOS-(\\d+)\\.(\\d+)"); + auto match = version_regex.match(fileName); + if (!match.hasMatch()) { return false; - } else if (mac_os_version == 13) { - // We support 10.13 High Sierra - return fileName.contains("macos10.13"); - } else if (14 <= mac_os_version && mac_os_version <= 15) { - // We support 10.14 Mojave, and 10.15 Catalina - return fileName.contains("macos10.14"); - } else { - // Future Mac releases we haven't heard of or accounted for yet - return (!fileName.contains("macos10.13") && !fileName.contains("macos10.14") && fileName.contains("macos")); } -} + // older(smaller) releases are compatible with a newer or the same system version + int sys_maj = QSysInfo::productVersion().split(".")[0].toInt(); + int sys_min = QSysInfo::productVersion().split(".")[1].toInt(); + int rel_maj = match.captured(1).toInt(); + int rel_min = match.captured(2).toInt(); + return rel_maj < sys_maj || (rel_maj == sys_maj && rel_min <= sys_min); +} #elif defined(Q_OS_WIN) bool ReleaseChannel::downloadMatchesCurrentOS(const QString &fileName) { @@ -283,18 +279,21 @@ void BetaReleaseChannel::fileListFinished() bool needToUpdate = (QString::compare(shortHash, myHash, Qt::CaseInsensitive) != 0); bool compatibleVersion = false; - foreach (QVariant file, resultList) { + QStringList resultUrlList{}; + for (QVariant file : resultList) { QVariantMap map = file.toMap(); + resultUrlList << map["browser_download_url"].toString(); + } - QString url = map["browser_download_url"].toString(); - - if (!downloadMatchesCurrentOS(url)) - continue; - - compatibleVersion = true; - lastRelease->setDownloadUrl(url); - qDebug() << "Found compatible version url=" << url; - break; + resultUrlList.sort(); + // iterate in reverse so the first item is the latest os version + for (auto url = resultUrlList.rbegin(); url < resultUrlList.rend(); ++url) { + if (downloadMatchesCurrentOS(*url)) { + compatibleVersion = true; + lastRelease->setDownloadUrl(*url); + qDebug() << "Found compatible version url=" << *url; + break; + } } emit finishedCheck(needToUpdate, compatibleVersion, lastRelease); diff --git a/cockatrice/src/remoteclient.cpp b/cockatrice/src/remoteclient.cpp index d6fb7bd8f..47f227440 100644 --- a/cockatrice/src/remoteclient.cpp +++ b/cockatrice/src/remoteclient.cpp @@ -337,9 +337,9 @@ void RemoteClient::websocketMessageReceived(const QByteArray &message) void RemoteClient::sendCommandContainer(const CommandContainer &cont) { #if GOOGLE_PROTOBUF_VERSION > 3001000 - unsigned int size = cont.ByteSizeLong(); + auto size = static_cast(cont.ByteSizeLong()); #else - unsigned int size = cont.ByteSize(); + auto size = static_cast(cont.ByteSize()); #endif #ifdef QT_DEBUG qDebug() << "OUT" << size << QString::fromStdString(cont.ShortDebugString()); @@ -419,7 +419,7 @@ void RemoteClient::doActivateToServer(const QString &_token) token = _token.trimmed(); - connectToHost(lastHostname, static_cast(lastPort)); + connectToHost(lastHostname, lastPort); setStatus(StatusActivating); } @@ -502,7 +502,7 @@ void RemoteClient::disconnectFromServer() emit sigDisconnectFromServer(); } -QString RemoteClient::getSrvClientID(const QString _hostname) +QString RemoteClient::getSrvClientID(const QString &_hostname) { QString srvClientID = SettingsCache::instance().getClientID(); QHostInfo hostInfo = QHostInfo::fromName(_hostname); @@ -518,11 +518,11 @@ QString RemoteClient::getSrvClientID(const QString _hostname) return uniqueServerClientID; } -bool RemoteClient::newMissingFeatureFound(QString _serversMissingFeatures) +bool RemoteClient::newMissingFeatureFound(const QString &_serversMissingFeatures) { bool newMissingFeature = false; QStringList serversMissingFeaturesList = _serversMissingFeatures.split(","); - foreach (const QString &feature, serversMissingFeaturesList) { + for (const QString &feature : serversMissingFeaturesList) { if (!feature.isEmpty()) { if (!SettingsCache::instance().getKnownMissingFeatures().contains(feature)) return true; @@ -535,7 +535,7 @@ void RemoteClient::clearNewClientFeatures() { QString newKnownMissingFeatures; QStringList existingKnownMissingFeatures = SettingsCache::instance().getKnownMissingFeatures().split(","); - foreach (const QString &existingKnownFeature, existingKnownMissingFeatures) { + for (const QString &existingKnownFeature : existingKnownMissingFeatures) { if (!existingKnownFeature.isEmpty()) { if (!clientFeatures.contains(existingKnownFeature)) newKnownMissingFeatures.append("," + existingKnownFeature); @@ -566,7 +566,7 @@ void RemoteClient::doRequestForgotPasswordToServer(const QString &hostname, unsi lastHostname = hostname; lastPort = port; - connectToHost(lastHostname, static_cast(lastPort)); + connectToHost(lastHostname, lastPort); setStatus(StatusRequestingForgotPassword); } @@ -598,7 +598,7 @@ void RemoteClient::doSubmitForgotPasswordResetToServer(const QString &hostname, token = _token.trimmed(); password = _newpassword; - connectToHost(lastHostname, static_cast(lastPort)); + connectToHost(lastHostname, lastPort); setStatus(StatusSubmitForgotPasswordReset); } @@ -632,7 +632,7 @@ void RemoteClient::doSubmitForgotPasswordChallengeToServer(const QString &hostna lastPort = port; email = _email; - connectToHost(lastHostname, static_cast(lastPort)); + connectToHost(lastHostname, lastPort); setStatus(StatusSubmitForgotPasswordChallenge); } diff --git a/cockatrice/src/remoteclient.h b/cockatrice/src/remoteclient.h index 3639a0ab9..9ae7e6284 100644 --- a/cockatrice/src/remoteclient.h +++ b/cockatrice/src/remoteclient.h @@ -97,10 +97,10 @@ private: QTcpSocket *socket; QWebSocket *websocket; QString lastHostname; - int lastPort; + unsigned int lastPort; - QString getSrvClientID(QString _hostname); - bool newMissingFeatureFound(QString _serversMissingFeatures); + QString getSrvClientID(const QString &_hostname); + bool newMissingFeatureFound(const QString &_serversMissingFeatures); void clearNewClientFeatures(); void connectToHost(const QString &hostname, unsigned int port); diff --git a/cockatrice/src/settings/gamefilterssettings.cpp b/cockatrice/src/settings/gamefilterssettings.cpp index 58d9fab27..8bd2c8ff4 100644 --- a/cockatrice/src/settings/gamefilterssettings.cpp +++ b/cockatrice/src/settings/gamefilterssettings.cpp @@ -1,6 +1,7 @@ #include "gamefilterssettings.h" #include +#include GameFiltersSettings::GameFiltersSettings(QString settingPath, QObject *parent) : SettingsManager(settingPath + "gamefilters.ini", parent) @@ -27,14 +28,25 @@ bool GameFiltersSettings::isShowBuddiesOnlyGames() return previous == QVariant() ? true : previous.toBool(); } -void GameFiltersSettings::setUnavailableGamesVisible(bool enabled) +void GameFiltersSettings::setShowFullGames(bool show) { - setValue(enabled, "unavailable_games_visible", "filter_games"); + setValue(show, "show_full_games", "filter_games"); } -bool GameFiltersSettings::isUnavailableGamesVisible() +bool GameFiltersSettings::isShowFullGames() { - QVariant previous = getValue("unavailable_games_visible", "filter_games"); + QVariant previous = getValue("show_full_games", "filter_games"); + return previous == QVariant() ? false : previous.toBool(); +} + +void GameFiltersSettings::setShowGamesThatStarted(bool show) +{ + setValue(show, "show_games_that_started", "filter_games"); +} + +bool GameFiltersSettings::isShowGamesThatStarted() +{ + QVariant previous = getValue("show_games_that_started", "filter_games"); return previous == QVariant() ? false : previous.toBool(); } @@ -102,6 +114,17 @@ int GameFiltersSettings::getMaxPlayers() return previous == QVariant() ? 99 : previous.toInt(); } +void GameFiltersSettings::setMaxGameAge(const QTime &maxGameAge) +{ + setValue(maxGameAge, "max_game_age_time", "filter_games"); +} + +QTime GameFiltersSettings::getMaxGameAge() +{ + QVariant previous = getValue("max_game_age_time", "filter_games"); + return previous.toTime(); +} + void GameFiltersSettings::setGameTypeEnabled(QString gametype, bool enabled) { setValue(enabled, "game_type/" + hashGameType(gametype), "filter_games"); @@ -117,3 +140,47 @@ bool GameFiltersSettings::isGameTypeEnabled(QString gametype) QVariant previous = getValue("game_type/" + hashGameType(gametype), "filter_games"); return previous == QVariant() ? false : previous.toBool(); } + +void GameFiltersSettings::setShowOnlyIfSpectatorsCanWatch(bool show) +{ + setValue(show, "show_only_if_spectators_can_watch", "filter_games"); +} + +bool GameFiltersSettings::isShowOnlyIfSpectatorsCanWatch() +{ + QVariant previous = getValue("show_only_if_spectators_can_watch", "filter_games"); + return previous == QVariant() ? false : previous.toBool(); +} + +void GameFiltersSettings::setShowSpectatorPasswordProtected(bool show) +{ + setValue(show, "show_spectator_password_protected", "filter_games"); +} + +bool GameFiltersSettings::isShowSpectatorPasswordProtected() +{ + QVariant previous = getValue("show_spectator_password_protected", "filter_games"); + return previous == QVariant() ? true : previous.toBool(); +} + +void GameFiltersSettings::setShowOnlyIfSpectatorsCanChat(bool show) +{ + setValue(show, "show_only_if_spectators_can_chat", "filter_games"); +} + +bool GameFiltersSettings::isShowOnlyIfSpectatorsCanChat() +{ + QVariant previous = getValue("show_only_if_spectators_can_chat", "filter_games"); + return previous == QVariant() ? true : previous.toBool(); +} + +void GameFiltersSettings::setShowOnlyIfSpectatorsCanSeeHands(bool show) +{ + setValue(show, "show_only_if_spectators_can_see_hands", "filter_games"); +} + +bool GameFiltersSettings::isShowOnlyIfSpectatorsCanSeeHands() +{ + QVariant previous = getValue("show_only_if_spectators_can_see_hands", "filter_games"); + return previous == QVariant() ? true : previous.toBool(); +} \ No newline at end of file diff --git a/cockatrice/src/settings/gamefilterssettings.h b/cockatrice/src/settings/gamefilterssettings.h index a24d423f0..fe168f53d 100644 --- a/cockatrice/src/settings/gamefilterssettings.h +++ b/cockatrice/src/settings/gamefilterssettings.h @@ -10,25 +10,37 @@ class GameFiltersSettings : public SettingsManager public: bool isShowBuddiesOnlyGames(); - bool isUnavailableGamesVisible(); + bool isShowFullGames(); + bool isShowGamesThatStarted(); bool isShowPasswordProtectedGames(); bool isHideIgnoredUserGames(); QString getGameNameFilter(); QString getCreatorNameFilter(); int getMinPlayers(); int getMaxPlayers(); + QTime getMaxGameAge(); bool isGameTypeEnabled(QString gametype); + bool isShowOnlyIfSpectatorsCanWatch(); + bool isShowSpectatorPasswordProtected(); + bool isShowOnlyIfSpectatorsCanChat(); + bool isShowOnlyIfSpectatorsCanSeeHands(); void setShowBuddiesOnlyGames(bool show); void setHideIgnoredUserGames(bool hide); - void setUnavailableGamesVisible(bool enabled); + void setShowFullGames(bool show); + void setShowGamesThatStarted(bool show); void setShowPasswordProtectedGames(bool show); void setGameNameFilter(QString gameName); void setCreatorNameFilter(QString creatorName); void setMinPlayers(int min); void setMaxPlayers(int max); + void setMaxGameAge(const QTime &maxGameAge); void setGameTypeEnabled(QString gametype, bool enabled); void setGameHashedTypeEnabled(QString gametypeHASHED, bool enabled); + void setShowOnlyIfSpectatorsCanWatch(bool show); + void setShowSpectatorPasswordProtected(bool show); + void setShowOnlyIfSpectatorsCanChat(bool show); + void setShowOnlyIfSpectatorsCanSeeHands(bool show); signals: public slots: diff --git a/cockatrice/src/settings/serverssettings.cpp b/cockatrice/src/settings/serverssettings.cpp index a4d831b32..8781cb34c 100644 --- a/cockatrice/src/settings/serverssettings.cpp +++ b/cockatrice/src/settings/serverssettings.cpp @@ -50,25 +50,21 @@ QString ServersSettings::getSite(QString defaultSite) QString ServersSettings::getPrevioushostName() { - return getValue("previoushostName", "server").toString(); + QVariant value = getValue("previoushostName", "server"); + return value == QVariant() ? "Rooster Ranges" : value.toString(); } int ServersSettings::getPrevioushostindex(const QString &saveName) { - int size = getValue("totalServers", "server", "server_details").toInt() + 1; + int size = getValue("totalServers", "server", "server_details").toInt(); - for (int i = 0; i < size; i++) + for (int i = 0; i <= size; ++i) if (saveName == getValue(QString("saveName%1").arg(i), "server", "server_details").toString()) return i; return -1; } -void ServersSettings::setHostName(QString hostname) -{ - setValue(hostname, "hostname", "server"); -} - QString ServersSettings::getHostname(QString defaultHost) { int index = getPrevioushostindex(getPrevioushostName()); @@ -76,11 +72,6 @@ QString ServersSettings::getHostname(QString defaultHost) return hostname == QVariant() ? std::move(defaultHost) : hostname.toString(); } -void ServersSettings::setPort(QString port) -{ - setValue(port, "port", "server"); -} - QString ServersSettings::getPort(QString defaultPort) { int index = getPrevioushostindex(getPrevioushostName()); @@ -89,11 +80,6 @@ QString ServersSettings::getPort(QString defaultPort) return port == QVariant() ? std::move(defaultPort) : port.toString(); } -void ServersSettings::setPlayerName(QString playerName) -{ - setValue(playerName, "playername", "server"); -} - QString ServersSettings::getPlayerName(QString defaultName) { int index = getPrevioushostindex(getPrevioushostName()); @@ -119,16 +105,6 @@ bool ServersSettings::getSavePassword() return save; } -void ServersSettings::setPassword(QString password) -{ - setValue(password, "password", "server"); -} - -void ServersSettings::setSavePassword(int save) -{ - setValue(save, "save_password", "server"); -} - void ServersSettings::setAutoConnect(int autoconnect) { setValue(autoconnect, "auto_connect", "server"); @@ -209,20 +185,48 @@ void ServersSettings::addNewServer(const QString &saveName, void ServersSettings::removeServer(QString servAddr) { - int size = getValue("totalServers", "server", "server_details").toInt() + 1; + int size = getValue("totalServers", "server", "server_details").toInt(); - for (int i = 0; i < size; i++) { - if (servAddr == getValue(QString("server%1").arg(i), "server", "server_details").toString()) { - deleteValue(QString("server%1").arg(i), "server", "server_details"); - deleteValue(QString("port%1").arg(i), "server", "server_details"); - deleteValue(QString("username%1").arg(i), "server", "server_details"); - deleteValue(QString("savePassword%1").arg(i), "server", "server_details"); - deleteValue(QString("password%1").arg(i), "server", "server_details"); - deleteValue(QString("saveName%1").arg(i), "server", "server_details"); - deleteValue(QString("site%1").arg(i), "server", "server_details"); - return; + bool found = false; + for (int i = 0; i <= size; ++i) { + if (!found) { + // find entry and overwrite it + if (servAddr == getValue(QString("server%1").arg(i), "server", "server_details").toString()) { + found = true; + } + } else { + // move all other entries after it one back, overwriting the previous one + int previous = i - 1; // we delete only one entry + setValue(getValue(QString("server%1").arg(i), "server", "server_details"), + QString("server%1").arg(previous), "server", "server_details"); + setValue(getValue(QString("port%1").arg(i), "server", "server_details"), QString("port%1").arg(previous), + "server", "server_details"); + setValue(getValue(QString("username%1").arg(i), "server", "server_details"), + QString("username%1").arg(previous), "server", "server_details"); + setValue(getValue(QString("savePassword%1").arg(i), "server", "server_details"), + QString("savePassword%1").arg(previous), "server", "server_details"); + setValue(getValue(QString("password%1").arg(i), "server", "server_details"), + QString("password%1").arg(previous), "server", "server_details"); + setValue(getValue(QString("saveName%1").arg(i), "server", "server_details"), + QString("saveName%1").arg(previous), "server", "server_details"); + setValue(getValue(QString("site%1").arg(i), "server", "server_details"), QString("site%1").arg(previous), + "server", "server_details"); } } + + // if we have deleted an entry, adjust the total + if (found) { + setValue(size - 1, "totalServers", "server", "server_details"); + + // delete last value + deleteValue(QString("server%1").arg(size), "server", "server_details"); + deleteValue(QString("port%1").arg(size), "server", "server_details"); + deleteValue(QString("username%1").arg(size), "server", "server_details"); + deleteValue(QString("savePassword%1").arg(size), "server", "server_details"); + deleteValue(QString("password%1").arg(size), "server", "server_details"); + deleteValue(QString("saveName%1").arg(size), "server", "server_details"); + deleteValue(QString("site%1").arg(size), "server", "server_details"); + } } /** @@ -230,9 +234,9 @@ void ServersSettings::removeServer(QString servAddr) */ bool ServersSettings::updateExistingServerWithoutLoss(QString saveName, QString serv, QString port, QString site) { - int size = getValue("totalServers", "server", "server_details").toInt() + 1; + int size = getValue("totalServers", "server", "server_details").toInt(); - for (int i = 0; i < size; i++) { + for (int i = 0; i <= size; ++i) { if (serv == getValue(QString("server%1").arg(i), "server", "server_details").toString()) { if (!port.isEmpty()) { setValue(port, QString("port%1").arg(i), "server", "server_details"); @@ -258,9 +262,9 @@ bool ServersSettings::updateExistingServer(QString saveName, bool savePassword, QString site) { - int size = getValue("totalServers", "server", "server_details").toInt() + 1; + int size = getValue("totalServers", "server", "server_details").toInt(); - for (int i = 0; i < size; i++) { + for (int i = 0; i <= size; ++i) { if (serv == getValue(QString("server%1").arg(i), "server", "server_details").toString()) { setValue(port, QString("port%1").arg(i), "server", "server_details"); if (!username.isEmpty()) { diff --git a/cockatrice/src/settings/serverssettings.h b/cockatrice/src/settings/serverssettings.h index 3195c26a8..72e6ce07e 100644 --- a/cockatrice/src/settings/serverssettings.h +++ b/cockatrice/src/settings/serverssettings.h @@ -4,6 +4,8 @@ #include "settingsmanager.h" #include +#define SERVERSETTINGS_DEFAULT_HOST "server.cockatrice.us" +#define SERVERSETTINGS_DEFAULT_PORT "4748" class ServersSettings : public SettingsManager { @@ -15,11 +17,11 @@ public: int getPrevioushostindex(const QString &); QStringList getPreviousHostList(); QString getPrevioushostName(); - QString getHostname(QString defaultHost = ""); - QString getPort(QString defaultPort = ""); + QString getHostname(QString defaultHost = SERVERSETTINGS_DEFAULT_HOST); + QString getPort(QString defaultPort = SERVERSETTINGS_DEFAULT_PORT); QString getPlayerName(QString defaultName = ""); - QString getFPHostname(QString defaultHost = ""); - QString getFPPort(QString defaultPort = ""); + QString getFPHostname(QString defaultHost = SERVERSETTINGS_DEFAULT_HOST); + QString getFPPort(QString defaultPort = SERVERSETTINGS_DEFAULT_PORT); QString getFPPlayerName(QString defaultName = ""); QString getPassword(); QString getSaveName(QString defaultname = ""); @@ -30,15 +32,10 @@ public: void setPreviousHostLogin(int previous); void setPrevioushostName(const QString &); void setPreviousHostList(QStringList list); - void setHostName(QString hostname); - void setPort(QString port); - void setPlayerName(QString playerName); void setAutoConnect(int autoconnect); void setSite(QString site); void setFPHostName(QString hostname); - void setPassword(QString password); void setFPPort(QString port); - void setSavePassword(int save); void setFPPlayerName(QString playerName); void addNewServer(const QString &saveName, const QString &serv, diff --git a/cockatrice/src/settingscache.cpp b/cockatrice/src/settingscache.cpp index 96178da98..045dce37c 100644 --- a/cockatrice/src/settingscache.cpp +++ b/cockatrice/src/settingscache.cpp @@ -60,11 +60,6 @@ void SettingsCache::translateLegacySettings() legacySetting.beginGroup("server"); servers().setPreviousHostLogin(legacySetting.value("previoushostlogin").toInt()); servers().setPreviousHostList(legacySetting.value("previoushosts").toStringList()); - servers().setHostName(legacySetting.value("hostname").toString()); - servers().setPort(legacySetting.value("port").toString()); - servers().setPlayerName(legacySetting.value("playername").toString()); - servers().setPassword(legacySetting.value("password").toString()); - servers().setSavePassword(legacySetting.value("save_password").toInt()); servers().setAutoConnect(legacySetting.value("auto_connect").toInt()); servers().setFPHostName(legacySetting.value("fphostname").toString()); servers().setFPPort(legacySetting.value("fpport").toString()); @@ -95,7 +90,8 @@ void SettingsCache::translateLegacySettings() // Game filters legacySetting.beginGroup("filter_games"); - gameFilters().setUnavailableGamesVisible(legacySetting.value("unavailable_games_visible").toBool()); + gameFilters().setShowFullGames(legacySetting.value("unavailable_games_visible").toBool()); + gameFilters().setShowGamesThatStarted(legacySetting.value("unavailable_games_visible").toBool()); gameFilters().setShowPasswordProtectedGames(legacySetting.value("show_password_protected_games").toBool()); gameFilters().setGameNameFilter(legacySetting.value("game_name_filter").toString()); gameFilters().setShowBuddiesOnlyGames(legacySetting.value("show_buddies_only_games").toBool()); @@ -196,6 +192,7 @@ SettingsCache::SettingsCache() deckPath = getSafeConfigPath("paths/decks", dataPath + "/decks/"); replaysPath = getSafeConfigPath("paths/replays", dataPath + "/replays/"); + themesPath = getSafeConfigPath("paths/themes", dataPath + "/themes/"); picsPath = getSafeConfigPath("paths/pics", dataPath + "/pics/"); // this has never been exposed as an user-configurable setting if (picsPath.endsWith("/")) { @@ -283,6 +280,7 @@ SettingsCache::SettingsCache() spectatorsNeedPassword = settings->value("game/spectatorsneedpassword", false).toBool(); spectatorsCanTalk = settings->value("game/spectatorscantalk", false).toBool(); spectatorsCanSeeEverything = settings->value("game/spectatorscanseeeverything", false).toBool(); + createGameAsSpectator = settings->value("game/creategameasspectator", false).toBool(); rememberGameSettings = settings->value("game/remembergamesettings", true).toBool(); clientID = settings->value("personal/clientid", CLIENT_INFO_NOT_SET).toString(); clientVersion = settings->value("personal/clientversion", CLIENT_INFO_NOT_SET).toString(); @@ -386,6 +384,13 @@ void SettingsCache::setReplaysPath(const QString &_replaysPath) settings->setValue("paths/replays", replaysPath); } +void SettingsCache::setThemesPath(const QString &_themesPath) +{ + themesPath = _themesPath; + settings->setValue("paths/themes", themesPath); + emit themeChanged(); +} + void SettingsCache::setCustomCardDatabasePath(const QString &_customCardDatabasePath) { customCardDatabasePath = _customCardDatabasePath; @@ -939,6 +944,12 @@ void SettingsCache::setSpectatorsCanSeeEverything(const bool _spectatorsCanSeeEv settings->setValue("game/spectatorscanseeeverything", spectatorsCanSeeEverything); } +void SettingsCache::setCreateGameAsSpectator(const bool _createGameAsSpectator) +{ + createGameAsSpectator = _createGameAsSpectator; + settings->setValue("game/creategameasspectator", createGameAsSpectator); +} + void SettingsCache::setRememberGameSettings(const bool _rememberGameSettings) { rememberGameSettings = _rememberGameSettings; diff --git a/cockatrice/src/settingscache.h b/cockatrice/src/settingscache.h index cbb8c36bd..e9478d76f 100644 --- a/cockatrice/src/settingscache.h +++ b/cockatrice/src/settingscache.h @@ -66,7 +66,7 @@ private: QByteArray mainWindowGeometry; QByteArray tokenDialogGeometry; QString lang; - QString deckPath, replaysPath, picsPath, customPicsPath, cardDatabasePath, customCardDatabasePath, + QString deckPath, replaysPath, picsPath, customPicsPath, cardDatabasePath, customCardDatabasePath, themesPath, spoilerDatabasePath, tokenDatabasePath, themeName; bool notifyAboutUpdates; bool notifyAboutNewVersion; @@ -124,6 +124,7 @@ private: bool spectatorsNeedPassword; bool spectatorsCanTalk; bool spectatorsCanSeeEverything; + bool createGameAsSpectator; int keepalive; void translateLegacySettings(); QString getSafeConfigPath(QString configEntry, QString defaultPath) const; @@ -156,6 +157,10 @@ public: { return replaysPath; } + QString getThemesPath() const + { + return themesPath; + } QString getPicsPath() const { return picsPath; @@ -394,6 +399,10 @@ public: { return spectatorsCanSeeEverything; } + bool getCreateGameAsSpectator() const + { + return createGameAsSpectator; + } bool getRememberGameSettings() const { return rememberGameSettings; @@ -474,6 +483,7 @@ public slots: void setSeenTips(const QList &_seenTips); void setDeckPath(const QString &_deckPath); void setReplaysPath(const QString &_replaysPath); + void setThemesPath(const QString &_themesPath); void setCustomCardDatabasePath(const QString &_customCardDatabasePath); void setPicsPath(const QString &_picsPath); void setCardDatabasePath(const QString &_cardDatabasePath); @@ -525,6 +535,7 @@ public slots: void setSpectatorsNeedPassword(const bool _spectatorsNeedPassword); void setSpectatorsCanTalk(const bool _spectatorsCanTalk); void setSpectatorsCanSeeEverything(const bool _spectatorsCanSeeEverything); + void setCreateGameAsSpectator(const bool _createGameAsSpectator); void setRememberGameSettings(const bool _rememberGameSettings); void setNotifyAboutUpdate(int _notifyaboutupdate); void setNotifyAboutNewVersion(int _notifyaboutnewversion); diff --git a/cockatrice/src/shortcutssettings.h b/cockatrice/src/shortcutssettings.h index 420854d0e..b76623947 100644 --- a/cockatrice/src/shortcutssettings.h +++ b/cockatrice/src/shortcutssettings.h @@ -219,7 +219,7 @@ private: parseSequenceString("Ctrl+S"), ShortcutGroup::Deck_Editor)}, {"TabDeckEditor/aSaveDeckAs", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Save Deck as..."), - parseSequenceString(""), + parseSequenceString("Ctrl+Shift+S"), ShortcutGroup::Deck_Editor)}, {"TabDeckEditor/aSaveDeckToClipboard", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Save Deck to Clipboard, Annotated"), @@ -502,9 +502,31 @@ private: {"Player/aMoveTopCardToBottom", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Bottom of Library"), parseSequenceString(""), ShortcutGroup::Move_top)}, + {"Player/aMoveBottomToPlay", + ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Stack"), parseSequenceString(""), ShortcutGroup::Move_bottom)}, + {"Player/aMoveBottomToPlayFaceDown", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Battlefield, Face Down"), + parseSequenceString(""), + ShortcutGroup::Move_bottom)}, {"Player/aMoveBottomCardToGrave", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Graveyard"), parseSequenceString(""), ShortcutGroup::Move_bottom)}, + {"Player/aMoveBottomCardsToGrave", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Graveyard (Multiple)"), + parseSequenceString(""), + ShortcutGroup::Move_bottom)}, + {"Player/aMoveBottomCardToExile", + ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Exile"), parseSequenceString(""), ShortcutGroup::Move_bottom)}, + {"Player/aMoveBottomCardsToExile", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Exile (Multiple)"), + parseSequenceString(""), + ShortcutGroup::Move_bottom)}, + {"Player/aMoveBottomCardToTop", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Top of Library"), + parseSequenceString(""), + ShortcutGroup::Move_bottom)}, + {"Player/aDrawBottomCard", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Draw Bottom Card"), + parseSequenceString(""), + ShortcutGroup::Move_bottom)}, + {"Player/aDrawBottomCards", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Draw Multiple Cards from Bottom..."), + parseSequenceString(""), + ShortcutGroup::Move_bottom)}, {"Player/aDrawArrow", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Draw Arrow..."), parseSequenceString(""), ShortcutGroup::Gameplay)}, @@ -537,6 +559,9 @@ private: {"Player/aAlwaysRevealTopCard", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Always Reveal Top Card"), parseSequenceString("Ctrl+N"), ShortcutGroup::Drawing)}, + {"Player/aAlwaysLookAtTopCard", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Always Look At Top Card"), + parseSequenceString("Ctrl+A"), + ShortcutGroup::Drawing)}, {"Player/aRotateViewCW", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Rotate View Clockwise"), parseSequenceString(""), ShortcutGroup::Gameplay)}, diff --git a/cockatrice/src/spoilerbackgroundupdater.cpp b/cockatrice/src/spoilerbackgroundupdater.cpp index 22d6e970c..7521b9ee9 100644 --- a/cockatrice/src/spoilerbackgroundupdater.cpp +++ b/cockatrice/src/spoilerbackgroundupdater.cpp @@ -167,8 +167,8 @@ bool SpoilerBackgroundUpdater::saveDownloadedFile(QByteArray data) QList lines = data.split('\n'); foreach (QByteArray line, lines) { - if (line.contains("created:")) { - QString timeStamp = QString(line).replace("created:", "").trimmed(); + if (line.contains("Created At:")) { + QString timeStamp = QString(line).replace("Created At:", "").trimmed(); timeStamp.chop(6); // Remove " (UTC)" auto utcTime = QLocale().toDateTime(timeStamp, "ddd, MMM dd yyyy, hh:mm:ss"); diff --git a/cockatrice/src/stackzone.cpp b/cockatrice/src/stackzone.cpp index f2be30734..26f19d557 100644 --- a/cockatrice/src/stackzone.cpp +++ b/cockatrice/src/stackzone.cpp @@ -47,7 +47,13 @@ QRectF StackZone::boundingRect() const void StackZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) { - painter->fillRect(boundingRect(), themeManager->getStackBgBrush()); + QBrush brush = themeManager->getStackBgBrush(); + + if (player->getZoneId() > 0) { + // If the extra image is not found, load the default one + brush = themeManager->getExtraStackBgBrush(QString::number(player->getZoneId()), brush); + } + painter->fillRect(boundingRect(), brush); } void StackZone::handleDropEvent(const QList &dragItems, diff --git a/cockatrice/src/tab_userlists.cpp b/cockatrice/src/tab_account.cpp similarity index 99% rename from cockatrice/src/tab_userlists.cpp rename to cockatrice/src/tab_account.cpp index ad0ec8dc8..4da8dc4bb 100644 --- a/cockatrice/src/tab_userlists.cpp +++ b/cockatrice/src/tab_account.cpp @@ -1,4 +1,4 @@ -#include "tab_userlists.h" +#include "tab_account.h" #include "abstractclient.h" #include "customlineedit.h" diff --git a/cockatrice/src/tab_userlists.h b/cockatrice/src/tab_account.h similarity index 97% rename from cockatrice/src/tab_userlists.h rename to cockatrice/src/tab_account.h index caa9166f2..ba6eb78ad 100644 --- a/cockatrice/src/tab_userlists.h +++ b/cockatrice/src/tab_account.h @@ -1,5 +1,5 @@ -#ifndef TAB_USERLISTS_H -#define TAB_USERLISTS_H +#ifndef TAB_ACCOUNT_H +#define TAB_ACCOUNT_H #include "pb/serverinfo_user.pb.h" #include "tab.h" diff --git a/cockatrice/src/tab_deck_editor.cpp b/cockatrice/src/tab_deck_editor.cpp index aab5f2da7..15e26e124 100644 --- a/cockatrice/src/tab_deck_editor.cpp +++ b/cockatrice/src/tab_deck_editor.cpp @@ -26,8 +26,6 @@ #include #include #include -#include -#include #include #include #include @@ -89,6 +87,8 @@ void TabDeckEditor::createDeckDock() commentsLabel = new QLabel(); commentsLabel->setObjectName("commentsLabel"); commentsEdit = new QTextEdit; + commentsEdit->setAcceptRichText(false); + commentsEdit->setMinimumHeight(nameEdit->minimumSizeHint().height()); commentsEdit->setObjectName("commentsEdit"); commentsLabel->setBuddy(commentsEdit); connect(commentsEdit, SIGNAL(textChanged()), this, SLOT(updateComments())); @@ -139,15 +139,15 @@ void TabDeckEditor::createDeckDock() lowerLayout->addWidget(deckView, 1, 0, 1, 5); // Create widgets for both layouts to make splitter work correctly - QWidget *topWidget = new QWidget; + auto *topWidget = new QWidget; topWidget->setLayout(upperLayout); - QWidget *bottomWidget = new QWidget; + auto *bottomWidget = new QWidget; bottomWidget->setLayout(lowerLayout); auto *split = new QSplitter; split->setObjectName("deckSplitter"); split->setOrientation(Qt::Vertical); - split->setChildrenCollapsible(false); + split->setChildrenCollapsible(true); split->addWidget(topWidget); split->addWidget(bottomWidget); split->setStretchFactor(0, 1); @@ -164,7 +164,7 @@ void TabDeckEditor::createDeckDock() deckDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); deckDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetMovable); - QWidget *deckDockContents = new QWidget(); + auto *deckDockContents = new QWidget(); deckDockContents->setObjectName("deckDockContents"); deckDockContents->setLayout(rightFrame); deckDock->setWidget(deckDockContents); @@ -188,7 +188,7 @@ void TabDeckEditor::createCardInfoDock() cardInfoDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); cardInfoDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetMovable); - QWidget *cardInfoDockContents = new QWidget(); + auto *cardInfoDockContents = new QWidget(); cardInfoDockContents->setObjectName("cardInfoDockContents"); cardInfoDockContents->setLayout(cardInfoFrame); cardInfoDock->setWidget(cardInfoDockContents); @@ -250,7 +250,7 @@ void TabDeckEditor::createFiltersDock() filterDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetMovable); - QWidget *filterDockContents = new QWidget(this); + auto *filterDockContents = new QWidget(this); filterDockContents->setObjectName("filterDockContents"); filterDockContents->setLayout(filterFrame); filterDock->setWidget(filterDockContents); @@ -476,7 +476,7 @@ void TabDeckEditor::databaseCustomMenu(QPoint point) relatedMenu->setDisabled(true); } else { for (const CardRelation *rel : relatedCards) { - QString relatedCardName = rel->getName(); + const QString &relatedCardName = rel->getName(); QAction *relatedCard = relatedMenu->addAction(relatedCardName); connect(relatedCard, &QAction::triggered, cardInfo, [this, relatedCardName] { cardInfo->setCard(relatedCardName); }); @@ -875,7 +875,7 @@ void TabDeckEditor::actSaveDeckToClipboardRaw() void TabDeckEditor::actPrintDeck() { - QPrintPreviewDialog *dlg = new QPrintPreviewDialog(this); + auto *dlg = new QPrintPreviewDialog(this); connect(dlg, SIGNAL(paintRequested(QPrinter *)), deckModel, SLOT(printDeckList(QPrinter *))); dlg->exec(); } @@ -933,8 +933,9 @@ void TabDeckEditor::actClearFilterAll() void TabDeckEditor::actClearFilterOne() { QModelIndexList selIndexes = filterView->selectionModel()->selectedIndexes(); - foreach (QModelIndex idx, selIndexes) + for (QModelIndex idx : selIndexes) { filterModel->removeRow(idx.row(), idx.parent()); + } } void TabDeckEditor::recursiveExpand(const QModelIndex &index) @@ -1249,7 +1250,7 @@ void TabDeckEditor::showSearchSyntaxHelp() .replace(QRegularExpression("^(##)(.*)", opts), "

\\2

") .replace(QRegularExpression("^(#)(.*)", opts), "

\\2

") .replace(QRegularExpression("^------*", opts), "
") - .replace(QRegularExpression("\\[([^\[]+)\\]\\(([^\\)]+)\\)", opts), "\\1"); + .replace(QRegularExpression(R"(\[([^[]+)\]\(([^\)]+)\))", opts), R"(\1)"); auto browser = new QTextBrowser; browser->setParent(this, Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowMinMaxButtonsHint | @@ -1262,6 +1263,6 @@ void TabDeckEditor::showSearchSyntaxHelp() browser->document()->setDefaultStyleSheet(sheet); browser->setHtml(text); - connect(browser, &QTextBrowser::anchorClicked, [=](QUrl link) { searchEdit->setText(link.fragment()); }); + connect(browser, &QTextBrowser::anchorClicked, [=](const QUrl &link) { searchEdit->setText(link.fragment()); }); browser->show(); } diff --git a/cockatrice/src/tab_deck_storage.cpp b/cockatrice/src/tab_deck_storage.cpp index db1716ad2..096faec26 100644 --- a/cockatrice/src/tab_deck_storage.cpp +++ b/cockatrice/src/tab_deck_storage.cpp @@ -196,6 +196,9 @@ void TabDeckStorage::uploadFinished(const Response &r, const CommandContainer &c void TabDeckStorage::actDeleteLocalDeck() { QModelIndex curLeft = localDirView->selectionModel()->currentIndex(); + if (localDirModel->isDir(curLeft)) + return; + if (QMessageBox::warning(this, tr("Delete local file"), tr("Are you sure you want to delete \"%1\"?").arg(localDirModel->fileName(curLeft)), QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) diff --git a/cockatrice/src/tab_game.cpp b/cockatrice/src/tab_game.cpp index 424b9e3cc..9cd7a9286 100644 --- a/cockatrice/src/tab_game.cpp +++ b/cockatrice/src/tab_game.cpp @@ -9,6 +9,7 @@ #include "deckview.h" #include "dlg_creategame.h" #include "dlg_load_remote_deck.h" +#include "dlg_manage_sets.h" #include "gamescene.h" #include "gameview.h" #include "get_pb_extension.h" @@ -52,7 +53,7 @@ #include "replay_timeline_widget.h" #include "settingscache.h" #include "tab_supervisor.h" -#include "window_sets.h" +#include "window_main.h" #include "zoneviewwidget.h" #include "zoneviewzone.h" @@ -162,6 +163,9 @@ void DeckViewContainer::updateSideboardLockButtonText() } else { sideboardLockButton->setText(tr("Sideboard locked")); } + // setting text on a button removes its shortcut + ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts(); + sideboardLockButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/sideboardLockButton")); } void DeckViewContainer::refreshShortcuts() @@ -855,10 +859,8 @@ void TabGame::processGameEventContainer(const GameEventContainer &cont, Abstract case GameEvent::LEAVE: eventSpectatorLeave(event.GetExtension(Event_Leave::ext), playerId, context); break; - default: { - qDebug() << "unhandled spectator game event" << eventType; + default: break; - } } } else { if ((clients.size() > 1) && (playerId != -1)) @@ -1194,6 +1196,11 @@ void TabGame::eventJoin(const Event_Join &event, int /*eventPlayerId*/, const Ga } else { Player *newPlayer = addPlayer(playerId, playerInfo.user_info()); messageLog->logJoin(newPlayer); + if (trayIcon) { + QString gameId(QString::number(gameInfo.game_id())); + trayIcon->showMessage(tr("A player has joined game #%1").arg(gameId), + tr("%1 has joined the game").arg(newPlayer->getName())); + } } playerListWidget->addPlayer(playerInfo); emitUserEvent(); @@ -1408,12 +1415,15 @@ Player *TabGame::getActiveLocalPlayer() const void TabGame::updateCardMenu(AbstractCardItem *card) { - Player *p; - if ((clients.size() > 1) || !players.contains(localPlayerId)) - p = card->getOwner(); - else - p = players.value(localPlayerId); - p->updateCardMenu(static_cast(card)); + Player *player; + if ((clients.size() > 1) || !players.contains(localPlayerId)) { + player = card->getOwner(); + } else { + player = players.value(localPlayerId); + } + if (player != nullptr) { + player->updateCardMenu(static_cast(card)); + } } void TabGame::createMenuItems() diff --git a/cockatrice/src/tab_logs.cpp b/cockatrice/src/tab_logs.cpp index 2960f91d3..5c318cf95 100644 --- a/cockatrice/src/tab_logs.cpp +++ b/cockatrice/src/tab_logs.cpp @@ -2,10 +2,10 @@ #include "abstractclient.h" #include "customlineedit.h" +#include "dlg_manage_sets.h" #include "pb/moderator_commands.pb.h" #include "pb/response_viewlog_history.pb.h" #include "pending_command.h" -#include "window_sets.h" #include #include diff --git a/cockatrice/src/tab_room.cpp b/cockatrice/src/tab_room.cpp index 9cffdda39..fdeee3976 100644 --- a/cockatrice/src/tab_room.cpp +++ b/cockatrice/src/tab_room.cpp @@ -14,8 +14,8 @@ #include "pb/serverinfo_room.pb.h" #include "pending_command.h" #include "settingscache.h" +#include "tab_account.h" #include "tab_supervisor.h" -#include "tab_userlists.h" #include "userlist.h" #include diff --git a/cockatrice/src/tab_server.cpp b/cockatrice/src/tab_server.cpp index aae93506f..03919f364 100644 --- a/cockatrice/src/tab_server.cpp +++ b/cockatrice/src/tab_server.cpp @@ -112,7 +112,7 @@ void RoomSelector::processListRoomsEvent(const Event_ListRooms &event) QString RoomSelector::getRoomPermissionDisplay(const ServerInfo_Room &room) { /* - * A room can have a permission level and a privilege level. How ever we want to display only the necessary + * A server room can have a permission level and a privilege level. How ever we want to display only the necessary * information on the server tab needed to inform users of required permissions to enter a room. If the room has a * privilege level the server tab will display the privilege level in the "permissions" column in the row however if * the room contains a permissions level for the room the permissions level defined for the room will be displayed. @@ -206,19 +206,23 @@ void TabServer::joinRoomFinished(const Response &r, case Response::RespOk: break; case Response::RespNameNotFound: - QMessageBox::critical(this, tr("Error"), tr("Failed to join the room: it doesn't exist on the server.")); + QMessageBox::critical(this, tr("Error"), + tr("Failed to join the server room: it doesn't exist on the server.")); return; case Response::RespContextError: - QMessageBox::critical(this, tr("Error"), - tr("The server thinks you are in the room but your client is unable to display it. " - "Try restarting your client.")); + QMessageBox::critical( + this, tr("Error"), + tr("The server thinks you are in the server room but your client is unable to display it. " + "Try restarting your client.")); return; case Response::RespUserLevelTooLow: - QMessageBox::critical(this, tr("Error"), tr("You do not have the required permission to join this room.")); + QMessageBox::critical(this, tr("Error"), + tr("You do not have the required permission to join this server room.")); return; default: - QMessageBox::critical(this, tr("Error"), - tr("Failed to join the room due to an unknown error: %1.").arg(r.response_code())); + QMessageBox::critical( + this, tr("Error"), + tr("Failed to join the server room due to an unknown error: %1.").arg(r.response_code())); return; } diff --git a/cockatrice/src/tab_supervisor.cpp b/cockatrice/src/tab_supervisor.cpp index d6f07873f..f5a199cc6 100644 --- a/cockatrice/src/tab_supervisor.cpp +++ b/cockatrice/src/tab_supervisor.cpp @@ -13,6 +13,7 @@ #include "pb/serverinfo_user.pb.h" #include "pixmapgenerator.h" #include "settingscache.h" +#include "tab_account.h" #include "tab_admin.h" #include "tab_deck_editor.h" #include "tab_deck_storage.h" @@ -22,7 +23,6 @@ #include "tab_replays.h" #include "tab_room.h" #include "tab_server.h" -#include "tab_userlists.h" #include "userlist.h" #include diff --git a/cockatrice/src/thememanager.cpp b/cockatrice/src/thememanager.cpp index 8b928d2a5..e51b03141 100644 --- a/cockatrice/src/thememanager.cpp +++ b/cockatrice/src/thememanager.cpp @@ -9,12 +9,17 @@ #include #include -#define DEFAULT_THEME_NAME "Default" +#define NONE_THEME_NAME "None " #define STYLE_CSS_NAME "style.css" #define HANDZONE_BG_NAME "handzone" #define PLAYERZONE_BG_NAME "playerzone" #define STACKZONE_BG_NAME "stackzone" #define TABLEZONE_BG_NAME "tablezone" +static const QColor HANDZONE_BG_DEFAULT = QColor(80, 100, 50); +static const QColor TABLEZONE_BG_DEFAULT = QColor(70, 50, 100); +static const QColor PLAYERZONE_BG_DEFAULT = QColor(200, 200, 200); +static const QColor STACKZONE_BG_DEFAULT = QColor(113, 43, 43); +static const QStringList DEFAULT_RESOURCE_PATHS = {":/resources"}; ThemeManager::ThemeManager(QObject *parent) : QObject(parent) { @@ -28,7 +33,7 @@ void ThemeManager::ensureThemeDirectoryExists() if (SettingsCache::instance().getThemeName().isEmpty() || !getAvailableThemes().contains(SettingsCache::instance().getThemeName())) { qDebug() << "Theme name not set, setting default value"; - SettingsCache::instance().setThemeName(DEFAULT_THEME_NAME); + SettingsCache::instance().setThemeName(NONE_THEME_NAME); } } @@ -37,12 +42,16 @@ QStringMap &ThemeManager::getAvailableThemes() QDir dir; availableThemes.clear(); - // load themes from user profile dir - dir.setPath(SettingsCache::instance().getDataPath() + "/themes"); + // add default value + availableThemes.insert(NONE_THEME_NAME, QString()); - foreach (QString themeName, dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Name)) { - if (!availableThemes.contains(themeName)) + // load themes from user profile dir + dir.setPath(SettingsCache::instance().getThemesPath()); + + for (QString themeName : dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Name)) { + if (!availableThemes.contains(themeName)) { availableThemes.insert(themeName, dir.absoluteFilePath(themeName)); + } } // load themes from cockatrice system dir @@ -56,9 +65,10 @@ QStringMap &ThemeManager::getAvailableThemes() #endif ); - foreach (QString themeName, dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Name)) { - if (!availableThemes.contains(themeName)) + for (QString themeName : dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Name)) { + if (!availableThemes.contains(themeName)) { availableThemes.insert(themeName, dir.absoluteFilePath(themeName)); + } } return availableThemes; @@ -97,27 +107,40 @@ void ThemeManager::themeChangedSlot() QString themeName = SettingsCache::instance().getThemeName(); qDebug() << "Theme changed:" << themeName; - QDir dir = getAvailableThemes().value(themeName); + QString dirPath = getAvailableThemes().value(themeName); + QDir dir = dirPath; // css - if (dir.exists(STYLE_CSS_NAME)) - + if (!dirPath.isEmpty() && dir.exists(STYLE_CSS_NAME)) { qApp->setStyleSheet("file:///" + dir.absoluteFilePath(STYLE_CSS_NAME)); - else + } else { qApp->setStyleSheet(""); + } - // resources - QStringList resources; - resources << dir.absolutePath() << ":/resources"; - QDir::setSearchPaths("theme", resources); + if (dirPath.isEmpty()) { + // set default values + QDir::setSearchPaths("theme", DEFAULT_RESOURCE_PATHS); + handBgBrush = HANDZONE_BG_DEFAULT; + tableBgBrush = TABLEZONE_BG_DEFAULT; + playerBgBrush = PLAYERZONE_BG_DEFAULT; + stackBgBrush = STACKZONE_BG_DEFAULT; + } else { + // resources + QStringList resources; + resources << dir.absolutePath() << DEFAULT_RESOURCE_PATHS; + QDir::setSearchPaths("theme", resources); - // zones bg - dir.cd("zones"); - handBgBrush = loadBrush(HANDZONE_BG_NAME, QColor(80, 100, 50)); - tableBgBrush = loadBrush(TABLEZONE_BG_NAME, QColor(70, 50, 100)); - playerBgBrush = loadBrush(PLAYERZONE_BG_NAME, QColor(200, 200, 200)); - stackBgBrush = loadBrush(STACKZONE_BG_NAME, QColor(113, 43, 43)); + // zones bg + dir.cd("zones"); + handBgBrush = loadBrush(HANDZONE_BG_NAME, HANDZONE_BG_DEFAULT); + tableBgBrush = loadBrush(TABLEZONE_BG_NAME, TABLEZONE_BG_DEFAULT); + playerBgBrush = loadBrush(PLAYERZONE_BG_NAME, PLAYERZONE_BG_DEFAULT); + stackBgBrush = loadBrush(STACKZONE_BG_NAME, STACKZONE_BG_DEFAULT); + } tableBgBrushesCache.clear(); + stackBgBrushesCache.clear(); + playerBgBrushesCache.clear(); + handBgBrushesCache.clear(); QPixmapCache::clear(); @@ -137,3 +160,45 @@ QBrush ThemeManager::getExtraTableBgBrush(QString extraNumber, QBrush &fallbackB return returnBrush; } + +QBrush ThemeManager::getExtraStackBgBrush(QString extraNumber, QBrush &fallbackBrush) +{ + QBrush returnBrush; + + if (!stackBgBrushesCache.contains(extraNumber.toInt())) { + returnBrush = loadExtraBrush(STACKZONE_BG_NAME + extraNumber, fallbackBrush); + stackBgBrushesCache.insert(extraNumber.toInt(), returnBrush); + } else { + returnBrush = stackBgBrushesCache.value(extraNumber.toInt()); + } + + return returnBrush; +} + +QBrush ThemeManager::getExtraPlayerBgBrush(QString extraNumber, QBrush &fallbackBrush) +{ + QBrush returnBrush; + + if (!playerBgBrushesCache.contains(extraNumber.toInt())) { + returnBrush = loadExtraBrush(PLAYERZONE_BG_NAME + extraNumber, fallbackBrush); + playerBgBrushesCache.insert(extraNumber.toInt(), returnBrush); + } else { + returnBrush = playerBgBrushesCache.value(extraNumber.toInt()); + } + + return returnBrush; +} + +QBrush ThemeManager::getExtraHandBgBrush(QString extraNumber, QBrush &fallbackBrush) +{ + QBrush returnBrush; + + if (!handBgBrushesCache.contains(extraNumber.toInt())) { + returnBrush = loadExtraBrush(HANDZONE_BG_NAME + extraNumber, fallbackBrush); + handBgBrushesCache.insert(extraNumber.toInt(), returnBrush); + } else { + returnBrush = handBgBrushesCache.value(extraNumber.toInt()); + } + + return returnBrush; +} diff --git a/cockatrice/src/thememanager.h b/cockatrice/src/thememanager.h index 23bad895c..3ab518b92 100644 --- a/cockatrice/src/thememanager.h +++ b/cockatrice/src/thememanager.h @@ -23,9 +23,9 @@ private: QBrush handBgBrush, stackBgBrush, tableBgBrush, playerBgBrush; QStringMap availableThemes; /* - Internal cache for table backgrounds + Internal cache for multiple backgrounds */ - QBrushMap tableBgBrushesCache; + QBrushMap tableBgBrushesCache, stackBgBrushesCache, playerBgBrushesCache, handBgBrushesCache; protected: void ensureThemeDirectoryExists(); @@ -51,6 +51,9 @@ public: } QStringMap &getAvailableThemes(); QBrush getExtraTableBgBrush(QString extraNumber, QBrush &fallbackBrush); + QBrush getExtraStackBgBrush(QString extraNumber, QBrush &fallbackBrush); + QBrush getExtraPlayerBgBrush(QString extraNumber, QBrush &fallbackBrush); + QBrush getExtraHandBgBrush(QString extraNumber, QBrush &fallbackBrush); protected slots: void themeChangedSlot(); signals: diff --git a/cockatrice/src/user_context_menu.cpp b/cockatrice/src/user_context_menu.cpp index 29d7d2eb1..761721112 100644 --- a/cockatrice/src/user_context_menu.cpp +++ b/cockatrice/src/user_context_menu.cpp @@ -12,9 +12,9 @@ #include "pb/response_warn_list.pb.h" #include "pb/session_commands.pb.h" #include "pending_command.h" +#include "tab_account.h" #include "tab_game.h" #include "tab_supervisor.h" -#include "tab_userlists.h" #include "userinfobox.h" #include "userlist.h" @@ -66,7 +66,7 @@ void UserContextMenu::retranslateUi() aBanHistory->setText(tr("View user's &ban history")); aPromoteToMod->setText(tr("&Promote user to moderator")); aDemoteFromMod->setText(tr("Dem&ote user from moderator")); - aPromoteToJudge->setText(tr("Promote user to &juge")); + aPromoteToJudge->setText(tr("Promote user to &judge")); aDemoteFromJudge->setText(tr("Demote user from judge")); } diff --git a/cockatrice/src/userconnection_information.h b/cockatrice/src/userconnection_information.h index 843f991a5..137060afe 100644 --- a/cockatrice/src/userconnection_information.h +++ b/cockatrice/src/userconnection_information.h @@ -17,7 +17,6 @@ private: QString password; bool savePassword; QString site; - bool isCustom; public: UserConnection_Information(); diff --git a/cockatrice/src/userinfobox.cpp b/cockatrice/src/userinfobox.cpp index 7a767475b..33b0ad61c 100644 --- a/cockatrice/src/userinfobox.cpp +++ b/cockatrice/src/userinfobox.cpp @@ -31,13 +31,13 @@ UserInfoBox::UserInfoBox(AbstractClient *_client, bool _editable, QWidget *paren avatarLabel.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); avatarLabel.setAlignment(Qt::AlignCenter); - QHBoxLayout *avatarLayout = new QHBoxLayout; + auto *avatarLayout = new QHBoxLayout; avatarLayout->setContentsMargins(0, 0, 0, 0); avatarLayout->addStretch(1); avatarLayout->addWidget(&avatarLabel, 3); avatarLayout->addStretch(1); - QGridLayout *mainLayout = new QGridLayout; + auto *mainLayout = new QGridLayout; mainLayout->addLayout(avatarLayout, 0, 0, 1, 3); mainLayout->addWidget(&nameLabel, 1, 0, 1, 3); mainLayout->addWidget(&realNameLabel1, 2, 0, 1, 1); @@ -53,7 +53,7 @@ UserInfoBox::UserInfoBox(AbstractClient *_client, bool _editable, QWidget *paren mainLayout->setColumnStretch(2, 10); if (editable) { - QHBoxLayout *buttonsLayout = new QHBoxLayout; + auto *buttonsLayout = new QHBoxLayout; buttonsLayout->addWidget(&editButton); buttonsLayout->addWidget(&passwordButton); buttonsLayout->addWidget(&avatarButton); @@ -85,10 +85,11 @@ void UserInfoBox::updateInfo(const ServerInfo_User &user) { const UserLevelFlags userLevel(user.user_level()); - const std::string bmp = user.avatar_bmp(); - if (!avatarPixmap.loadFromData((const uchar *)bmp.data(), bmp.size())) + const std::string &bmp = user.avatar_bmp(); + if (!avatarPixmap.loadFromData((const uchar *)bmp.data(), static_cast(bmp.size()))) { avatarPixmap = UserLevelPixmapGenerator::generatePixmap(64, userLevel, false, QString::fromStdString(user.privlevel())); + } avatarLabel.setPixmap(avatarPixmap.scaled(400, 200, Qt::KeepAspectRatio, Qt::SmoothTransformation)); nameLabel.setText(QString::fromStdString(user.name())); diff --git a/cockatrice/src/userlist.cpp b/cockatrice/src/userlist.cpp index 879027f98..fa39d69b6 100644 --- a/cockatrice/src/userlist.cpp +++ b/cockatrice/src/userlist.cpp @@ -8,8 +8,8 @@ #include "pb/session_commands.pb.h" #include "pending_command.h" #include "pixmapgenerator.h" +#include "tab_account.h" #include "tab_supervisor.h" -#include "tab_userlists.h" #include "user_context_menu.h" #include diff --git a/cockatrice/src/window_main.cpp b/cockatrice/src/window_main.cpp index 38140459a..4b6cd9d41 100644 --- a/cockatrice/src/window_main.cpp +++ b/cockatrice/src/window_main.cpp @@ -25,6 +25,7 @@ #include "dlg_forgotpasswordchallenge.h" #include "dlg_forgotpasswordrequest.h" #include "dlg_forgotpasswordreset.h" +#include "dlg_manage_sets.h" #include "dlg_register.h" #include "dlg_settings.h" #include "dlg_tip_of_the_day.h" @@ -44,7 +45,6 @@ #include "tab_game.h" #include "tab_supervisor.h" #include "version_string.h" -#include "window_sets.h" #include #include @@ -146,12 +146,14 @@ void MainWindow::statusChanged(ClientStatus _status) aConnect->setEnabled(true); aRegister->setEnabled(true); aDisconnect->setEnabled(false); + aForgotPassword->setEnabled(true); break; case StatusLoggingIn: aSinglePlayer->setEnabled(false); aConnect->setEnabled(false); aRegister->setEnabled(false); aDisconnect->setEnabled(true); + aForgotPassword->setEnabled(false); break; case StatusConnecting: case StatusRegistering: @@ -219,6 +221,7 @@ void MainWindow::actSinglePlayer() aConnect->setEnabled(false); aRegister->setEnabled(false); + aForgotPassword->setEnabled(false); aSinglePlayer->setEnabled(false); localServer = new LocalServer(this); @@ -269,6 +272,7 @@ void MainWindow::localGameEnded() aConnect->setEnabled(true); aRegister->setEnabled(true); + aForgotPassword->setEnabled(true); aSinglePlayer->setEnabled(true); } @@ -628,6 +632,7 @@ void MainWindow::retranslateUi() aDeckEditor->setText(tr("&Deck editor")); aFullScreen->setText(tr("&Full screen")); aRegister->setText(tr("&Register to server...")); + aForgotPassword->setText(tr("&Restore password...")); aSettings->setText(tr("&Settings...")); aSettings->setIcon(QPixmap("theme:icons/settings")); aExit->setText(tr("&Exit")); @@ -672,6 +677,8 @@ void MainWindow::createActions() connect(aFullScreen, SIGNAL(toggled(bool)), this, SLOT(actFullScreen(bool))); aRegister = new QAction(this); connect(aRegister, SIGNAL(triggered()), this, SLOT(actRegister())); + aForgotPassword = new QAction(this); + connect(aForgotPassword, SIGNAL(triggered()), this, SLOT(actForgotPasswordRequest())); aSettings = new QAction(this); connect(aSettings, SIGNAL(triggered()), this, SLOT(actSettings())); aExit = new QAction(this); @@ -739,6 +746,7 @@ void MainWindow::createMenus() cockatriceMenu->addAction(aConnect); cockatriceMenu->addAction(aDisconnect); cockatriceMenu->addAction(aRegister); + cockatriceMenu->addAction(aForgotPassword); cockatriceMenu->addSeparator(); cockatriceMenu->addAction(aSinglePlayer); cockatriceMenu->addAction(aWatchReplay); @@ -769,8 +777,8 @@ void MainWindow::createMenus() } MainWindow::MainWindow(QWidget *parent) - : QMainWindow(parent), localServer(nullptr), bHasActivated(false), cardUpdateProcess(nullptr), - logviewDialog(nullptr) + : QMainWindow(parent), localServer(nullptr), bHasActivated(false), askedForDbUpdater(false), + cardUpdateProcess(nullptr), logviewDialog(nullptr) { connect(&SettingsCache::instance(), SIGNAL(pixmapCacheSizeChanged(int)), this, SLOT(pixmapCacheSizeChanged(int))); pixmapCacheSizeChanged(SettingsCache::instance().getPixmapCacheSize()); @@ -1003,6 +1011,10 @@ void MainWindow::showWindowIfHidden() void MainWindow::cardDatabaseLoadingFailed() { + if (askedForDbUpdater) { + return; + } + askedForDbUpdater = true; QMessageBox msgBox; msgBox.setWindowTitle(tr("Card database")); msgBox.setIcon(QMessageBox::Question); @@ -1109,17 +1121,34 @@ void MainWindow::actCheckCardUpdates() if (dir.exists(binaryName)) { updaterCmd = dir.absoluteFilePath(binaryName); + } else { // try and find the directory oracle is stored in the build directory + QDir findLocalDir(dir); + findLocalDir.cdUp(); + findLocalDir.cd(getCardUpdaterBinaryName()); + if (findLocalDir.exists(binaryName)) { + dir = findLocalDir; + updaterCmd = dir.absoluteFilePath(binaryName); + } } if (updaterCmd.isEmpty()) { QMessageBox::warning(this, tr("Error"), tr("Unable to run the card database updater: ") + dir.absoluteFilePath(binaryName)); + exitCardDatabaseUpdate(); return; } cardUpdateProcess->start(updaterCmd, QStringList()); } +void MainWindow::exitCardDatabaseUpdate() +{ + cardUpdateProcess->deleteLater(); + cardUpdateProcess = nullptr; + + QtConcurrent::run(db, &CardDatabase::loadCardDatabases); +} + void MainWindow::cardUpdateError(QProcess::ProcessError err) { QString error; @@ -1145,18 +1174,13 @@ void MainWindow::cardUpdateError(QProcess::ProcessError err) break; } - cardUpdateProcess->deleteLater(); - cardUpdateProcess = nullptr; - + exitCardDatabaseUpdate(); QMessageBox::warning(this, tr("Error"), tr("The card database updater exited with an error: %1").arg(error)); } void MainWindow::cardUpdateFinished(int, QProcess::ExitStatus) { - cardUpdateProcess->deleteLater(); - cardUpdateProcess = nullptr; - - QtConcurrent::run(db, &CardDatabase::loadCardDatabases); + exitCardDatabaseUpdate(); } void MainWindow::actCheckServerUpdates() @@ -1297,7 +1321,7 @@ void MainWindow::actForgotPasswordRequest() void MainWindow::forgotPasswordSuccess() { QMessageBox::information( - this, tr("Forgot Password"), + this, tr("Reset Password"), tr("Your password has been reset successfully, you can now log in using the new credentials.")); SettingsCache::instance().servers().setFPHostName(""); SettingsCache::instance().servers().setFPPort(""); @@ -1307,7 +1331,7 @@ void MainWindow::forgotPasswordSuccess() void MainWindow::forgotPasswordError() { QMessageBox::warning( - this, tr("Forgot Password"), + this, tr("Reset Password"), tr("Failed to reset user account password, please contact the server operator to reset your password.")); SettingsCache::instance().servers().setFPHostName(""); SettingsCache::instance().servers().setFPPort(""); @@ -1316,7 +1340,7 @@ void MainWindow::forgotPasswordError() void MainWindow::promptForgotPasswordReset() { - QMessageBox::information(this, tr("Forgot Password"), + QMessageBox::information(this, tr("Reset Password"), tr("Activation request received, please check your email for an activation token.")); DlgForgotPasswordReset dlg(this); if (dlg.exec()) { diff --git a/cockatrice/src/window_main.h b/cockatrice/src/window_main.h index 47e10ad47..c17ef4989 100644 --- a/cockatrice/src/window_main.h +++ b/cockatrice/src/window_main.h @@ -116,23 +116,23 @@ private: void createTrayIcon(); void createTrayActions(); int getNextCustomSetPrefix(QDir dataDir); - // TODO: add a preference item to choose updater name for other games inline QString getCardUpdaterBinaryName() { return "oracle"; }; + void exitCardDatabaseUpdate(); QList tabMenus; QMenu *cockatriceMenu, *dbMenu, *helpMenu, *trayIconMenu; QAction *aConnect, *aDisconnect, *aSinglePlayer, *aWatchReplay, *aDeckEditor, *aFullScreen, *aSettings, *aExit, - *aAbout, *aTips, *aCheckCardUpdates, *aRegister, *aUpdate, *aViewLog, *closeAction; + *aAbout, *aTips, *aCheckCardUpdates, *aRegister, *aForgotPassword, *aUpdate, *aViewLog, *closeAction; QAction *aManageSets, *aEditTokens, *aOpenCustomFolder, *aOpenCustomsetsFolder, *aAddCustomSet; TabSupervisor *tabSupervisor; WndSets *wndSets; RemoteClient *client; QThread *clientThread; LocalServer *localServer; - bool bHasActivated; + bool bHasActivated, askedForDbUpdater; QMessageBox serverShutdownMessageBox; QProcess *cardUpdateProcess; DlgViewLog *logviewDialog; diff --git a/cockatrice/src/zoneviewwidget.cpp b/cockatrice/src/zoneviewwidget.cpp index ffda0eafd..87348cbab 100644 --- a/cockatrice/src/zoneviewwidget.cpp +++ b/cockatrice/src/zoneviewwidget.cpp @@ -3,7 +3,6 @@ #include "carditem.h" #include "gamescene.h" #include "pb/command_shuffle.pb.h" -#include "pb/command_stop_dump_zone.pb.h" #include "player.h" #include "settingscache.h" #include "zoneviewzone.h" @@ -200,12 +199,6 @@ void ZoneViewWidget::handleScrollBarChange(int value) void ZoneViewWidget::closeEvent(QCloseEvent *event) { disconnect(zone, SIGNAL(beingDeleted()), this, 0); - if (zone->getNumberCards() != -2) { - Command_StopDumpZone cmd; - cmd.set_player_id(player->getId()); - cmd.set_zone_name(zone->getName().toStdString()); - player->sendGameCommand(cmd); - } if (shuffleCheckBox.isChecked()) player->sendGameCommand(Command_Shuffle()); emit closePressed(this); diff --git a/cockatrice/src/zoneviewzone.cpp b/cockatrice/src/zoneviewzone.cpp index c71f9a098..840452317 100644 --- a/cockatrice/src/zoneviewzone.cpp +++ b/cockatrice/src/zoneviewzone.cpp @@ -26,16 +26,18 @@ ZoneViewZone::ZoneViewZone(Player *_p, numberCards(_numberCards), origZone(_origZone), revealZone(_revealZone), writeableRevealZone(_writeableRevealZone), sortByName(false), sortByType(false) { - if (!(revealZone && !writeableRevealZone)) - origZone->setView(this); + if (!(revealZone && !writeableRevealZone)) { + origZone->getViews().append(this); + } } ZoneViewZone::~ZoneViewZone() { emit beingDeleted(); qDebug("ZoneViewZone destructor"); - if (!(revealZone && !writeableRevealZone)) - origZone->setView(NULL); + if (!(revealZone && !writeableRevealZone)) { + origZone->getViews().removeOne(this); + } } QRectF ZoneViewZone::boundingRect() const @@ -81,15 +83,19 @@ void ZoneViewZone::initializeCards(const QList &cardLis void ZoneViewZone::zoneDumpReceived(const Response &r) { + static constexpr int MAX_VIEW_MENU = 300; const Response_DumpZone &resp = r.GetExtension(Response_DumpZone::ext); const int respCardListSize = resp.zone_info().card_list_size(); for (int i = 0; i < respCardListSize; ++i) { const ServerInfo_Card &cardInfo = resp.zone_info().card_list(i); - CardItem *card = new CardItem(player, QString::fromStdString(cardInfo.name()), cardInfo.id(), revealZone, this); - addCard(card, false, i); + auto cardName = QString::fromStdString(cardInfo.name()); + // stop updating card menus after MAX_VIEW_MENU cards + // this means only the first cards in the menu have actions but others can still be dragged + auto *card = new CardItem(player, cardName, cardInfo.id(), revealZone, this, this, i < MAX_VIEW_MENU); + cards.insert(i, card); } - reorganizeCards(); + emit cardCountChanged(); } // Because of boundingRect(), this function must not be called before the zone was added to a scene. @@ -238,11 +244,11 @@ QSizeF ZoneViewZone::sizeHint(Qt::SizeHint /*which*/, const QSizeF & /*constrain void ZoneViewZone::setWriteableRevealZone(bool _writeableRevealZone) { - if (writeableRevealZone && !_writeableRevealZone) - origZone->setView(this); - else if (!writeableRevealZone && _writeableRevealZone) - origZone->setView(NULL); - + if (writeableRevealZone && !_writeableRevealZone) { + origZone->getViews().append(this); + } else if (!writeableRevealZone && _writeableRevealZone) { + origZone->getViews().removeOne(this); + } writeableRevealZone = _writeableRevealZone; } diff --git a/cockatrice/themes/CMakeLists.txt b/cockatrice/themes/CMakeLists.txt index 2ec2ae87b..c417dfa2e 100644 --- a/cockatrice/themes/CMakeLists.txt +++ b/cockatrice/themes/CMakeLists.txt @@ -3,7 +3,6 @@ # add themes subfolders SET(defthemes - Default Fabric Leather Plasma diff --git a/cockatrice/themes/Default/.gitignore b/cockatrice/themes/Default/.gitignore deleted file mode 100644 index 86d0cb272..000000000 --- a/cockatrice/themes/Default/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# Ignore everything in this directory -* -# Except this file -!.gitignore \ No newline at end of file diff --git a/cockatrice/translations/cockatrice_el.ts b/cockatrice/translations/cockatrice_el.ts index 70a93ff99..972dbd1ec 100644 --- a/cockatrice/translations/cockatrice_el.ts +++ b/cockatrice/translations/cockatrice_el.ts @@ -12,12 +12,12 @@ Set counter - + Ρύθμιση μετρητή New value for counter '%1': - + Νέα τιμή για τον μετρητή '%1' diff --git a/cockatrice/translations/cockatrice_es.ts b/cockatrice/translations/cockatrice_es.ts index eeded1902..9571c522b 100644 --- a/cockatrice/translations/cockatrice_es.ts +++ b/cockatrice/translations/cockatrice_es.ts @@ -3690,17 +3690,17 @@ Cockatrice está recargando la base de datos de cartas. Declare Attackers - + Declarar atacantes Declare Blockers - + Declarar bloqueadores Combat Damage - + Daño de combate @@ -3715,7 +3715,7 @@ Cockatrice está recargando la base de datos de cartas. End/Cleanup - + Paso Final/Limpieza @@ -3826,7 +3826,7 @@ Cockatrice está recargando la base de datos de cartas. &Exile - &Exilio + &Exiliar @@ -3934,7 +3934,7 @@ Cockatrice está recargando la base de datos de cartas. Play top card - + Jugar la carta superior @@ -4389,7 +4389,7 @@ Cockatrice está recargando la base de datos de cartas. Common deck formats (*.cod *.dec *.dek *.txt *.mwDeck) - + Formatos de mazo comunes (*.cod *.dec *.dek *.txt *.mwDeck) @@ -5311,7 +5311,7 @@ Por favor, introduce un nombre: &Focus Chat - + &Resaltar chat @@ -5771,12 +5771,12 @@ Cuando más información introduzcas, más específicos serán los resultados. Click to view - + Click para ver Your buddy %1 has signed on! - + Tu amigo %1 se ha conectado! @@ -6048,7 +6048,7 @@ Por favor, absténgase de realizar de nuevo esta actividad o se tomarán medidas Copy hash to clipboard - + Copiar al portapapeles @@ -6299,17 +6299,17 @@ Por favor, absténgase de realizar de nuevo esta actividad o se tomarán medidas Use tear-off menus, allowing right click menus to persist on screen - + Usar menus flotantes, permitir click derecho para anclar menus en la pantalla Notifications settings - + Ajustes de notificaciones Notify in the taskbar when users in your buddy list connect - + Recibir una notificación en la barra de tareas cuando alguien de tu lista de amigos se conecte @@ -6672,52 +6672,52 @@ Por favor, absténgase de realizar de nuevo esta actividad o se tomarán medidas Card Counters - + Contadores de carta Player Counters - + Contadores de jugador Move Selected Card - + Mover la carta seleccionada Move Top Card - + Mover la carta superior Move Bottom Card - + Mover la carta inferior Chat Room - + Sala de chat Game Window - + Ventana de juego Load Deck from Clipboard - + Cargar deck desde el portapapeles Check for Card Updates... - + Buscar actualizaciones de cartas ... Connect... - + Conectar... @@ -6727,425 +6727,425 @@ Por favor, absténgase de realizar de nuevo esta actividad o se tomarán medidas Settings... - + Ajustes... Start a Local Game... - + Empezar una partida local... Watch Replay... - + Ver repetición... Analyze Deck - + Analizar mazo Clear All Filters - + Eliminar filtros Clear Selected Filter - + Eleminar filtro seleccionado Remove Card - + Quitar carta Manage Sets... - + Administrar ediciones Edit Custom Tokens... - + Editar fichas personalizadas Export Deck - + Exportar mazo Add Card - + Añadir carta Load Deck... - + Cargar mazo... Load Deck from Clipboard... - + Cargar mazo desde el portapapeles New Deck - + Nuevo mazo Open Custom Pictures Folder - + Abrir carpeta de imágenes personalizadas Print Deck... - + Imprimir mazo... Delete Card - + Borrar carta Reset Layout - + Restablecer disposición Save Deck - + Guardar mazo Save Deck as... - + Guarda mazo como... Save Deck to Clipboard, Annotated - + Guarda mazo al portapapeles, Comentado Save Deck to Clipboard - + Guardar mazo al portapapeles Load Local Deck... - + Cargar mazo local... Load Remote Deck... - + Carga mazo remoto... Set Ready to Start - + Marcar listo para empezar Toggle Sideboard Lock - + Permitir bloqueo de banquillo Add Green Counter - + Añadir contador verde Remove Green Counter - + Retirar un contador verde Set Green Counters... - + Establecer contadores verdes... Add Yellow Counter - + Añadir contador amarillo Remove Yellow Counter - + Retirar un contador amarillo Set Yellow Counters... - + Establecer contadores amarillos... Add Red Counter - + Agregar contador rojo Remove Red Counter - + Retirar un contador rojo Set Red Counters... - + Establecer contadores rojos... Add Life Counter - + Añadir contador de vida Remove Life Counter - + Retirar un contador de vida Set Life Counters... - + Establecer contadores de vida... Add White Counter - + Agregar contador blanco Remove White Counter - + Retirar un contador blanco... Set White Counters... - + Establecer contadores blancos... Add Blue Counter - + Agregar contador azul Remove Blue Counter - + Retirar un contador azul... Set Blue Counters... - + Establecer contadores azules... Add Black Counter - + Agregar contador negro Remove Black Counter - + Quitar contador negro Set Black Counters... - + Establecer contadores negros... Add Colorless Counter - + Agregar contador incoloro Remove Colorless Counter - + Retirar contador incoloro Set Colorless Counters... - + Establecer contadores incoloros... Add Storm Counter - + Agregar contador de tormenta Remove Storm Counter - + Retirar contador de tormenta Set Storm Counters... - + Establecer contadores de tormenta... Add Power (+1/+0) - + Agregar fuerza (+1/+0) Remove Power (-1/-0) - + Reducir fuerza (-1/-0) Move Toughness to Power (+1/-1) - + Pasar de resistencia a fuerza (+1/-1) Add Toughness (+0/+1) - + Agregar resistencia (+0/+1) Remove Toughness (-0/-1) - + Reducir resistencia (-0/-1) Move Power to Toughness (-1/+1) - + Pasar de fuerza a resistencia (-1/+1) Add Power and Toughness (+1/+1) - + Agregar fuerza y resistencia (+1/+1) Remove Power and Toughness (-1/-1) - + Reducir fuerza y resistencia (-1/-1) Set Power and Toughness... - + Establecer fuerza y resistencia... Reset Power and Toughness - + Restablecer fuerza y resistencia First Main Phase - + Primera fase principal Start Combat - + Comenzar combate End Combat - + Terminar combate Second Main Phase - + Segunda fase principal Next Phase - + Próxima fase Next Phase Action - + Próxima fase de acción Next Turn - + Próximo turno Untap All - + Enderezar todas Toggle Untap - + Alternar enderezamiento Turn Card Over - + Girar carta Peek Card - + Mirar carta Play Card - + Jugar carta Attach Card... - + Anexar carta... Unattach Card - + Desanexar carta... Clone Card - + Clonar carta Create Token... - + Crear ficha... Create All Related Tokens - + Crear todas las fichas relacionadas Create Another Token - + Crear otra ficha Set Annotation... - + Escribir anotación... Bottom of Library - + Parte inferior de la biblioteca @@ -7156,113 +7156,113 @@ Por favor, absténgase de realizar de nuevo esta actividad o se tomarán medidas Top of Library - + Parte superior de la biblioteca Battlefield, Face Down - + Campo de batalla, boca abajo Battlefield - + Campo de batalla Top Cards of Library - + Carta de la parte superior de la biblioteca Close Recent View - + Cerrar vista reciente Stack - + Apilar Graveyard (Multiple) - + Cementerio (Multiple) Exile (Multiple) - + Exiliar (Multiple) Draw Arrow... - + Dibujar flecha Remove Local Arrows - + Eliminar flechas Leave Game - + Abandonar partida Roll Dice... - + Lanzar dado... Shuffle Library - + Barajar biblioteca Draw a Card - + Robar una carta Draw Multiple Cards... - + Robar múltiples cartas... Undo Draw - + Deshacer último robo Always Reveal Top Card - + Mostrar siempre la carta superior Rotate View Clockwise - + Rotar vista en sentido horario Rotate View Counterclockwise - + Rotar vista en sentido antihorario Unfocus Text Box - + No resaltar la caja de texto Focus Chat - + Resaltar el chat Clear Chat - + Borrar chat diff --git a/cockatrice/translations/cockatrice_it.ts b/cockatrice/translations/cockatrice_it.ts index 0d8fbd37e..df56d3dd3 100644 --- a/cockatrice/translations/cockatrice_it.ts +++ b/cockatrice/translations/cockatrice_it.ts @@ -669,17 +669,17 @@ Questa è visibile solo ai moderatori e non alla persona bannata. Ready to start - + Pronto ad iniziare Sideboard unlocked - + Sideboard sbloccata Sideboard locked - + Sideboard bloccata @@ -1999,7 +1999,7 @@ Potresti dover creare dal codice sorgente da solo. Games shown: %1 / %2 - + Partite mostrate: %1 / %2 @@ -3663,62 +3663,62 @@ Il database delle carte verrà ricaricato. Unknown Phase - + Fase sconosciuta Untap - + Stappa Upkeep - + Mantenimento Draw - + Pesca First Main - + Prima fase principale Beginning of Combat - + Inizio combattimento Declare Attackers - + Dichiarazione attaccanti Declare Blockers - + Dichiarazione bloccanti Combat Damage - + Danno da combattimento End of Combat - + Fine combattimento Second Main - + Seconda fase principale End/Cleanup - + Fine/Cancellazione @@ -3937,7 +3937,7 @@ Il database delle carte verrà ricaricato. Play top card - + Gioca la prima carta @@ -4392,7 +4392,7 @@ Il database delle carte verrà ricaricato. Common deck formats (*.cod *.dec *.dek *.txt *.mwDeck) - + Formati di mazzo comuni (*.cod *.dec *.dek *.txt *.mwDeck) @@ -5774,12 +5774,12 @@ Più informazioni inserisci, più specifici saranno i risultati. Click to view - + Clicca per visualizzare Your buddy %1 has signed on! - + Il tuo amico %1 si è collegato @@ -5891,42 +5891,42 @@ Se pregato di evitare di continuare questa attività o potrebbero venire presi u Life - + Vita White - + Bianco Blue - + Blu Black - + Nero Red - + Rosso Green - + Verde Colorless - + Incolore Other - + Altro @@ -6054,7 +6054,7 @@ Se pregato di evitare di continuare questa attività o potrebbero venire presi u Copy hash to clipboard - + Copia hash nella clipboard @@ -6310,7 +6310,7 @@ Se pregato di evitare di continuare questa attività o potrebbero venire presi u Notifications settings - + Impostazioni notifiche @@ -6688,82 +6688,82 @@ Se pregato di evitare di continuare questa attività o potrebbero venire presi u Move Selected Card - + Muovi la carta selezionata Move Top Card - + Muovi la prima carta Move Bottom Card - + Muovi l'ultima carta Chat Room - + Chat Room Game Window - + Finestra di Gioco Load Deck from Clipboard - + Carica mazzo dalla clipboard Check for Card Updates... - + Controlla per aggiornamenti carte... Connect... - + Connetti... Register... - + Registrati... Settings... - + Impostazioni... Start a Local Game... - + Inizia una partita in locale... Watch Replay... - + Guarda Replay... Analyze Deck - + Analizza mazzo Clear All Filters - + Elimina tutti i filtri Clear Selected Filter - + Elimina il filtro selezionato Remove Card - + Rimuovi carta @@ -6773,22 +6773,22 @@ Se pregato di evitare di continuare questa attività o potrebbero venire presi u Edit Custom Tokens... - + Modifica pedine personalizzate Export Deck - + Esporta mazzo Add Card - + Aggiungi Carta Load Deck... - + Carica mazzo... @@ -6798,7 +6798,7 @@ Se pregato di evitare di continuare questa attività o potrebbero venire presi u New Deck - + Nuovo mazzo @@ -6808,12 +6808,12 @@ Se pregato di evitare di continuare questa attività o potrebbero venire presi u Print Deck... - + Stampa mazzo... Delete Card - + Elimina carta @@ -6824,12 +6824,12 @@ Se pregato di evitare di continuare questa attività o potrebbero venire presi u Save Deck - + Salva mazzo Save Deck as... - + Salva mazzo con nome @@ -6945,7 +6945,7 @@ Se pregato di evitare di continuare questa attività o potrebbero venire presi u Add Blue Counter - + Aggiungi Contatore Blu @@ -7055,27 +7055,27 @@ Se pregato di evitare di continuare questa attività o potrebbero venire presi u First Main Phase - + Prima Fase Principale Start Combat - + Inizio Combattimento End Combat - + Fine Combattimento Second Main Phase - + Seconda Fase Principale Next Phase - + Fase Successiva @@ -7085,12 +7085,12 @@ Se pregato di evitare di continuare questa attività o potrebbero venire presi u Next Turn - + Turno Successivo Untap All - + Stappa Tutto @@ -7110,7 +7110,7 @@ Se pregato di evitare di continuare questa attività o potrebbero venire presi u Play Card - + Gioca Carta @@ -7125,7 +7125,7 @@ Se pregato di evitare di continuare questa attività o potrebbero venire presi u Clone Card - + Clona Carta @@ -7188,7 +7188,7 @@ Se pregato di evitare di continuare questa attività o potrebbero venire presi u Stack - + Pila @@ -7213,22 +7213,22 @@ Se pregato di evitare di continuare questa attività o potrebbero venire presi u Leave Game - + Abbandona Partita Roll Dice... - + Tira un Dado... Shuffle Library - + Mischia il Mazzo Draw a Card - + Pesca una Carta diff --git a/cockatrice/translations/cockatrice_ja.ts b/cockatrice/translations/cockatrice_ja.ts index 6b45a7b9f..d83687a75 100644 --- a/cockatrice/translations/cockatrice_ja.ts +++ b/cockatrice/translations/cockatrice_ja.ts @@ -12,12 +12,12 @@ Set counter - + カウンターの設定 New value for counter '%1': - + '%1'カウンターの新しい値: @@ -589,32 +589,32 @@ This is only saved for moderators and cannot be seen by the banned person. Last Change - + 最新に更新 Do not close settings until manual update is complete - + 手動更新が完了するまでこのウィンドウを閉じないでください。 Download card pictures on the fly - + カード画像を自動的にダウンロード(英語) How to add a custom URL - + URLの追加方法について(英語) Delete Downloaded Images - + ダウンロードした画像を削除 Reset Download URLs - + ダウンロードURLをデフォルトに戻す @@ -624,7 +624,7 @@ This is only saved for moderators and cannot be seen by the banned person. Press the button to manually update without relaunching - ボタンを押すと再起動せずに手動で更新します + ボタンを押すと再起動せずに手動で更新します。 @@ -669,17 +669,17 @@ This is only saved for moderators and cannot be seen by the banned person. Ready to start - + 準備完了!( ・`д・´) Sideboard unlocked - + サイドボード使用可能 Sideboard locked - + サイドボードロック中 @@ -1177,7 +1177,7 @@ To remove your current avatar, confirm without choosing a new image. Edit custom tokens - + カスタムトークンを編集 @@ -1245,7 +1245,7 @@ Make sure to enable the 'Token' set in the "Manage sets" dia Hide '&ignored user' games - + 無視ユーザーのゲームを非表示 @@ -1676,7 +1676,7 @@ http://github.com/Cockatrice/Cockatrice/issues でチケットを提出してく Card Sources - + カードダウンロード @@ -1998,7 +1998,7 @@ You may have to build from source yourself. Games shown: %1 / %2 - + 表示されているゲーム: %1 / %2 @@ -2409,7 +2409,7 @@ Please close that session first and re-login. Your password has been reset successfully, you can now log in using the new credentials. - + パスワードをリセットしました。 @@ -2541,13 +2541,15 @@ This usually means that your client version is out of date, and the server sent This server requires client IDs. Your client is either failing to generate an ID or you are running a modified client. Please close and reopen your client to try again. - + このサーバはクライアントのIDを必要とします。あなたのクライアントはIDの生成に失敗しているか、変更されたクライアントを実行しています。 +クライアントを再起動してもう一度お試し下さい。 An internal error has occurred, please close and reopen Cockatrice before trying again. If the error persists, ensure you are running the latest version of the software and if needed contact the software developers. - + 内部エラーが発生しました。Cockatriceを再起動して再度お試しください。 +エラーが解決しない場合は、ソフトウェアの最新バージョンを実行していることを確認し、必要に応じてソフトウェア開発者にお問い合わせください。 @@ -2734,24 +2736,26 @@ Local version is %1, remote version is %2. New Version - + 新しいバージョン Congratulations on updating to Cockatrice %1! Oracle will now launch to update your card database. - + Cockatrice %1へのアップデートおめでとうございます! +Oracleが起動し、カードデータベースが更新されます。 Cockatrice installed - + Cockatriceがインストールされました Congratulations on installing Cockatrice %1! Oracle will now launch to install the initial card database. - + Cockatrice %1のインストールおめでとうございます! +Oracleを起動して、初期カードデータベースをインストールします。 @@ -2896,14 +2900,16 @@ If unsure or first time user, choose "Yes" Edit custom &tokens... - + カスタムトークンを編集... %n new set(s) found in the card database Set code(s): %1 Do you want to enable it/them? - + 新しいカードセットが見つかりました。 +コード: %1 +有効にしますか? @@ -3093,12 +3099,12 @@ Cockatrice will now reload the card database. %1 shuffles their deck and draws a new hand of %2 card(s). - + %1はライブラリーを切り直し、新たにカードを%2枚引いた。 %1 shuffles their deck and draws a new hand. - + %1はライブラリーを切り直し、新たに手札を引いた。 @@ -3115,17 +3121,17 @@ Cockatrice will now reload the card database. %1 reversed turn order, now it's %2. - + %1はターン順を逆にした(%2)。 reversed - + 逆転 normal - + 順転 @@ -3245,12 +3251,12 @@ Cockatrice will now reload the card database. %1 shuffles the bottom %3 cards of %2. - + %1 は%2の一番下の%3枚のカードをに無作為の順番で置いた。 %1 shuffles the top %3 cards of %2. - + %1 は%2の一番上の%3枚のカードをに無作為の順番で置いた。 @@ -3385,17 +3391,17 @@ Cockatrice will now reload the card database. %1 removes the PT of %2. - + %1 は %2 のP/Tを取り除いた。 %1 changes the PT of %2 from nothing to %4. - + %1 は %2 のP/Tを%4にした。 %1 changes the PT of %2 from %3 to %4. - + %1 は %2 のP/Tを%3から%4にした。 @@ -3498,17 +3504,17 @@ Cockatrice will now reload the card database. Add New URL - + 新しいURLを追加 Edit URL - + URLを編集 Remove URL - + URLを削除 @@ -3599,52 +3605,52 @@ Cockatrice will now reload the card database. Color(s) - + Loyalty - + 忠誠度 Card Type - + カード・タイプ Converted Mana Cost - + 点数で見たマナ・コスト Main Card Type - + メインのカード・タイプ Mana Cost - + マナ・コスト P/T - + P/T Side - + 表裏 Layout - + レイアウト Color Identity - + 固有色 @@ -3652,62 +3658,62 @@ Cockatrice will now reload the card database. Unknown Phase - + 謎のフェイズ Untap - + アンタップ・ステップ Upkeep - + アップキープ・ステップ Draw - + ドロー・ステップ First Main - + 戦闘前メイン・フェイズ Beginning of Combat - + 戦闘開始ステップ Declare Attackers - + 攻撃クリーチャー指定ステップ Declare Blockers - + ブロック・クリーチャー指定ステップ Combat Damage - + 戦闘ダメージ・ステップ End of Combat - + 戦闘終了ステップ Second Main - + 戦闘後メイン・フェイズ End/Cleanup - + 最終フェイズ @@ -3866,7 +3872,7 @@ Cockatrice will now reload the card database. &View hand - + 手札を見る @@ -3926,7 +3932,7 @@ Cockatrice will now reload the card database. Play top card - + 一番上のカードをプレイ @@ -4111,12 +4117,12 @@ Cockatrice will now reload the card database. Increase power and decrease toughness - + パワーを上げてタフネスを下げる Decrease power and increase toughness - + パワーを下げてタフネスを上げる @@ -4156,17 +4162,17 @@ Cockatrice will now reload the card database. &Bottom of library in random order - + ライブラリーの一番下に無作為の順番で Draw hand - + 手札を引く 0 and lower are in comparison to current hand size - + 0以下の数を指定すると現在の枚数から減らします @@ -4182,7 +4188,7 @@ Cockatrice will now reload the card database. Change power/toughness - + P/Tを変更 @@ -4381,7 +4387,7 @@ Cockatrice will now reload the card database. Common deck formats (*.cod *.dec *.dek *.txt *.mwDeck) - + 共通デッキ形式 (*.cod *.dec *.txt *.mwDeck) @@ -4389,17 +4395,17 @@ Cockatrice will now reload the card database. Cancel - + キャンセル Discard - + カードを捨てる Help - + ヘルプ @@ -4576,7 +4582,7 @@ Cockatrice will now reload the card database. Hit the key/combination of keys you want to set for this action - + このアクションに設定するキー(の組み合わせ)を押してください @@ -4591,12 +4597,12 @@ Cockatrice will now reload the card database. Clear - + クリア Restore default - + 元に戻す @@ -4632,52 +4638,52 @@ Cockatrice will now reload the card database. Restore all default shortcuts - + すべてのショートカットを元に戻す Do you really want to restore all default shortcuts? - + 本当にすべてのショートカットキーを元に戻しますか? Clear all default shortcuts - + すべてのショートカットをクリア Do you really want to clear all shortcuts? - + 本当にすべてのショートカットキーをクリアしますか? Action - + アクション Shortcut - + ショートカットキー Section: - + セクション: Action: - + アクション: Shortcut: - + ショートカット: How to set custom shortcuts - + ショートカットキーの設定方法について(英語) @@ -5254,7 +5260,7 @@ Please enter a name: Next phase with &action - + ターン起因処理をしながら次のフェイズ @@ -5264,7 +5270,7 @@ Please enter a name: Reverse turn order - + ゲームのターン順を逆転する @@ -5350,12 +5356,12 @@ Please enter a name: Unconcede - + 投了を取り消す You have already conceded. Do you want to return to this game? - + あなたはすでに投了しています! 本当にゲームに復帰しますか? @@ -5731,12 +5737,12 @@ The more information you put in, the more specific your results will be. Failed to join the room: it doesn't exist on the server. - + ルームに参加できませんでした: サーバーに存在しません。 The server thinks you are in the room but your client is unable to display it. Try restarting your client. - + サーバーからルームに入室中の応答がありましたが、クライアントが表示できませんでした。クライアントを再起動してみてください。 @@ -5764,12 +5770,12 @@ The more information you put in, the more specific your results will be. Click to view - + クリックで見る Your buddy %1 has signed on! - + %1がオンライン! @@ -5805,7 +5811,7 @@ To update your client, go to Help -> Check for Updates. You have been promoted. Please log out and back in for changes to take effect. - + あなたは昇格されました。変更を有効にするためにログインしなおして下さい。 @@ -5881,42 +5887,42 @@ Please refrain from engaging in this activity or further actions may be taken ag Life - + ライフ White - + Blue - + Black - + Red - + Green - + Colorless - + 無色 Other - + その他 @@ -6044,7 +6050,7 @@ Please refrain from engaging in this activity or further actions may be taken ag Copy hash to clipboard - + クリップボードにハッシュをコピー @@ -6295,7 +6301,7 @@ Please refrain from engaging in this activity or further actions may be taken ag Use tear-off menus, allowing right click menus to persist on screen - + ティアオフメニューを使用して、右クリックメニューを画面に表示したままにする @@ -6305,7 +6311,7 @@ Please refrain from engaging in this activity or further actions may be taken ag Notify in the taskbar when users in your buddy list connect - + フレンドリストのユーザーが接続したときにタスクバーで通知する @@ -6445,7 +6451,7 @@ Please refrain from engaging in this activity or further actions may be taken ag Only cards in enabled sets will appear in the card list of the deck editor - + 有効にしたセットのカードのみがデッキエディタのカードリストに表示されます @@ -7045,7 +7051,7 @@ Please refrain from engaging in this activity or further actions may be taken ag First Main Phase - + 戦闘前メイン・フェイズ @@ -7060,17 +7066,17 @@ Please refrain from engaging in this activity or further actions may be taken ag Second Main Phase - + 戦闘後メイン・フェイズ Next Phase - + 次のフェイズ Next Phase Action - + 処理しながら次のフェイズ diff --git a/cockatrice/translations/cockatrice_nl.ts b/cockatrice/translations/cockatrice_nl.ts index 8619fb9bd..ffe63af15 100644 --- a/cockatrice/translations/cockatrice_nl.ts +++ b/cockatrice/translations/cockatrice_nl.ts @@ -594,7 +594,7 @@ Dit wordt opgeslagen voor het moderatorteam en zal niet zichtbaar zijn voor de v Do not close settings until manual update is complete - + Sluit de instellingen niet af voordat de handmatige update is voltooid @@ -669,17 +669,17 @@ Dit wordt opgeslagen voor het moderatorteam en zal niet zichtbaar zijn voor de v Ready to start - + Gereed voor start Sideboard unlocked - + Sideboard ontgrendeld Sideboard locked - + Sideboard vergrendeld @@ -1245,7 +1245,7 @@ Zorg ervoor dat u de 'Token' set in het " Beheer sets" dialo Hide '&ignored user' games - + Verberg spellen door genegeerde gebruikers @@ -1998,7 +1998,7 @@ Mogelijk moet je zelf van source bouwen. Games shown: %1 / %2 - + Spellen weergegeven: %1 / %2 @@ -3101,12 +3101,12 @@ Cockatrice zal nu de kaartendatabase herladen. %1 shuffles their deck and draws a new hand of %2 card(s). - + %1 schudt zijn of haar deck en raapt een nieuwe hand van %2 kaart.%1 schudt zijn of haar deck en raapt een nieuwe hand van %2 kaarten. %1 shuffles their deck and draws a new hand. - + %1 schudt zijn of haar deck en trekt een nieuwe hand. @@ -3123,17 +3123,17 @@ Cockatrice zal nu de kaartendatabase herladen. %1 reversed turn order, now it's %2. - + %1 keert de beurtvolgorde om, deze is nu %2. reversed - + omgekeerd normal - + normaal @@ -3660,62 +3660,62 @@ Cockatrice zal nu de kaartendatabase herladen. Unknown Phase - + Onbekende fase Untap - + Untap Upkeep - + Upkeep Draw - + Rapen First Main - + Eerste Hoofdfase Beginning of Combat - + Begin Gevecht Declare Attackers - + Aanvallers Aangeven Declare Blockers - + Verdedigers Aangeven Combat Damage - + Gevecht Schade End of Combat - + Einde Gevecht Second Main - + Tweede Hoofdfase End/Cleanup - + Einde/Opruimen @@ -3934,7 +3934,7 @@ Cockatrice zal nu de kaartendatabase herladen. Play top card - + Speel bovenste kaart @@ -4169,12 +4169,12 @@ Cockatrice zal nu de kaartendatabase herladen. Draw hand - + Trek hand 0 and lower are in comparison to current hand size - + 0 en lager zijn in vergelijking met de huidige handgrootte @@ -4389,7 +4389,7 @@ Cockatrice zal nu de kaartendatabase herladen. Common deck formats (*.cod *.dec *.dek *.txt *.mwDeck) - + Gebruikelijke formaten voor decks (*.cod *.dec *.dek *.txt *.mwDeck) @@ -5270,7 +5270,7 @@ Please enter a name: Reverse turn order - + Keer beurtvolgorde om @@ -5310,7 +5310,7 @@ Please enter a name: &Focus Chat - + &Focus Chat @@ -5770,12 +5770,12 @@ Hoe meer informatie u inbrengt, hoe specifieker uw resultaten zullen zijn. Click to view - + Klik om weer te geven Your buddy %1 has signed on! - + Je maatje %1 heeft in gelogd! @@ -5887,42 +5887,42 @@ Gelieve af te zien van deelname aan deze activiteit of er kunnen verdere acties Life - + Leven White - + Wit Blue - + Blauw Black - + Zwart Red - + Rood Green - + Groen Colorless - + Kleurloos Other - + Overig @@ -6050,7 +6050,7 @@ Gelieve af te zien van deelname aan deze activiteit of er kunnen verdere acties Copy hash to clipboard - + Copieer hash naar klembord diff --git a/cockatrice/translations/cockatrice_pt_BR.ts b/cockatrice/translations/cockatrice_pt_BR.ts index f4814fa96..af87a34da 100644 --- a/cockatrice/translations/cockatrice_pt_BR.ts +++ b/cockatrice/translations/cockatrice_pt_BR.ts @@ -5775,7 +5775,7 @@ Quanto mais informação você inserir, mais específicos seus resultados serão Click to view - + Clique para visualizar @@ -6809,7 +6809,7 @@ Por favor evite tais ações ou outras medidas poderão ser tomadas contra você Print Deck... - + Imprimir Deck... diff --git a/cockatrice/translations/cockatrice_tr.ts b/cockatrice/translations/cockatrice_tr.ts index d89225b2f..f4722764b 100644 --- a/cockatrice/translations/cockatrice_tr.ts +++ b/cockatrice/translations/cockatrice_tr.ts @@ -70,7 +70,7 @@ Invert vertical coordinate - + Dikey koordinatı ters çevir @@ -167,7 +167,7 @@ This is only saved for moderators and cannot be seen by the banned person. Error - + Hata @@ -195,22 +195,22 @@ This is only saved for moderators and cannot be seen by the banned person. Beta Releases - + Beta Sürümü No reply received from the release update server. - + Sürüm güncelleme sunucusundan yanıt alınamadı. Invalid reply received from the release update server. - + Sürüm güncelleme sunucusundan geçersiz yanıt alındı. No reply received from the file update server. - + Dosya güncelleme sunucusundan yanıt alınamadı. @@ -218,7 +218,7 @@ This is only saved for moderators and cannot be seen by the banned person. Name - + İsim @@ -228,12 +228,12 @@ This is only saved for moderators and cannot be seen by the banned person. Mana cost - + Mana bedeli Card type - + Kart tipi @@ -243,7 +243,7 @@ This is only saved for moderators and cannot be seen by the banned person. Color(s) - + Renk(ler) @@ -252,13 +252,13 @@ This is only saved for moderators and cannot be seen by the banned person. AND Logical conjunction operator used in card filter - + VE OR Logical disjunction operator used in card filter - + VEYA @@ -275,22 +275,22 @@ This is only saved for moderators and cannot be seen by the banned person. Name - + İsim Type - + Tür Color - + Renk Text - + Yazı @@ -300,7 +300,7 @@ This is only saved for moderators and cannot be seen by the banned person. Mana Cost - + Mana Bedeli @@ -310,22 +310,22 @@ This is only saved for moderators and cannot be seen by the banned person. Rarity - + Enderlik Power - + Güç Toughness - + Dayanıklılık Loyalty - + Sadakat @@ -343,7 +343,7 @@ This is only saved for moderators and cannot be seen by the banned person. Description - + Açıklama @@ -361,12 +361,12 @@ This is only saved for moderators and cannot be seen by the banned person. Unknown card: - + Bilinmeyen kart: Name: - + İsim: @@ -517,7 +517,7 @@ This is only saved for moderators and cannot be seen by the banned person. Success - + Başarılı @@ -542,23 +542,23 @@ This is only saved for moderators and cannot be seen by the banned person. Add URL - + URL ekle URL: - + URL: Edit URL - + URL'yi düzenle Updating... - + Güncelleniyor... @@ -797,7 +797,7 @@ This is only saved for moderators and cannot be seen by the banned person. Server URL - + Sunucu URL @@ -1114,27 +1114,27 @@ To remove your current avatar, confirm without choosing a new image. white - + beyaz blue - + mavi black - + siyah red - + kırmızı green - + yeşil diff --git a/common/decklist.cpp b/common/decklist.cpp index e28084de2..a1463d0cc 100644 --- a/common/decklist.cpp +++ b/common/decklist.cpp @@ -492,7 +492,9 @@ bool DeckList::loadFromStream_Plain(QTextStream &in) const QRegularExpression reEmpty("^\\s*$"); const QRegularExpression reComment("[\\w\\[\\(\\{].*$", QRegularExpression::UseUnicodePropertiesOption); const QRegularExpression reSBMark("^\\s*sb:\\s*(.+)", QRegularExpression::CaseInsensitiveOption); - const QRegularExpression reSBComment("sideboard", QRegularExpression::CaseInsensitiveOption); + const QRegularExpression reSBComment("^sideboard\\b.*$", QRegularExpression::CaseInsensitiveOption); + const QRegularExpression reDeckComment("^((main)?deck(list)?|mainboard)\\b", + QRegularExpression::CaseInsensitiveOption); // simplified matches const QRegularExpression reMultiplier("^[xX\\(\\[]*(\\d+)[xX\\*\\)\\]]* ?(.+)"); @@ -563,6 +565,16 @@ bool DeckList::loadFromStream_Plain(QTextStream &in) } comments.chop(1); // remove last newline + // discard empty lines + while (index < max_line && inputs.at(index).contains(reEmpty)) { + ++index; + } + + // discard line if it starts with deck or mainboard, all cards until the sideboard starts are in the mainboard + if (inputs.at(index).contains(reDeckComment)) { + ++index; + } + // parse decklist for (; index < max_line; ++index) { diff --git a/common/featureset.cpp b/common/featureset.cpp index cdd30f223..0151d5c0a 100644 --- a/common/featureset.cpp +++ b/common/featureset.cpp @@ -26,8 +26,8 @@ void FeatureSet::initalizeFeatureList(QMap &featureList) featureList.insert("forgot_password", false); featureList.insert("websocket", false); // These are temp to force users onto a newer client - featureList.insert("2.6.1_min_version", false); featureList.insert("2.7.0_min_version", false); + featureList.insert("2.8.0_min_version", false); } void FeatureSet::enableRequiredFeature(QMap &featureList, QString featureName) diff --git a/common/pb/CMakeLists.txt b/common/pb/CMakeLists.txt index 5467303f6..ea1855d89 100644 --- a/common/pb/CMakeLists.txt +++ b/common/pb/CMakeLists.txt @@ -48,7 +48,6 @@ SET(PROTO_FILES command_set_sideboard_lock.proto command_shuffle.proto commands.proto - command_stop_dump_zone.proto command_undo_draw.proto context_concede.proto context_connection_state_changed.proto @@ -102,7 +101,6 @@ SET(PROTO_FILES event_set_card_counter.proto event_set_counter.proto event_shuffle.proto - event_stop_dump_zone.proto event_user_joined.proto event_user_left.proto event_user_message.proto diff --git a/common/pb/admin_commands.proto b/common/pb/admin_commands.proto index 6f974d92f..8faaec2d2 100644 --- a/common/pb/admin_commands.proto +++ b/common/pb/admin_commands.proto @@ -37,4 +37,3 @@ message Command_AdjustMod { optional bool should_be_mod = 2; optional bool should_be_judge = 3; } - diff --git a/common/pb/command_attach_card.proto b/common/pb/command_attach_card.proto index 654e57c5f..9e13edc7b 100644 --- a/common/pb/command_attach_card.proto +++ b/common/pb/command_attach_card.proto @@ -10,5 +10,3 @@ message Command_AttachCard { optional string target_zone = 4; optional sint32 target_card_id = 5 [default = -1]; } - - diff --git a/common/pb/command_change_zone_properties.proto b/common/pb/command_change_zone_properties.proto index f89e36aaa..027cc5720 100644 --- a/common/pb/command_change_zone_properties.proto +++ b/common/pb/command_change_zone_properties.proto @@ -6,6 +6,9 @@ message Command_ChangeZoneProperties { optional Command_ChangeZoneProperties ext = 1031; } optional string zone_name = 1; - + + // Reveal top card to all players. optional bool always_reveal_top_card = 10; + // reveal top card to the owner. + optional bool always_look_at_top_card = 11; } diff --git a/common/pb/command_concede.proto b/common/pb/command_concede.proto index 8377d958f..9bec8e48c 100644 --- a/common/pb/command_concede.proto +++ b/common/pb/command_concede.proto @@ -6,7 +6,6 @@ message Command_Concede { } } - message Command_Unconcede { extend GameCommand { optional Command_Unconcede ext = 1032; diff --git a/common/pb/command_create_token.proto b/common/pb/command_create_token.proto index 9fc61b70e..d54256484 100644 --- a/common/pb/command_create_token.proto +++ b/common/pb/command_create_token.proto @@ -15,5 +15,3 @@ message Command_CreateToken { optional string target_zone = 9; optional sint32 target_card_id = 10 [default = -1]; } - - diff --git a/common/pb/command_deck_del_dir.proto b/common/pb/command_deck_del_dir.proto index 3364cac42..10790749f 100644 --- a/common/pb/command_deck_del_dir.proto +++ b/common/pb/command_deck_del_dir.proto @@ -7,4 +7,3 @@ message Command_DeckDelDir { } optional string path = 1; } - diff --git a/common/pb/command_deck_download.proto b/common/pb/command_deck_download.proto index 19dda1528..610a2e785 100644 --- a/common/pb/command_deck_download.proto +++ b/common/pb/command_deck_download.proto @@ -7,4 +7,3 @@ message Command_DeckDownload { } optional sint32 deck_id = 1 [default = -1]; } - diff --git a/common/pb/command_deck_new_dir.proto b/common/pb/command_deck_new_dir.proto index b6e24afcd..7611fa531 100644 --- a/common/pb/command_deck_new_dir.proto +++ b/common/pb/command_deck_new_dir.proto @@ -8,4 +8,3 @@ message Command_DeckNewDir { optional string path = 1; optional string dir_name = 2; } - diff --git a/common/pb/command_deck_upload.proto b/common/pb/command_deck_upload.proto index 0f250c5f0..63d9c80ef 100644 --- a/common/pb/command_deck_upload.proto +++ b/common/pb/command_deck_upload.proto @@ -5,7 +5,7 @@ message Command_DeckUpload { extend SessionCommand { optional Command_DeckUpload ext = 1013; } - optional string path = 1; // to upload a new deck - optional uint32 deck_id = 2; // to replace an existing deck + optional string path = 1; // to upload a new deck + optional uint32 deck_id = 2; // to replace an existing deck optional string deck_list = 3; } diff --git a/common/pb/command_draw_cards.proto b/common/pb/command_draw_cards.proto index 6851ac00c..d95e05787 100644 --- a/common/pb/command_draw_cards.proto +++ b/common/pb/command_draw_cards.proto @@ -6,4 +6,3 @@ message Command_DrawCards { } optional uint32 number = 1; } - diff --git a/common/pb/command_flip_card.proto b/common/pb/command_flip_card.proto index 07f034009..fe5047199 100644 --- a/common/pb/command_flip_card.proto +++ b/common/pb/command_flip_card.proto @@ -9,5 +9,3 @@ message Command_FlipCard { optional bool face_down = 3; optional string pt = 4; } - - diff --git a/common/pb/command_game_say.proto b/common/pb/command_game_say.proto index 6aa47e0e2..2011ee096 100644 --- a/common/pb/command_game_say.proto +++ b/common/pb/command_game_say.proto @@ -6,5 +6,3 @@ message Command_GameSay { } optional string message = 1; } - - diff --git a/common/pb/command_kick_from_game.proto b/common/pb/command_kick_from_game.proto index e95037c71..331ec2547 100644 --- a/common/pb/command_kick_from_game.proto +++ b/common/pb/command_kick_from_game.proto @@ -3,7 +3,6 @@ import "game_commands.proto"; message Command_KickFromGame { extend GameCommand { optional Command_KickFromGame ext = 1000; -} + } optional sint32 player_id = 1 [default = -1]; } - diff --git a/common/pb/command_leave_game.proto b/common/pb/command_leave_game.proto index afa1e6c4e..8518cf2fc 100644 --- a/common/pb/command_leave_game.proto +++ b/common/pb/command_leave_game.proto @@ -5,4 +5,3 @@ message Command_LeaveGame { optional Command_LeaveGame ext = 1001; } } - diff --git a/common/pb/command_move_card.proto b/common/pb/command_move_card.proto index ad3f7c573..590a834b3 100644 --- a/common/pb/command_move_card.proto +++ b/common/pb/command_move_card.proto @@ -23,4 +23,3 @@ message Command_MoveCard { optional sint32 x = 6 [default = -1]; optional sint32 y = 7 [default = -1]; } - diff --git a/common/pb/command_mulligan.proto b/common/pb/command_mulligan.proto index 38ed5a364..a48b1a9ca 100644 --- a/common/pb/command_mulligan.proto +++ b/common/pb/command_mulligan.proto @@ -6,4 +6,3 @@ message Command_Mulligan { } optional uint32 number = 7; } - diff --git a/common/pb/command_replay_modify_match.proto b/common/pb/command_replay_modify_match.proto index 6b342f44a..94e35db20 100644 --- a/common/pb/command_replay_modify_match.proto +++ b/common/pb/command_replay_modify_match.proto @@ -8,4 +8,3 @@ message Command_ReplayModifyMatch { optional sint32 game_id = 1 [default = -1]; optional bool do_not_hide = 2; } - diff --git a/common/pb/command_reverse_turn.proto b/common/pb/command_reverse_turn.proto index 193379a0a..c5cc1c4d9 100644 --- a/common/pb/command_reverse_turn.proto +++ b/common/pb/command_reverse_turn.proto @@ -3,6 +3,5 @@ import "game_commands.proto"; message Command_ReverseTurn { extend GameCommand { optional Command_ReverseTurn ext = 1034; - } + } } - diff --git a/common/pb/command_roll_die.proto b/common/pb/command_roll_die.proto index bdcc7b51c..0d0c48075 100644 --- a/common/pb/command_roll_die.proto +++ b/common/pb/command_roll_die.proto @@ -6,4 +6,3 @@ message Command_RollDie { } optional uint32 sides = 1; } - diff --git a/common/pb/command_set_sideboard_plan.proto b/common/pb/command_set_sideboard_plan.proto index 7ed0d10cb..89c676d2d 100644 --- a/common/pb/command_set_sideboard_plan.proto +++ b/common/pb/command_set_sideboard_plan.proto @@ -8,4 +8,3 @@ message Command_SetSideboardPlan { } repeated MoveCard_ToZone move_list = 1; } - diff --git a/common/pb/command_shuffle.proto b/common/pb/command_shuffle.proto index 507195a48..a99f90896 100644 --- a/common/pb/command_shuffle.proto +++ b/common/pb/command_shuffle.proto @@ -8,4 +8,3 @@ message Command_Shuffle { optional sint32 start = 2 [default = 0]; optional sint32 end = 3 [default = -1]; } - diff --git a/common/pb/command_stop_dump_zone.proto b/common/pb/command_stop_dump_zone.proto deleted file mode 100644 index 1896c9dcb..000000000 --- a/common/pb/command_stop_dump_zone.proto +++ /dev/null @@ -1,9 +0,0 @@ -syntax = "proto2"; -import "game_commands.proto"; -message Command_StopDumpZone { - extend GameCommand { - optional Command_StopDumpZone ext = 1025; - } - optional sint32 player_id = 1; - optional string zone_name = 2; -} diff --git a/common/pb/commands.proto b/common/pb/commands.proto index b417550a3..b6eaf6733 100644 --- a/common/pb/commands.proto +++ b/common/pb/commands.proto @@ -7,10 +7,10 @@ import "admin_commands.proto"; message CommandContainer { optional uint64 cmd_id = 1; - + optional uint32 game_id = 10; optional uint32 room_id = 20; - + repeated SessionCommand session_command = 100; repeated GameCommand game_command = 101; repeated RoomCommand room_command = 102; diff --git a/common/pb/event_change_zone_properties.proto b/common/pb/event_change_zone_properties.proto index 0f1deb6d9..35b077b38 100644 --- a/common/pb/event_change_zone_properties.proto +++ b/common/pb/event_change_zone_properties.proto @@ -6,6 +6,9 @@ message Event_ChangeZoneProperties { optional Event_ChangeZoneProperties ext = 2020; } optional string zone_name = 1; - + + // Reveal top card to all players. optional bool always_reveal_top_card = 10; + // reveal top card to the owner. + optional bool always_look_at_top_card = 11; } diff --git a/common/pb/event_game_host_changed.proto b/common/pb/event_game_host_changed.proto index 50e3f968f..0afe2effb 100644 --- a/common/pb/event_game_host_changed.proto +++ b/common/pb/event_game_host_changed.proto @@ -6,4 +6,3 @@ message Event_GameHostChanged { optional Event_GameHostChanged ext = 1003; } } - diff --git a/common/pb/event_kicked.proto b/common/pb/event_kicked.proto index e1fd57bdd..02036cee7 100644 --- a/common/pb/event_kicked.proto +++ b/common/pb/event_kicked.proto @@ -6,4 +6,3 @@ message Event_Kicked { optional Event_Kicked ext = 1004; } } - diff --git a/common/pb/event_leave_room.proto b/common/pb/event_leave_room.proto index 98dafe2d4..dc9f3e866 100644 --- a/common/pb/event_leave_room.proto +++ b/common/pb/event_leave_room.proto @@ -7,6 +7,3 @@ message Event_LeaveRoom { } optional string name = 1; } - - - diff --git a/common/pb/event_notify_user.proto b/common/pb/event_notify_user.proto index 5cda9578a..3a90d278b 100644 --- a/common/pb/event_notify_user.proto +++ b/common/pb/event_notify_user.proto @@ -4,7 +4,7 @@ import "session_event.proto"; message Event_NotifyUser { enum NotificationType { - UNKNOWN = 0; // Default enum value if no "type" is defined when used + UNKNOWN = 0; // Default enum value if no "type" is defined when used PROMOTED = 1; WARNING = 2; IDLEWARNING = 3; @@ -18,5 +18,4 @@ message Event_NotifyUser { optional string warning_reason = 2; optional string custom_title = 3; optional string custom_content = 4; - } diff --git a/common/pb/event_room_say.proto b/common/pb/event_room_say.proto index b5d02c443..2c6a990ea 100644 --- a/common/pb/event_room_say.proto +++ b/common/pb/event_room_say.proto @@ -14,5 +14,4 @@ message Event_RoomSay { optional string message = 2; optional RoomMessageType message_type = 3; optional uint64 time_of = 4; - } diff --git a/common/pb/event_stop_dump_zone.proto b/common/pb/event_stop_dump_zone.proto deleted file mode 100644 index 3e8f16936..000000000 --- a/common/pb/event_stop_dump_zone.proto +++ /dev/null @@ -1,10 +0,0 @@ -syntax = "proto2"; -import "game_event.proto"; - -message Event_StopDumpZone { - extend GameEvent { - optional Event_StopDumpZone ext = 2019; - } - optional sint32 zone_owner_id = 1; - optional string zone_name = 2; -} diff --git a/common/pb/game_commands.proto b/common/pb/game_commands.proto index 76f7b2008..3a36cd21e 100644 --- a/common/pb/game_commands.proto +++ b/common/pb/game_commands.proto @@ -26,7 +26,7 @@ message GameCommand { NEXT_TURN = 1022; SET_ACTIVE_PHASE = 1023; DUMP_ZONE = 1024; - STOP_DUMP_ZONE = 1025; + STOP_DUMP_ZONE = 1025; // deprecated REVEAL_CARDS = 1026; MOVE_CARD = 1027; SET_SIDEBOARD_PLAN = 1028; diff --git a/common/pb/game_event.proto b/common/pb/game_event.proto index 0d7c2d9c7..45644e2c0 100644 --- a/common/pb/game_event.proto +++ b/common/pb/game_event.proto @@ -28,7 +28,7 @@ message GameEvent { SET_ACTIVE_PLAYER = 2016; SET_ACTIVE_PHASE = 2017; DUMP_ZONE = 2018; - STOP_DUMP_ZONE = 2019; + STOP_DUMP_ZONE = 2019; // deprecated CHANGE_ZONE_PROPERTIES = 2020; REVERSE_TURN = 2021; } diff --git a/common/pb/isl_message.proto b/common/pb/isl_message.proto index 125205334..d4bb8d785 100644 --- a/common/pb/isl_message.proto +++ b/common/pb/isl_message.proto @@ -9,17 +9,17 @@ message IslMessage { enum MessageType { GAME_COMMAND_CONTAINER = 0; ROOM_COMMAND_CONTAINER = 1; - + RESPONSE = 10; SESSION_EVENT = 11; GAME_EVENT_CONTAINER = 12; ROOM_EVENT = 13; } optional MessageType message_type = 1; - + optional uint64 session_id = 9; optional sint32 player_id = 10 [default = -1]; - + optional CommandContainer game_command = 100; optional CommandContainer room_command = 101; diff --git a/common/pb/moderator_commands.proto b/common/pb/moderator_commands.proto index d7050ad1b..cc2156b86 100644 --- a/common/pb/moderator_commands.proto +++ b/common/pb/moderator_commands.proto @@ -60,13 +60,12 @@ message Command_ViewLogHistory { extend ModeratorCommand { optional Command_ViewLogHistory ext = 1005; } - optional string user_name = 1; // user that created message - optional string ip_address = 2; // ip address of user that created message - optional string game_name = 3; // client id of user that created the message - optional string game_id = 4; // game number the message was sent to - optional string message = 5; // raw message that was sent - repeated string log_location = 6; // destination of message (ex: main room, game room, private chat) - required uint32 date_range = 7; // the length of time (in minutes) to look back for + optional string user_name = 1; // user that created message + optional string ip_address = 2; // ip address of user that created message + optional string game_name = 3; // client id of user that created the message + optional string game_id = 4; // game number the message was sent to + optional string message = 5; // raw message that was sent + repeated string log_location = 6; // destination of message (ex: main room, game room, private chat) + required uint32 date_range = 7; // the length of time (in minutes) to look back for optional uint32 maximum_results = 8; // the maximum number of query results - } diff --git a/common/pb/response.proto b/common/pb/response.proto index 75bca97a8..e2b3a71af 100644 --- a/common/pb/response.proto +++ b/common/pb/response.proto @@ -25,21 +25,23 @@ message Response { RespAccessDenied = 20; RespUsernameInvalid = 21; RespRegistrationRequired = 22; - RespRegistrationAccepted = 23; // Server agrees to process client's registration request - RespUserAlreadyExists = 24; // Client attempted to register a name which is already registered + RespRegistrationAccepted = 23; // Server agrees to process client's registration request + RespUserAlreadyExists = 24; // Client attempted to register a name which is already registered RespEmailRequiredToRegister = 25; // Server requires email to register accounts but client did not provide one - RespTooManyRequests = 26; // Server refused to complete command because client has sent too many too quickly + RespTooManyRequests = 26; // Server refused to complete command because client has sent too many too quickly RespPasswordTooShort = 27; // Server requires a decent password - RespAccountNotActivated = 28; // Client attempted to log into a registered username but the account hasn't been activated + RespAccountNotActivated = + 28; // Client attempted to log into a registered username but the account hasn't been activated RespRegistrationDisabled = 29; // Server does not allow clients to register - RespRegistrationFailed = 30; // Server accepted a reg request but failed to perform the registration - RespActivationAccepted = 31; // Server accepted a reg user activation token - RespActivationFailed = 32; // Server didn't accept a reg user activation token - RespRegistrationAcceptedNeedsActivation = 33; // Server accepted cient registration, but it will need token activation + RespRegistrationFailed = 30; // Server accepted a reg request but failed to perform the registration + RespActivationAccepted = 31; // Server accepted a reg user activation token + RespActivationFailed = 32; // Server didn't accept a reg user activation token + RespRegistrationAcceptedNeedsActivation = + 33; // Server accepted cient registration, but it will need token activation RespClientIdRequired = 34; // Server requires client to generate and send its client id before allowing access RespClientUpdateRequired = 35; // Client is missing features that the server is requiring - RespServerFull = 36; // Server user limit reached - RespEmailBlackListed = 37; // Server has blacklisted the email address provided for registration + RespServerFull = 36; // Server user limit reached + RespEmailBlackListed = 37; // Server has blacklisted the email address provided for registration } enum ResponseType { JOIN_ROOM = 1000; @@ -64,6 +66,6 @@ message Response { } required uint64 cmd_id = 1; optional ResponseCode response_code = 2; - + extensions 100 to max; } diff --git a/common/pb/response_adjust_mod.proto b/common/pb/response_adjust_mod.proto index c91493f8f..948eec1ba 100644 --- a/common/pb/response_adjust_mod.proto +++ b/common/pb/response_adjust_mod.proto @@ -1,7 +1,7 @@ syntax = "proto2"; import "response.proto"; -message Response_AdjustMod{ +message Response_AdjustMod { extend Response { optional Response_AdjustMod ext = 1011; } diff --git a/common/pb/response_ban_history.proto b/common/pb/response_ban_history.proto index de587d6d9..696004152 100644 --- a/common/pb/response_ban_history.proto +++ b/common/pb/response_ban_history.proto @@ -2,7 +2,7 @@ syntax = "proto2"; import "response.proto"; import "serverinfo_ban.proto"; -message Response_BanHistory{ +message Response_BanHistory { extend Response { optional Response_BanHistory ext = 1012; } diff --git a/common/pb/response_get_games_of_user.proto b/common/pb/response_get_games_of_user.proto index f179bcc62..dd0ddd160 100644 --- a/common/pb/response_get_games_of_user.proto +++ b/common/pb/response_get_games_of_user.proto @@ -10,4 +10,3 @@ message Response_GetGamesOfUser { repeated ServerInfo_Room room_list = 1; repeated ServerInfo_Game game_list = 2; } - diff --git a/common/pb/response_get_user_info.proto b/common/pb/response_get_user_info.proto index fbfaaf217..0332a07ad 100644 --- a/common/pb/response_get_user_info.proto +++ b/common/pb/response_get_user_info.proto @@ -8,5 +8,3 @@ message Response_GetUserInfo { } optional ServerInfo_User user_info = 1; } - - diff --git a/common/pb/response_list_users.proto b/common/pb/response_list_users.proto index 825ae6f7f..d653521dd 100644 --- a/common/pb/response_list_users.proto +++ b/common/pb/response_list_users.proto @@ -8,4 +8,3 @@ message Response_ListUsers { } repeated ServerInfo_User user_list = 1; } - diff --git a/common/pb/response_replay_download.proto b/common/pb/response_replay_download.proto index d263f8d9b..1805280c1 100644 --- a/common/pb/response_replay_download.proto +++ b/common/pb/response_replay_download.proto @@ -7,4 +7,3 @@ message Response_ReplayDownload { } optional bytes replay_data = 1; } - diff --git a/common/pb/response_viewlog_history.proto b/common/pb/response_viewlog_history.proto index 2572d7d22..2ec0aff28 100644 --- a/common/pb/response_viewlog_history.proto +++ b/common/pb/response_viewlog_history.proto @@ -2,7 +2,7 @@ syntax = "proto2"; import "response.proto"; import "serverinfo_chat_message.proto"; -message Response_ViewLogHistory{ +message Response_ViewLogHistory { extend Response { optional Response_ViewLogHistory ext = 1015; } diff --git a/common/pb/response_warn_history.proto b/common/pb/response_warn_history.proto index a43180f3b..8108ececf 100644 --- a/common/pb/response_warn_history.proto +++ b/common/pb/response_warn_history.proto @@ -2,7 +2,7 @@ syntax = "proto2"; import "response.proto"; import "serverinfo_warning.proto"; -message Response_WarnHistory{ +message Response_WarnHistory { extend Response { optional Response_WarnHistory ext = 1013; } diff --git a/common/pb/response_warn_list.proto b/common/pb/response_warn_list.proto index 8d893103b..d48352529 100644 --- a/common/pb/response_warn_list.proto +++ b/common/pb/response_warn_list.proto @@ -1,7 +1,7 @@ syntax = "proto2"; import "response.proto"; -message Response_WarnList{ +message Response_WarnList { extend Response { optional Response_WarnList ext = 1014; } diff --git a/common/pb/room_commands.proto b/common/pb/room_commands.proto index d1dfde9f4..57e27dff1 100644 --- a/common/pb/room_commands.proto +++ b/common/pb/room_commands.proto @@ -37,6 +37,7 @@ message Command_CreateGame { optional bool spectators_see_everything = 9; repeated uint32 game_type_ids = 10; optional bool join_as_judge = 11; + optional bool join_as_spectator = 12; } message Command_JoinGame { diff --git a/common/pb/server_message.proto b/common/pb/server_message.proto index a9330ab3c..50bbfb0fd 100644 --- a/common/pb/server_message.proto +++ b/common/pb/server_message.proto @@ -12,7 +12,7 @@ message ServerMessage { ROOM_EVENT = 3; } optional MessageType message_type = 1; - + optional Response response = 2; optional SessionEvent session_event = 3; optional GameEventContainer game_event_container = 4; diff --git a/common/pb/serverinfo_ban.proto b/common/pb/serverinfo_ban.proto index 2ad7ffd4d..241fc3b50 100644 --- a/common/pb/serverinfo_ban.proto +++ b/common/pb/serverinfo_ban.proto @@ -3,10 +3,10 @@ syntax = "proto2"; * Historical ban information stored in the ban table */ message ServerInfo_Ban { - required string admin_id = 1; // id of the staff member placing the ban - required string admin_name = 2; // name of the staff member placing the ban - required string ban_time = 3; // start time of the ban - required string ban_length = 4; // amount of time in minutes the ban is for - optional string ban_reason = 5; // reason seen only by moderation staff + required string admin_id = 1; // id of the staff member placing the ban + required string admin_name = 2; // name of the staff member placing the ban + required string ban_time = 3; // start time of the ban + required string ban_length = 4; // amount of time in minutes the ban is for + optional string ban_reason = 5; // reason seen only by moderation staff optional string visible_reason = 6; // reason shown to the user } diff --git a/common/pb/serverinfo_chat_message.proto b/common/pb/serverinfo_chat_message.proto index 27d4386e5..d47ec4fc7 100644 --- a/common/pb/serverinfo_chat_message.proto +++ b/common/pb/serverinfo_chat_message.proto @@ -5,12 +5,12 @@ syntax = "proto2"; * These communications are also stored in the DB log table. */ message ServerInfo_ChatMessage { - optional string time = 1; // time chat was sent - optional string sender_id = 2; // id of sender + optional string time = 1; // time chat was sent + optional string sender_id = 2; // id of sender optional string sender_name = 3; // name of sender - optional string sender_ip = 4; // ip of sender - optional string message = 5; // message + optional string sender_ip = 4; // ip of sender + optional string message = 5; // message optional string target_type = 6; // target type (room,game,chat) - optional string target_id = 7; // id of target + optional string target_id = 7; // id of target optional string target_name = 8; // name of target } diff --git a/common/pb/serverinfo_gametype.proto b/common/pb/serverinfo_gametype.proto index a135b89b6..b73841be4 100644 --- a/common/pb/serverinfo_gametype.proto +++ b/common/pb/serverinfo_gametype.proto @@ -3,4 +3,3 @@ message ServerInfo_GameType { optional sint32 game_type_id = 1; optional string description = 2; }; - diff --git a/common/pb/serverinfo_replay_match.proto b/common/pb/serverinfo_replay_match.proto index 05abeb945..7fdc6471b 100644 --- a/common/pb/serverinfo_replay_match.proto +++ b/common/pb/serverinfo_replay_match.proto @@ -3,7 +3,7 @@ import "serverinfo_replay.proto"; message ServerInfo_ReplayMatch { repeated ServerInfo_Replay replay_list = 1; - + optional sint32 game_id = 2 [default = -1]; optional string room_name = 3; optional uint32 time_started = 4; diff --git a/common/pb/serverinfo_warning.proto b/common/pb/serverinfo_warning.proto index f2efed576..20287be06 100644 --- a/common/pb/serverinfo_warning.proto +++ b/common/pb/serverinfo_warning.proto @@ -3,8 +3,8 @@ syntax = "proto2"; * Historical warning information stored in the warnings table */ message ServerInfo_Warning { - optional string user_name = 1; // name of user being warned + optional string user_name = 1; // name of user being warned optional string admin_name = 2; // name of the moderator making the warning - optional string reason = 3; // type of warning being placed - optional string time_of = 4; // time of warning + optional string reason = 3; // type of warning being placed + optional string time_of = 4; // time of warning } diff --git a/common/pb/serverinfo_zone.proto b/common/pb/serverinfo_zone.proto index d359613c4..0efa2d9be 100644 --- a/common/pb/serverinfo_zone.proto +++ b/common/pb/serverinfo_zone.proto @@ -11,7 +11,7 @@ message ServerInfo_Zone { // setting beingLookedAt to true. // Cards in a zone with the type HiddenZone are referenced by their // list index, whereas cards in any other zone are referenced by their ids. - + PrivateZone = 0; PublicZone = 1; HiddenZone = 2; @@ -21,5 +21,8 @@ message ServerInfo_Zone { optional bool with_coords = 3; optional sint32 card_count = 4; repeated ServerInfo_Card card_list = 5; + // Reveal top card to all players. optional bool always_reveal_top_card = 10; + // reveal top card to the owner. + optional bool always_look_at_top_card = 11; } diff --git a/common/server.cpp b/common/server.cpp index 401ce5f30..75538c3ac 100644 --- a/common/server.cpp +++ b/common/server.cpp @@ -85,6 +85,15 @@ AuthenticationResult Server::loginUser(Server_ProtocolHandler *session, QString &clientVersion, QString & /* connectionType */) { + bool hasClientId = false; + if (clientid.isEmpty()) { + // client id is empty, either out dated client or client has been modified + if (getClientIDRequiredEnabled()) + return ClientIdRequired; + } else { + hasClientId = true; + } + if (name.size() > 35) name = name.left(35); @@ -164,15 +173,10 @@ AuthenticationResult Server::loginUser(Server_ProtocolHandler *session, event.mutable_user_info()->CopyFrom(session->copyUserInfo(true, true, true)); locker.unlock(); - if (clientid.isEmpty()) { - // client id is empty, either out dated client or client has been modified - if (getClientIDRequiredEnabled()) - return ClientIdRequired; - } else { + if (hasClientId) { // update users database table with client id databaseInterface->updateUsersClientID(name, clientid); } - databaseInterface->updateUsersLastLoginData(name, clientVersion); se = Server_ProtocolHandler::prepareSessionEvent(event); sendIsl_SessionEvent(*se); diff --git a/common/server.h b/common/server.h index 6e62fb6cd..8c06182a0 100644 --- a/common/server.h +++ b/common/server.h @@ -155,7 +155,7 @@ public: } virtual int getMaxGamesPerUser() const { - return 0; + return -1; } virtual int getCommandCountingInterval() const { diff --git a/common/server_cardzone.cpp b/common/server_cardzone.cpp index 6d519a590..a35bd218d 100644 --- a/common/server_cardzone.cpp +++ b/common/server_cardzone.cpp @@ -32,7 +32,7 @@ Server_CardZone::Server_CardZone(Server_Player *_player, bool _has_coords, ServerInfo_Zone::ZoneType _type) : player(_player), name(_name), has_coords(_has_coords), type(_type), cardsBeingLookedAt(0), - alwaysRevealTopCard(false) + alwaysRevealTopCard(false), alwaysLookAtTopCard(false) { } @@ -305,6 +305,7 @@ void Server_CardZone::getInfo(ServerInfo_Zone *info, Server_Player *playerWhosAs info->set_with_coords(has_coords); info->set_card_count(cards.size()); info->set_always_reveal_top_card(alwaysRevealTopCard); + info->set_always_look_at_top_card(alwaysLookAtTopCard); if ((((playerWhosAsking == player) || omniscient) && (type != ServerInfo_Zone::HiddenZone)) || ((playerWhosAsking != player) && (type == ServerInfo_Zone::PublicZone))) { QListIterator cardIterator(cards); diff --git a/common/server_cardzone.h b/common/server_cardzone.h index b9fa8ba99..536ca58a7 100644 --- a/common/server_cardzone.h +++ b/common/server_cardzone.h @@ -42,6 +42,7 @@ private: int cardsBeingLookedAt; QSet playersWithWritePermission; bool alwaysRevealTopCard; + bool alwaysLookAtTopCard; QList cards; QMap> coordinateMap; // y -> (x -> card) QMap> freePilesMap; // y -> (cardName -> x) @@ -108,6 +109,14 @@ public: { alwaysRevealTopCard = _alwaysRevealTopCard; } + bool getAlwaysLookAtTopCard() const + { + return alwaysLookAtTopCard; + } + void setAlwaysLookAtTopCard(bool _alwaysLookAtTopCard) + { + alwaysLookAtTopCard = _alwaysLookAtTopCard; + } }; #endif diff --git a/common/server_game.cpp b/common/server_game.cpp index 3996c69b8..46c67f8ea 100644 --- a/common/server_game.cpp +++ b/common/server_game.cpp @@ -69,7 +69,11 @@ Server_Game::Server_Game(const ServerInfo_User &_creatorInfo, spectatorsNeedPassword(_spectatorsNeedPassword), spectatorsCanTalk(_spectatorsCanTalk), spectatorsSeeEverything(_spectatorsSeeEverything), inactivityCounter(0), startTimeOfThisGame(0), secondsElapsed(0), firstGameStarted(false), turnOrderReversed(false), startTime(QDateTime::currentDateTime()), +#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) + gameMutex() +#else gameMutex(QMutex::Recursive) +#endif { currentReplay = new GameReplay; currentReplay->set_replay_id(room->getServer()->getDatabaseInterface()->getNextReplayId()); @@ -93,9 +97,9 @@ Server_Game::~Server_Game() gameClosed = true; sendGameEventContainer(prepareGameEvent(Event_GameClosed(), -1)); - QMapIterator playerIterator(players); - while (playerIterator.hasNext()) - playerIterator.next().value()->prepareDestroy(); + for (Server_Player *player : players.values()) { + player->prepareDestroy(); + } players.clear(); room->removeGame(this); @@ -131,9 +135,9 @@ void Server_Game::storeGameInformation() for (int i = gameInfo.game_types_size() - 1; i >= 0; --i) gameTypes.append(allGameTypes[gameInfo.game_types(i)]); - QSetIterator playerIterator(allPlayersEver); - while (playerIterator.hasNext()) - replayMatchInfo->add_player_names(playerIterator.next().toStdString()); + for (auto playerName : allPlayersEver) { + replayMatchInfo->add_player_names(playerName.toStdString()); + } for (int i = 0; i < replayList.size(); ++i) { ServerInfo_Replay *replayInfo = replayMatchInfo->add_replay_list(); @@ -142,23 +146,21 @@ void Server_Game::storeGameInformation() replayInfo->set_duration(replayList[i]->duration_seconds()); } - QSet allUsersInGame = allPlayersEver + allSpectatorsEver; - QSetIterator allUsersIterator(allUsersInGame); - SessionEvent *sessionEvent = Server_ProtocolHandler::prepareSessionEvent(replayEvent); Server *server = room->getServer(); server->clientsLock.lockForRead(); - while (allUsersIterator.hasNext()) { - Server_AbstractUserInterface *userHandler = server->findUser(allUsersIterator.next()); + for (auto userName : allPlayersEver + allSpectatorsEver) { + Server_AbstractUserInterface *userHandler = server->findUser(userName); if (userHandler && server->getStoreReplaysEnabled()) userHandler->sendProtocolItem(*sessionEvent); } server->clientsLock.unlock(); delete sessionEvent; - if (server->getStoreReplaysEnabled()) + if (server->getStoreReplaysEnabled()) { server->getDatabaseInterface()->storeGameInformation(room->getName(), gameTypes, gameInfo, allPlayersEver, allSpectatorsEver, replayList); + } } void Server_Game::pingClockTimeout() @@ -169,26 +171,30 @@ void Server_Game::pingClockTimeout() GameEventStorage ges; ges.setGameEventContext(Context_PingChanged()); - QList pingList; - QMapIterator playerIterator(players); bool allPlayersInactive = true; int playerCount = 0; - while (playerIterator.hasNext()) { - Server_Player *player = playerIterator.next().value(); - if (!player->getSpectator()) + for (auto *player : players) { + if (player == nullptr) + continue; + + if (!player->getSpectator()) { ++playerCount; + } - const int oldPingTime = player->getPingTime(); - player->playerMutex.lock(); + int oldPingTime = player->getPingTime(); int newPingTime; - if (player->getUserInterface()) - newPingTime = player->getUserInterface()->getLastCommandTime(); - else - newPingTime = -1; - player->playerMutex.unlock(); + { + QMutexLocker playerMutexLocker(&player->playerMutex); + if (player->getUserInterface()) { + newPingTime = player->getUserInterface()->getLastCommandTime(); + } else { + newPingTime = -1; + } + } - if ((newPingTime != -1) && !player->getSpectator()) + if ((newPingTime != -1) && (!player->getSpectator() || player->getPlayerId() == hostId)) { allPlayersInactive = false; + } if ((abs(oldPingTime - newPingTime) > 1) || ((newPingTime == -1) && (oldPingTime != -1)) || ((newPingTime != -1) && (oldPingTime == -1))) { @@ -203,21 +209,23 @@ void Server_Game::pingClockTimeout() const int maxTime = room->getServer()->getMaxGameInactivityTime(); if (allPlayersInactive) { - if (((++inactivityCounter >= maxTime) && (maxTime > 0)) || (playerCount < maxPlayers)) + if (((maxTime > 0) && (++inactivityCounter >= maxTime)) || (playerCount < maxPlayers)) { deleteLater(); - } else + } + } else { inactivityCounter = 0; + } } int Server_Game::getPlayerCount() const { QMutexLocker locker(&gameMutex); - QMapIterator playerIterator(players); int result = 0; - while (playerIterator.hasNext()) - if (!playerIterator.next().value()->getSpectator()) + for (Server_Player *player : players.values()) { + if (!player->getSpectator()) ++result; + } return result; } @@ -225,11 +233,11 @@ int Server_Game::getSpectatorCount() const { QMutexLocker locker(&gameMutex); - QMapIterator playerIterator(players); int result = 0; - while (playerIterator.hasNext()) - if (playerIterator.next().value()->getSpectator()) + for (Server_Player *player : players.values()) { + if (player->getSpectator()) ++result; + } return result; } @@ -246,9 +254,9 @@ void Server_Game::createGameStateChangedEvent(Event_GameStateChanged *event, } else event->set_game_started(false); - QMapIterator playerIterator(players); - while (playerIterator.hasNext()) - playerIterator.next().value()->getInfo(event->add_player_list(), playerWhosAsking, omniscient, withUserInfo); + for (Server_Player *otherPlayer : players.values()) { + otherPlayer->getInfo(event->add_player_list(), playerWhosAsking, omniscient, withUserInfo); + } } void Server_Game::sendGameStateToPlayers() @@ -273,9 +281,7 @@ void Server_Game::sendGameStateToPlayers() createGameStateChangedEvent(&spectatorEvent, 0, false, false); // send game state info to clients according to their role in the game - QMapIterator playerIterator(players); - while (playerIterator.hasNext()) { - Server_Player *player = playerIterator.next().value(); + for (Server_Player *player : players.values()) { GameEventContainer *gec; if (player->getSpectator()) gec = prepareGameEvent(spectatorEvent, -1); @@ -297,23 +303,17 @@ void Server_Game::doStartGameIfReady() if (getPlayerCount() < maxPlayers) return; - QMapIterator playerIterator(players); - while (playerIterator.hasNext()) { - Server_Player *p = playerIterator.next().value(); - if (!p->getReadyStart() && !p->getSpectator()) + for (Server_Player *player : players.values()) { + if (!player->getReadyStart() && !player->getSpectator()) return; } - playerIterator.toFront(); - while (playerIterator.hasNext()) { - Server_Player *p = playerIterator.next().value(); - if (!p->getSpectator()) - p->setupZones(); + for (Server_Player *player : players.values()) { + if (!player->getSpectator()) + player->setupZones(); } gameStarted = true; - playerIterator.toFront(); - while (playerIterator.hasNext()) { - Server_Player *player = playerIterator.next().value(); + for (Server_Player *player : players.values()) { player->setConceded(false); player->setReadyStart(false); } @@ -363,11 +363,9 @@ void Server_Game::stopGameIfFinished() { QMutexLocker locker(&gameMutex); - QMapIterator playerIterator(players); int playing = 0; - while (playerIterator.hasNext()) { - Server_Player *p = playerIterator.next().value(); - if (!p->getConceded() && !p->getSpectator()) + for (Server_Player *player : players.values()) { + if (!player->getConceded() && !player->getSpectator()) ++playing; } if (playing > 1) @@ -375,11 +373,9 @@ void Server_Game::stopGameIfFinished() gameStarted = false; - playerIterator.toFront(); - while (playerIterator.hasNext()) { - Server_Player *p = playerIterator.next().value(); - p->clearZones(); - p->setConceded(false); + for (Server_Player *player : players.values()) { + player->clearZones(); + player->setConceded(false); } sendGameStateToPlayers(); @@ -400,11 +396,9 @@ Response::ResponseCode Server_Game::checkJoin(ServerInfo_User *user, bool asJudge) { Server_DatabaseInterface *databaseInterface = room->getServer()->getDatabaseInterface(); - { - QMapIterator playerIterator(players); - while (playerIterator.hasNext()) - if (playerIterator.next().value()->getUserInfo()->name() == user->name()) - return Response::RespContextError; + for (Server_Player *player : players.values()) { + if (player->getUserInfo()->name() == user->name()) + return Response::RespContextError; } if (asJudge && !(user->user_level() & ServerInfo_User::IsJudge)) { @@ -437,10 +431,10 @@ bool Server_Game::containsUser(const QString &userName) const { QMutexLocker locker(&gameMutex); - QMapIterator playerIterator(players); - while (playerIterator.hasNext()) - if (playerIterator.next().value()->getUserInfo()->name() == userName.toStdString()) + for (Server_Player *player : players.values()) { + if (player->getUserInfo()->name() == userName.toStdString()) return true; + } return false; } @@ -462,14 +456,18 @@ void Server_Game::addPlayer(Server_AbstractUserInterface *userInterface, sendGameEventContainer(prepareGameEvent(joinEvent, -1)); const QString playerName = QString::fromStdString(newPlayer->getUserInfo()->name()); - if (spectator) - allSpectatorsEver.insert(playerName); - else - allPlayersEver.insert(playerName); players.insert(newPlayer->getPlayerId(), newPlayer); - if (newPlayer->getUserInfo()->name() == creatorInfo->name()) { - hostId = newPlayer->getPlayerId(); - sendGameEventContainer(prepareGameEvent(Event_GameHostChanged(), hostId)); + if (spectator) { + allSpectatorsEver.insert(playerName); + } else { + allPlayersEver.insert(playerName); + + // if the original creator of the game joins, give them host status back + // FIXME: transferring host to spectators has side effects + if (newPlayer->getUserInfo()->name() == creatorInfo->name()) { + hostId = newPlayer->getPlayerId(); + sendGameEventContainer(prepareGameEvent(Event_GameHostChanged(), hostId)); + } } if (broadcastUpdate) { @@ -509,26 +507,24 @@ void Server_Game::removePlayer(Server_Player *player, Event_Leave::LeaveReason r bool spectator = player->getSpectator(); player->prepareDestroy(); - if (!getPlayerCount()) { - gameClosed = true; - deleteLater(); - return; - } else if (!spectator) { - if (playerHost) { - int newHostId = -1; - QMapIterator playerIterator(players); - while (playerIterator.hasNext()) { - Server_Player *p = playerIterator.next().value(); - if (!p->getSpectator()) { - newHostId = p->getPlayerId(); - break; - } - } - if (newHostId != -1) { - hostId = newHostId; - sendGameEventContainer(prepareGameEvent(Event_GameHostChanged(), hostId)); + if (playerHost) { + int newHostId = -1; + for (Server_Player *otherPlayer : players.values()) { + if (!otherPlayer->getSpectator()) { + newHostId = otherPlayer->getPlayerId(); + break; } } + if (newHostId != -1) { + hostId = newHostId; + sendGameEventContainer(prepareGameEvent(Event_GameHostChanged(), hostId)); + } else { + gameClosed = true; + deleteLater(); + return; + } + } + if (!spectator) { stopGameIfFinished(); if (gameStarted && playerActive) nextTurn(); @@ -549,10 +545,8 @@ void Server_Game::removeArrowsRelatedToPlayer(GameEventStorage &ges, Server_Play // Remove all arrows of other players pointing to the player being removed or to one of his cards. // Also remove all arrows starting at one of his cards. This is necessary since players can create // arrows that start at another person's cards. - QMapIterator playerIterator(players); - while (playerIterator.hasNext()) { - Server_Player *p = playerIterator.next().value(); - QList arrows = p->getArrows().values(); + for (Server_Player *otherPlayer : players.values()) { + QList arrows = otherPlayer->getArrows().values(); QList toDelete; for (int i = 0; i < arrows.size(); ++i) { Server_Arrow *a = arrows[i]; @@ -570,9 +564,9 @@ void Server_Game::removeArrowsRelatedToPlayer(GameEventStorage &ges, Server_Play for (int i = 0; i < toDelete.size(); ++i) { Event_DeleteArrow event; event.set_arrow_id(toDelete[i]->getId()); - ges.enqueueGameEvent(event, p->getPlayerId()); + ges.enqueueGameEvent(event, otherPlayer->getPlayerId()); - p->deleteArrow(toDelete[i]->getId()); + otherPlayer->deleteArrow(toDelete[i]->getId()); } } } @@ -581,16 +575,20 @@ void Server_Game::unattachCards(GameEventStorage &ges, Server_Player *player) { QMutexLocker locker(&gameMutex); - QMapIterator zoneIterator(player->getZones()); - while (zoneIterator.hasNext()) { - Server_CardZone *zone = zoneIterator.next().value(); - for (int i = 0; i < zone->getCards().size(); ++i) { - Server_Card *card = zone->getCards().at(i); - + for (auto zone : player->getZones()) { + for (auto card : zone->getCards()) { // Make a copy of the list because the original one gets modified during the loop QList attachedCards = card->getAttachedCards(); - for (int i = 0; i < attachedCards.size(); ++i) - attachedCards[i]->getZone()->getPlayer()->unattachCard(ges, attachedCards[i]); + for (Server_Card *attachedCard : attachedCards) { + auto otherPlayer = attachedCard->getZone()->getPlayer(); + // do not modify the current player's zone! + // this would cause the current card iterator to be invalidated! + // we only have to return cards owned by other players + // because the current player is leaving the game anyway + if (otherPlayer != player) { + otherPlayer->unattachCard(ges, attachedCard); + } + } } } } @@ -629,9 +627,7 @@ void Server_Game::setActivePhase(int _activePhase) { QMutexLocker locker(&gameMutex); - QMapIterator playerIterator(players); - while (playerIterator.hasNext()) { - Server_Player *player = playerIterator.next().value(); + for (Server_Player *player : players.values()) { QList toDelete = player->getArrows().values(); for (int i = 0; i < toDelete.size(); ++i) { Server_Arrow *a = toDelete[i]; @@ -701,10 +697,9 @@ void Server_Game::createGameJoinedEvent(Server_Player *player, ResponseContainer event2.set_active_player_id(activePlayer); event2.set_active_phase(activePhase); - QMapIterator playerIterator(players); - while (playerIterator.hasNext()) - playerIterator.next().value()->getInfo(event2.add_player_list(), player, - player->getSpectator() && spectatorsSeeEverything, true); + for (Server_Player *player : players.values()) { + player->getInfo(event2.add_player_list(), player, player->getSpectator() && spectatorsSeeEverything, true); + } rc.enqueuePostResponseItem(ServerMessage::GAME_EVENT_CONTAINER, prepareGameEvent(event2, -1)); } @@ -716,14 +711,12 @@ void Server_Game::sendGameEventContainer(GameEventContainer *cont, QMutexLocker locker(&gameMutex); cont->set_game_id(gameId); - QMapIterator playerIterator(players); - while (playerIterator.hasNext()) { - Server_Player *p = playerIterator.next().value(); + for (Server_Player *player : players.values()) { const bool playerPrivate = - (p->getPlayerId() == privatePlayerId) || (p->getSpectator() && spectatorsSeeEverything); + (player->getPlayerId() == privatePlayerId) || (player->getSpectator() && spectatorsSeeEverything); if ((recipients.testFlag(GameEventStorageItem::SendToPrivate) && playerPrivate) || (recipients.testFlag(GameEventStorageItem::SendToOthers) && !playerPrivate)) - p->sendGameEvent(*cont); + player->sendGameEvent(*cont); } if (recipients.testFlag(GameEventStorageItem::SendToPrivate)) { cont->set_seconds_elapsed(secondsElapsed - startTimeOfThisGame); @@ -756,11 +749,12 @@ void Server_Game::getInfo(ServerInfo_Game &result) const result.set_room_id(room->getId()); result.set_game_id(gameId); - if (gameClosed) + if (gameClosed) { result.set_closed(true); - else { - for (int i = 0; i < gameTypes.size(); ++i) - result.add_game_types(gameTypes[i]); + } else { + for (auto type : gameTypes) { + result.add_game_types(type); + } result.set_max_players(getMaxPlayers()); result.set_description(getDescription().toStdString()); diff --git a/common/server_game.h b/common/server_game.h index 905f7c182..133089405 100644 --- a/common/server_game.h +++ b/common/server_game.h @@ -88,7 +88,11 @@ private slots: void doStartGameIfReady(); public: +#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) + mutable QRecursiveMutex gameMutex; +#else mutable QMutex gameMutex; +#endif Server_Game(const ServerInfo_User &_creatorInfo, int _gameId, const QString &_description, diff --git a/common/server_player.cpp b/common/server_player.cpp index 35debaa42..db9f1fa18 100644 --- a/common/server_player.cpp +++ b/common/server_player.cpp @@ -34,7 +34,6 @@ #include "pb/command_set_sideboard_lock.pb.h" #include "pb/command_set_sideboard_plan.pb.h" #include "pb/command_shuffle.pb.h" -#include "pb/command_stop_dump_zone.pb.h" #include "pb/command_undo_draw.pb.h" #include "pb/context_concede.pb.h" #include "pb/context_connection_state_changed.pb.h" @@ -65,7 +64,6 @@ #include "pb/event_set_card_counter.pb.h" #include "pb/event_set_counter.pb.h" #include "pb/event_shuffle.pb.h" -#include "pb/event_stop_dump_zone.pb.h" #include "pb/response.pb.h" #include "pb/response_deck_download.pb.h" #include "pb/response_dump_zone.pb.h" @@ -149,9 +147,9 @@ void Server_Player::setupZones() // ------------------------------------------------------------------ // Create zones - Server_CardZone *deckZone = new Server_CardZone(this, "deck", false, ServerInfo_Zone::HiddenZone); + auto *deckZone = new Server_CardZone(this, "deck", false, ServerInfo_Zone::HiddenZone); addZone(deckZone); - Server_CardZone *sbZone = new Server_CardZone(this, "sb", false, ServerInfo_Zone::HiddenZone); + auto *sbZone = new Server_CardZone(this, "sb", false, ServerInfo_Zone::HiddenZone); addZone(sbZone); addZone(new Server_CardZone(this, "table", true, ServerInfo_Zone::PublicZone)); addZone(new Server_CardZone(this, "hand", false, ServerInfo_Zone::PrivateZone)); @@ -318,18 +316,41 @@ Response::ResponseCode Server_Player::drawCards(GameEventStorage &ges, int numbe ges.enqueueGameEvent(eventPrivate, playerId, GameEventStorageItem::SendToPrivate, playerId); ges.enqueueGameEvent(eventOthers, playerId, GameEventStorageItem::SendToOthers); - if (deckZone->getAlwaysRevealTopCard() && !deckZone->getCards().isEmpty()) { - Event_RevealCards revealEvent; - revealEvent.set_zone_name(deckZone->getName().toStdString()); - revealEvent.set_card_id(0); - deckZone->getCards().first()->getInfo(revealEvent.add_cards()); - - ges.enqueueGameEvent(revealEvent, playerId); - } + revealTopCardIfNeeded(deckZone, ges); return Response::RespOk; } +void Server_Player::revealTopCardIfNeeded(Server_CardZone *zone, GameEventStorage &ges) +{ + if (zone->getCards().isEmpty()) { + return; + } + if (zone->getAlwaysRevealTopCard()) { + Event_RevealCards revealEvent; + revealEvent.set_zone_name(zone->getName().toStdString()); + revealEvent.set_card_id(0); + zone->getCards().first()->getInfo(revealEvent.add_cards()); + + ges.enqueueGameEvent(revealEvent, playerId); + return; + } + if (zone->getAlwaysLookAtTopCard()) { + Event_DumpZone dumpEvent; + dumpEvent.set_zone_owner_id(playerId); + dumpEvent.set_zone_name(zone->getName().toStdString()); + dumpEvent.set_number_cards(1); + ges.enqueueGameEvent(dumpEvent, playerId, GameEventStorageItem::SendToOthers); + + Event_RevealCards revealEvent; + revealEvent.set_zone_name(zone->getName().toStdString()); + revealEvent.set_number_of_cards(1); + revealEvent.set_card_id(0); + zone->getCards().first()->getInfo(revealEvent.add_cards()); + ges.enqueueGameEvent(revealEvent, playerId, GameEventStorageItem::SendToPrivate, playerId); + } +} + class Server_Player::MoveCardCompareFunctor { private: @@ -424,27 +445,16 @@ Response::ResponseCode Server_Player::moveCard(GameEventStorage &ges, int originalPosition = cardsToMove[cardIndex].second; int position = startzone->removeCard(card); - if (startzone->getName() == "hand") { - if (undoingDraw) { - lastDrawList.removeAt(lastDrawList.indexOf(card->getId())); - } else if (lastDrawList.contains(card->getId())) { - lastDrawList.clear(); - } - } - if ((startzone == targetzone) && !startzone->hasCoords()) { - if (!secondHalf && (originalPosition < x)) { - xIndex = -1; - secondHalf = true; - } else if (secondHalf) { - --xIndex; - } else { - ++xIndex; + // "Undo draw" should only remain valid if the just-drawn card stays within the user's hand (e.g., they only + // reorder their hand). If a just-drawn card leaves the hand then remove cards before it from the list + // (Ignore the case where the card is currently being un-drawn.) + if (startzone->getName() == "hand" && targetzone->getName() != "hand" && !undoingDraw) { + int index = lastDrawList.lastIndexOf(card->getId()); + if (index != -1) { + lastDrawList.erase(lastDrawList.begin(), lastDrawList.begin() + index); } - } else { - ++xIndex; } - int newX = x + xIndex; // Attachment relationships can be retained when moving a card onto the opponent's table if (startzone->getName() != targetzone->getName()) { @@ -485,6 +495,20 @@ Response::ResponseCode Server_Player::moveCard(GameEventStorage &ges, card->deleteLater(); } else { + if ((startzone == targetzone) && !startzone->hasCoords()) { + if (!secondHalf && (originalPosition < x)) { + xIndex = -1; + secondHalf = true; + } else if (secondHalf) { + --xIndex; + } else { + ++xIndex; + } + } else { + ++xIndex; + } + int newX = x + xIndex; + if (!targetzone->hasCoords()) { y = 0; card->resetState(); @@ -591,22 +615,12 @@ Response::ResponseCode Server_Player::moveCard(GameEventStorage &ges, setCardAttrHelper(ges, targetzone->getPlayer()->getPlayerId(), targetzone->getName(), card->getId(), AttrPT, ptString); } - } - if (startzone->getAlwaysRevealTopCard() && !startzone->getCards().isEmpty() && (originalPosition == 0)) { - Event_RevealCards revealEvent; - revealEvent.set_zone_name(startzone->getName().toStdString()); - revealEvent.set_card_id(0); - startzone->getCards().first()->getInfo(revealEvent.add_cards()); - - ges.enqueueGameEvent(revealEvent, playerId); - } - if (targetzone->getAlwaysRevealTopCard() && !targetzone->getCards().isEmpty() && (newX == 0)) { - Event_RevealCards revealEvent; - revealEvent.set_zone_name(targetzone->getName().toStdString()); - revealEvent.set_card_id(0); - targetzone->getCards().first()->getInfo(revealEvent.add_cards()); - - ges.enqueueGameEvent(revealEvent, playerId); + if (originalPosition == 0) { + revealTopCardIfNeeded(startzone, ges); + } + if (newX == 0) { + revealTopCardIfNeeded(targetzone, ges); + } } } if (undoingDraw) { @@ -974,15 +988,7 @@ Server_Player::cmdShuffle(const Command_Shuffle &cmd, ResponseContainer & /*rc*/ event.set_start(cmd.start()); event.set_end(cmd.end()); ges.enqueueGameEvent(event, playerId); - - if (zone->getAlwaysRevealTopCard() && !zone->getCards().isEmpty()) { - Event_RevealCards revealEvent; - revealEvent.set_zone_name(zone->getName().toStdString()); - revealEvent.set_card_id(0); - zone->getCards().first()->getInfo(revealEvent.add_cards()); - - ges.enqueueGameEvent(revealEvent, playerId); - } + revealTopCardIfNeeded(zone, ges); return Response::RespOk; } @@ -1340,7 +1346,7 @@ Server_Player::cmdCreateToken(const Command_CreateToken &cmd, ResponseContainer y = 0; } - Server_Card *card = new Server_Card(cardName, newCardId(), x, y); + auto *card = new Server_Card(cardName, newCardId(), x, y); card->moveToThread(thread()); card->setPT(QString::fromStdString(cmd.pt())); card->setColor(QString::fromStdString(cmd.color())); @@ -1624,8 +1630,8 @@ Server_Player::cmdCreateCounter(const Command_CreateCounter &cmd, ResponseContai return Response::RespContextError; } - Server_Counter *c = new Server_Counter(newCounterId(), QString::fromStdString(cmd.counter_name()), - cmd.counter_color(), cmd.radius(), cmd.value()); + auto *c = new Server_Counter(newCounterId(), QString::fromStdString(cmd.counter_name()), cmd.counter_color(), + cmd.radius(), cmd.value()); addCounter(c); Event_CreateCounter event; @@ -1822,36 +1828,6 @@ Server_Player::cmdDumpZone(const Command_DumpZone &cmd, ResponseContainer &rc, G return Response::RespOk; } -Response::ResponseCode -Server_Player::cmdStopDumpZone(const Command_StopDumpZone &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) -{ - if (!game->getGameStarted()) { - return Response::RespGameNotStarted; - } - if (conceded) { - return Response::RespContextError; - } - - Server_Player *otherPlayer = game->getPlayers().value(cmd.player_id()); - if (!otherPlayer) { - return Response::RespNameNotFound; - } - Server_CardZone *zone = otherPlayer->getZones().value(QString::fromStdString(cmd.zone_name())); - if (!zone) { - return Response::RespNameNotFound; - } - - if (zone->getType() == ServerInfo_Zone::HiddenZone) { - zone->setCardsBeingLookedAt(0); - - Event_StopDumpZone event; - event.set_zone_owner_id(cmd.player_id()); - event.set_zone_name(zone->getName().toStdString()); - ges.enqueueGameEvent(event, playerId); - } - return Response::RespOk; -} - Response::ResponseCode Server_Player::cmdRevealCards(const Command_RevealCards &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) { @@ -1977,27 +1953,31 @@ Response::ResponseCode Server_Player::cmdChangeZoneProperties(const Command_Chan Event_ChangeZoneProperties event; event.set_zone_name(cmd.zone_name()); - if (cmd.has_always_reveal_top_card()) { - if (zone->getAlwaysRevealTopCard() == cmd.always_reveal_top_card()) { - return Response::RespContextError; - } - zone->setAlwaysRevealTopCard(cmd.always_reveal_top_card()); - event.set_always_reveal_top_card(cmd.always_reveal_top_card()); - - ges.enqueueGameEvent(event, playerId); - - if (!zone->getCards().isEmpty() && cmd.always_reveal_top_card()) { - Event_RevealCards revealEvent; - revealEvent.set_zone_name(zone->getName().toStdString()); - revealEvent.set_card_id(0); - zone->getCards().first()->getInfo(revealEvent.add_cards()); - - ges.enqueueGameEvent(revealEvent, playerId); - } - return Response::RespOk; - } else { + // Neither value set -> error. + if (!cmd.has_always_look_at_top_card() && !cmd.has_always_reveal_top_card()) { return Response::RespContextError; } + + // Neither value changed -> error. + bool alwaysRevealChanged = + cmd.has_always_reveal_top_card() && zone->getAlwaysRevealTopCard() != cmd.always_reveal_top_card(); + bool alwaysLookAtTopChanged = + cmd.has_always_look_at_top_card() && zone->getAlwaysLookAtTopCard() != cmd.always_look_at_top_card(); + if (!alwaysRevealChanged && !alwaysLookAtTopChanged) { + return Response::RespContextError; + } + + if (cmd.has_always_reveal_top_card()) { + zone->setAlwaysRevealTopCard(cmd.always_reveal_top_card()); + event.set_always_reveal_top_card(cmd.always_reveal_top_card()); + } + if (cmd.has_always_look_at_top_card()) { + zone->setAlwaysLookAtTopCard(cmd.always_look_at_top_card()); + event.set_always_look_at_top_card(cmd.always_look_at_top_card()); + } + ges.enqueueGameEvent(event, playerId); + revealTopCardIfNeeded(zone, ges); + return Response::RespOk; } Response::ResponseCode @@ -2103,9 +2083,6 @@ Server_Player::processGameCommand(const GameCommand &command, ResponseContainer case GameCommand::DUMP_ZONE: return cmdDumpZone(command.GetExtension(Command_DumpZone::ext), rc, ges); break; - case GameCommand::STOP_DUMP_ZONE: - return cmdStopDumpZone(command.GetExtension(Command_StopDumpZone::ext), rc, ges); - break; case GameCommand::REVEAL_CARDS: return cmdRevealCards(command.GetExtension(Command_RevealCards::ext), rc, ges); break; diff --git a/common/server_player.h b/common/server_player.h index a7f3eaeb4..379bbc602 100644 --- a/common/server_player.h +++ b/common/server_player.h @@ -55,7 +55,6 @@ class Command_DelCounter; class Command_NextTurn; class Command_SetActivePhase; class Command_DumpZone; -class Command_StopDumpZone; class Command_RevealCards; class Command_ReverseTurn; class Command_MoveCard; @@ -84,6 +83,7 @@ private: bool readyStart; bool conceded; bool sideboardLocked; + void revealTopCardIfNeeded(Server_CardZone *zone, GameEventStorage &ges); public: mutable QMutex playerMutex; @@ -225,8 +225,6 @@ public: Response::ResponseCode cmdSetActivePhase(const Command_SetActivePhase &cmd, ResponseContainer &rc, GameEventStorage &ges); Response::ResponseCode cmdDumpZone(const Command_DumpZone &cmd, ResponseContainer &rc, GameEventStorage &ges); - Response::ResponseCode - cmdStopDumpZone(const Command_StopDumpZone &cmd, ResponseContainer &rc, GameEventStorage &ges); Response::ResponseCode cmdRevealCards(const Command_RevealCards &cmd, ResponseContainer &rc, GameEventStorage &ges); Response::ResponseCode cmdReverseTurn(const Command_ReverseTurn & /*cmd*/, ResponseContainer & /*rc*/, GameEventStorage &ges); diff --git a/common/server_protocolhandler.cpp b/common/server_protocolhandler.cpp index 96715d4ea..d723bd634 100644 --- a/common/server_protocolhandler.cpp +++ b/common/server_protocolhandler.cpp @@ -286,11 +286,13 @@ Response::ResponseCode Server_ProtocolHandler::processGameCommandContainer(const if (!antifloodCommandsWhiteList.contains((GameCommand::GameCommandType)getPbExtension(sc))) ++commandCountOverTime[0]; - for (int i = 0; i < commandCountOverTime.size(); ++i) + for (int i = 0; i < commandCountOverTime.size(); ++i) { totalCount += commandCountOverTime[i]; + } - if (totalCount > maxCommandCountPerInterval) + if (maxCommandCountPerInterval > 0 && totalCount > maxCommandCountPerInterval) { return Response::RespChatFlood; + } } Response::ResponseCode resp = player->processGameCommand(sc, rc, ges); @@ -733,12 +735,16 @@ Server_ProtocolHandler::cmdRoomSay(const Command_RoomSay &cmd, Server_Room *room if (messageCountOverTime.isEmpty()) messageCountOverTime.prepend(0); ++messageCountOverTime[0]; - for (int i = 0; i < messageCountOverTime.size(); ++i) + for (int i = 0; i < messageCountOverTime.size(); ++i) { totalCount += messageCountOverTime[i]; + } - if ((totalSize > server->getMaxMessageSizePerInterval()) || - (totalCount > server->getMaxMessageCountPerInterval())) + int maxMessageSizePerInterval = server->getMaxMessageSizePerInterval(); + int maxMessageCountPerInterval = server->getMaxMessageCountPerInterval(); + if ((maxMessageSizePerInterval > 0 && totalSize > maxMessageSizePerInterval) || + (maxMessageCountPerInterval > 0 && totalCount > maxMessageCountPerInterval)) { return Response::RespChatFlood; + } } msg.replace(QChar('\n'), QChar(' ')); @@ -760,22 +766,28 @@ Server_ProtocolHandler::cmdCreateGame(const Command_CreateGame &cmd, Server_Room if (gameId == -1) return Response::RespInternalError; - if (server->getMaxGamesPerUser() > 0) - if (room->getGamesCreatedByUser(QString::fromStdString(userInfo->name())) >= server->getMaxGamesPerUser()) - return Response::RespContextError; - - if (cmd.join_as_judge() && !server->permitCreateGameAsJudge() && - !(userInfo->user_level() & ServerInfo_User::IsJudge)) { + auto level = userInfo->user_level(); + bool isJudge = level & ServerInfo_User::IsJudge; + int maxGames = server->getMaxGamesPerUser(); + bool asJudge = cmd.join_as_judge(); + bool asSpectator = cmd.join_as_spectator(); + // allow judges to open games as spectator without limit to facilitate bots etc, -1 means no limit + if (!(isJudge && asJudge && asSpectator) && maxGames >= 0 && + room->getGamesCreatedByUser(QString::fromStdString(userInfo->name())) >= maxGames) { return Response::RespContextError; } - QList gameTypes; - for (int i = cmd.game_type_ids_size() - 1; i >= 0; --i) - gameTypes.append(cmd.game_type_ids(i)); + // if a non judge user tries to create a game as judge while not permitted, instead create a normal game + if (asJudge && !(server->permitCreateGameAsJudge() || isJudge)) { + asJudge = false; + } - QString description = QString::fromStdString(cmd.description()); - if (description.size() > 60) - description = description.left(60); + QList gameTypes; + for (int i = cmd.game_type_ids_size() - 1; i >= 0; --i) { // FIXME: why are these iterated in reverse? + gameTypes.append(cmd.game_type_ids(i)); + } + + QString description = QString::fromStdString(cmd.description()).left(60); // When server doesn't permit registered users to exist, do not honor only-reg setting bool onlyRegisteredUsers = cmd.only_registered() && (server->permitUnregisteredUsers()); @@ -784,7 +796,7 @@ Server_ProtocolHandler::cmdCreateGame(const Command_CreateGame &cmd, Server_Room cmd.only_buddies(), onlyRegisteredUsers, cmd.spectators_allowed(), cmd.spectators_need_password(), cmd.spectators_can_talk(), cmd.spectators_see_everything(), room); - game->addPlayer(this, rc, false, cmd.join_as_judge(), false); + game->addPlayer(this, rc, asSpectator, asJudge, false); room->addGame(game); return Response::RespOk; diff --git a/common/server_response_containers.cpp b/common/server_response_containers.cpp index 415e4af96..766721bb4 100644 --- a/common/server_response_containers.cpp +++ b/common/server_response_containers.cpp @@ -55,9 +55,11 @@ void GameEventStorage::sendToGame(Server_Game *game) GameEventContainer *contPrivate = new GameEventContainer; GameEventContainer *contOthers = new GameEventContainer; + int id = privatePlayerId; if (forcedByJudge != -1) { contPrivate->set_forced_by_judge(forcedByJudge); contOthers->set_forced_by_judge(forcedByJudge); + id = forcedByJudge; } for (int i = 0; i < gameEventList.size(); ++i) { const GameEvent &event = gameEventList[i]->getGameEvent(); @@ -71,8 +73,8 @@ void GameEventStorage::sendToGame(Server_Game *game) contPrivate->mutable_context()->CopyFrom(*gameEventContext); contOthers->mutable_context()->CopyFrom(*gameEventContext); } - game->sendGameEventContainer(contPrivate, GameEventStorageItem::SendToPrivate, privatePlayerId); - game->sendGameEventContainer(contOthers, GameEventStorageItem::SendToOthers, privatePlayerId); + game->sendGameEventContainer(contPrivate, GameEventStorageItem::SendToPrivate, id); + game->sendGameEventContainer(contOthers, GameEventStorageItem::SendToOthers, id); } ResponseContainer::ResponseContainer(int _cmdId) : cmdId(_cmdId), responseExtension(0) diff --git a/common/server_room.cpp b/common/server_room.cpp index 369b5f78e..9569a15e8 100644 --- a/common/server_room.cpp +++ b/common/server_room.cpp @@ -243,8 +243,8 @@ Response::ResponseCode Server_Room::processJoinGameCommand(const Command_JoinGam // server->roomsMutex is always locked. QReadLocker roomGamesLocker(&gamesLock); - Server_Game *g = games.value(cmd.game_id()); - if (!g) { + Server_Game *game = games.value(cmd.game_id()); + if (!game) { if (externalGames.contains(cmd.game_id())) { CommandContainer cont; cont.set_cmd_id(rc.getCmdId()); @@ -256,16 +256,18 @@ Response::ResponseCode Server_Room::processJoinGameCommand(const Command_JoinGam userInterface->getUserInfo()->session_id(), id); return Response::RespNothing; - } else + } else { return Response::RespNameNotFound; + } } - QMutexLocker gameLocker(&g->gameMutex); + QMutexLocker gameLocker(&game->gameMutex); - Response::ResponseCode result = g->checkJoin(userInterface->getUserInfo(), QString::fromStdString(cmd.password()), - cmd.spectator(), cmd.override_restrictions(), cmd.join_as_judge()); + Response::ResponseCode result = + game->checkJoin(userInterface->getUserInfo(), QString::fromStdString(cmd.password()), cmd.spectator(), + cmd.override_restrictions(), cmd.join_as_judge()); if (result == Response::RespOk) - g->addPlayer(userInterface, rc, cmd.spectator(), cmd.join_as_judge()); + game->addPlayer(userInterface, rc, cmd.spectator(), cmd.join_as_judge()); return result; } diff --git a/common/sfmt/SFMT-common.h b/common/sfmt/SFMT-common.h index c7d8aa9fb..a5a9b0504 100644 --- a/common/sfmt/SFMT-common.h +++ b/common/sfmt/SFMT-common.h @@ -28,7 +28,7 @@ extern "C" { #include "SFMT.h" inline static void do_recursion(w128_t * r, w128_t * a, w128_t * b, - w128_t * c, w128_t * d); + w128_t * c, w128_t * d); inline static void rshift128(w128_t *out, w128_t const *in, int shift); inline static void lshift128(w128_t *out, w128_t const *in, int shift); @@ -123,24 +123,24 @@ inline static void lshift128(w128_t *out, w128_t const *in, int shift) */ #ifdef ONLY64 inline static void do_recursion(w128_t *r, w128_t *a, w128_t *b, w128_t *c, - w128_t *d) { + w128_t *d) { w128_t x; w128_t y; lshift128(&x, a, SFMT_SL2); rshift128(&y, c, SFMT_SR2); r->u[0] = a->u[0] ^ x.u[0] ^ ((b->u[0] >> SFMT_SR1) & SFMT_MSK2) ^ y.u[0] - ^ (d->u[0] << SFMT_SL1); + ^ (d->u[0] << SFMT_SL1); r->u[1] = a->u[1] ^ x.u[1] ^ ((b->u[1] >> SFMT_SR1) & SFMT_MSK1) ^ y.u[1] - ^ (d->u[1] << SFMT_SL1); + ^ (d->u[1] << SFMT_SL1); r->u[2] = a->u[2] ^ x.u[2] ^ ((b->u[2] >> SFMT_SR1) & SFMT_MSK4) ^ y.u[2] - ^ (d->u[2] << SFMT_SL1); + ^ (d->u[2] << SFMT_SL1); r->u[3] = a->u[3] ^ x.u[3] ^ ((b->u[3] >> SFMT_SR1) & SFMT_MSK3) ^ y.u[3] - ^ (d->u[3] << SFMT_SL1); + ^ (d->u[3] << SFMT_SL1); } #else inline static void do_recursion(w128_t *r, w128_t *a, w128_t *b, - w128_t *c, w128_t *d) + w128_t *c, w128_t *d) { w128_t x; w128_t y; @@ -148,17 +148,17 @@ inline static void do_recursion(w128_t *r, w128_t *a, w128_t *b, lshift128(&x, a, SFMT_SL2); rshift128(&y, c, SFMT_SR2); r->u[0] = a->u[0] ^ x.u[0] ^ ((b->u[0] >> SFMT_SR1) & SFMT_MSK1) - ^ y.u[0] ^ (d->u[0] << SFMT_SL1); + ^ y.u[0] ^ (d->u[0] << SFMT_SL1); r->u[1] = a->u[1] ^ x.u[1] ^ ((b->u[1] >> SFMT_SR1) & SFMT_MSK2) - ^ y.u[1] ^ (d->u[1] << SFMT_SL1); + ^ y.u[1] ^ (d->u[1] << SFMT_SL1); r->u[2] = a->u[2] ^ x.u[2] ^ ((b->u[2] >> SFMT_SR1) & SFMT_MSK3) - ^ y.u[2] ^ (d->u[2] << SFMT_SL1); + ^ y.u[2] ^ (d->u[2] << SFMT_SL1); r->u[3] = a->u[3] ^ x.u[3] ^ ((b->u[3] >> SFMT_SR1) & SFMT_MSK4) - ^ y.u[3] ^ (d->u[3] << SFMT_SL1); + ^ y.u[3] ^ (d->u[3] << SFMT_SL1); } #endif -#endif - #if defined(__cplusplus) } #endif + +#endif // SFMT_COMMON_H diff --git a/common/sfmt/SFMT-params.h b/common/sfmt/SFMT-params.h index 372e6f11a..2fe663ab6 100644 --- a/common/sfmt/SFMT-params.h +++ b/common/sfmt/SFMT-params.h @@ -46,8 +46,8 @@ */ /** the parameter of shift right as one 128-bit register. - * The 128-bit integer is shifted by (SFMT_SL2 * 8) bits. -#define SFMT_SR21 1 + * The 128-bit integer is shifted by (SFMT_SR2 * 8) bits. +#define SFMT_SR2 1 */ /** A bitmask, used in the recursion. These parameters are introduced @@ -59,10 +59,10 @@ */ /** These definitions are part of a 128-bit period certification vector. -#define SFMT_PARITY1 0x00000001U -#define SFMT_PARITY2 0x00000000U -#define SFMT_PARITY3 0x00000000U -#define SFMT_PARITY4 0xc98e126aU +#define SFMT_PARITY1 0x00000001U +#define SFMT_PARITY2 0x00000000U +#define SFMT_PARITY3 0x00000000U +#define SFMT_PARITY4 0xc98e126aU */ #if SFMT_MEXP == 607 diff --git a/common/sfmt/SFMT.c b/common/sfmt/SFMT.c index 2652df7de..b4ac9308b 100644 --- a/common/sfmt/SFMT.c +++ b/common/sfmt/SFMT.c @@ -40,11 +40,6 @@ extern "C" { #undef ONLY64 #endif -/** - * parameters used by sse2. - */ -static const w128_t sse2_param_mask = {{SFMT_MSK1, SFMT_MSK2, - SFMT_MSK3, SFMT_MSK4}}; /*---------------- STATIC FUNCTIONS ----------------*/ @@ -60,11 +55,18 @@ inline static void swap(w128_t *array, int size); #if defined(HAVE_ALTIVEC) #include "SFMT-alti.h" #elif defined(HAVE_SSE2) +/** + * parameters used by sse2. + */ + static const w128_t sse2_param_mask = {{SFMT_MSK1, SFMT_MSK2, + SFMT_MSK3, SFMT_MSK4}}; #if defined(_MSC_VER) #include "SFMT-sse2-msc.h" #else #include "SFMT-sse2.h" #endif +#elif defined(HAVE_NEON) + #include "SFMT-neon.h" #endif /** @@ -81,7 +83,7 @@ inline static int idxof(int i) { } #endif -#if (!defined(HAVE_ALTIVEC)) && (!defined(HAVE_SSE2)) +#if (!defined(HAVE_ALTIVEC)) && (!defined(HAVE_SSE2)) && (!defined(HAVE_NEON)) /** * This function fills the user-specified array with pseudorandom * integers. @@ -166,17 +168,19 @@ static uint32_t func2(uint32_t x) { * @param sfmt SFMT internal state */ static void period_certification(sfmt_t * sfmt) { - int inner = 0; + uint32_t inner = 0; int i, j; uint32_t work; uint32_t *psfmt32 = &sfmt->state[0].u[0]; const uint32_t parity[4] = {SFMT_PARITY1, SFMT_PARITY2, SFMT_PARITY3, SFMT_PARITY4}; - for (i = 0; i < 4; i++) + for (i = 0; i < 4; i++) { inner ^= psfmt32[idxof(i)] & parity[i]; - for (i = 16; i > 0; i >>= 1) + } + for (i = 16; i > 0; i >>= 1) { inner ^= inner >> i; + } inner &= 1; /* check OK */ if (inner == 1) { @@ -232,7 +236,7 @@ int sfmt_get_min_array_size64(sfmt_t * sfmt) { return SFMT_N64; } -#if !defined(HAVE_SSE2) && !defined(HAVE_ALTIVEC) +#if !defined(HAVE_SSE2) && !defined(HAVE_ALTIVEC) && !defined(HAVE_NEON) /** * This function fills the internal state array with pseudorandom * integers. diff --git a/common/sfmt/SFMT.h b/common/sfmt/SFMT.h index dca308a00..79e012d63 100644 --- a/common/sfmt/SFMT.h +++ b/common/sfmt/SFMT.h @@ -79,6 +79,15 @@ union W128_T { uint32_t u[4]; uint64_t u64[2]; }; +#elif defined(HAVE_NEON) + #include + +/** 128-bit data structure */ +union W128_T { + uint32_t u[4]; + uint64_t u64[2]; + uint32x4_t si; +}; #elif defined(HAVE_SSE2) #include @@ -247,7 +256,7 @@ inline static double sfmt_genrand_real3(sfmt_t * sfmt) */ inline static double sfmt_to_res53(uint64_t v) { - return v * (1.0/18446744073709551616.0); + return (v >> 11) * (1.0/9007199254740992.0); } /** diff --git a/dbconverter/src/mocks.cpp b/dbconverter/src/mocks.cpp index f107d5d40..82e732e7c 100644 --- a/dbconverter/src/mocks.cpp +++ b/dbconverter/src/mocks.cpp @@ -96,6 +96,9 @@ void SettingsCache::setDeckPath(const QString &/* _deckPath */) void SettingsCache::setReplaysPath(const QString &/* _replaysPath */) { } +void SettingsCache::setThemesPath(const QString &/* _themesPath */) +{ +} void SettingsCache::setPicsPath(const QString &/* _picsPath */) { } @@ -242,6 +245,9 @@ void SettingsCache::setSpectatorsCanTalk(const bool /* _spectatorsCanTalk */) void SettingsCache::setSpectatorsCanSeeEverything(const bool /* _spectatorsCanSeeEverything */) { } +void SettingsCache::setCreateGameAsSpectator(const bool /* _createGameAsSpectator */) +{ +} void SettingsCache::setRememberGameSettings(const bool /* _rememberGameSettings */) { } diff --git a/docker-compose.yml b/docker-compose.yml index 515aeb643..7cef0ffe0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,6 +3,7 @@ version: '3' services: mysql: image: mysql + command: --sql_mode= environment: - MYSQL_ROOT_PASSWORD=root-password - MYSQL_DATABASE=servatrice diff --git a/oracle/src/main.cpp b/oracle/src/main.cpp index b26c339d2..6cae0b6d6 100644 --- a/oracle/src/main.cpp +++ b/oracle/src/main.cpp @@ -62,6 +62,10 @@ int main(int argc, char *argv[]) QIcon icon("theme:appicon.svg"); wizard.setWindowIcon(icon); +#if QT_VERSION >= QT_VERSION_CHECK(5, 7, 0) + // set name of the app desktop file; used by wayland to load the window icon + QGuiApplication::setDesktopFileName("oracle"); +#endif wizard.show(); diff --git a/oracle/src/oracleimporter.cpp b/oracle/src/oracleimporter.cpp index ebad71283..785f54700 100644 --- a/oracle/src/oracleimporter.cpp +++ b/oracle/src/oracleimporter.cpp @@ -3,16 +3,15 @@ #include "carddbparser/cockatricexml4.h" #include "qt-json/json.h" -#include #include #include #include -SplitCardPart::SplitCardPart(const int _index, +SplitCardPart::SplitCardPart(const QString &_name, const QString &_text, const QVariantHash &_properties, const CardInfoPerSet _setInfo) - : index(_index), text(_text), properties(_properties), setInfo(_setInfo) + : name(_name), text(_text), properties(_properties), setInfo(_setInfo) { } @@ -25,7 +24,7 @@ bool OracleImporter::readSetsFromByteArray(const QByteArray &data) QList newSetList; bool ok; - setsMap = QtJson::Json::parse(QString(data), ok).toMap(); + setsMap = QtJson::Json::parse(QString(data), ok).toMap().value("data").toMap(); if (!ok) { qDebug() << "error: QtJson::Json::parse()"; return false; @@ -133,7 +132,7 @@ CardInfoPtr OracleImporter::addCard(QString name, sortAndReduceColors(allColors); properties.insert("colors", allColors); } - QString allColorIdent = properties.value("colorIdenity").toString(); + QString allColorIdent = properties.value("coloridentity").toString(); if (allColorIdent.size() > 1) { sortAndReduceColors(allColorIdent); properties.insert("coloridentity", allColorIdent); @@ -146,23 +145,10 @@ CardInfoPtr OracleImporter::addCard(QString name, (text.contains(name + " enters the battlefield tapped") && !text.contains(name + " enters the battlefield tapped unless")); - // detect mana generator artifacts - QStringList cardTextRows = text.split("\n"); - bool mArtifact = false; - QString cardType = properties.value("type").toString(); - if (cardType.endsWith("Artifact")) { - for (int i = 0; i < cardTextRows.size(); ++i) { - cardTextRows[i].remove(QRegularExpression(R"(\".*?\")")); - if (cardTextRows[i].contains("{T}") && cardTextRows[i].contains("to your mana pool")) { - mArtifact = true; - } - } - } - // table row int tableRow = 1; QString mainCardType = properties.value("maintype").toString(); - if ((mainCardType == "Land") || mArtifact) + if ((mainCardType == "Land")) tableRow = 0; else if ((mainCardType == "Sorcery") || (mainCardType == "Instant")) tableRow = 3; @@ -174,16 +160,8 @@ CardInfoPtr OracleImporter::addCard(QString name, properties.insert("side", side); // upsideDown (flip cards) - bool upsideDown = false; - QStringList additionalNames = properties.value("names").toStringList(); QString layout = properties.value("layout").toString(); - if (layout == "flip") { - if (properties.value("side").toString() != "front") { - upsideDown = true; - } - // reset the side property, since the card has no back image - properties.insert("side", "front"); - } + bool upsideDown = layout == "flip" && side == "back"; // insert the card and its properties QList reverseRelatedCards; @@ -192,36 +170,41 @@ CardInfoPtr OracleImporter::addCard(QString name, CardInfoPtr newCard = CardInfo::newInstance(name, text, isToken, properties, relatedCards, reverseRelatedCards, setsInfo, cipt, tableRow, upsideDown); + if (name.isEmpty()) { + qDebug() << "warning: an empty card was added to set" << setInfo.getPtr()->getShortName(); + } cards.insert(name, newCard); + return newCard; } -QString OracleImporter::getStringPropertyFromMap(QVariantMap card, QString propertyName) +QString OracleImporter::getStringPropertyFromMap(const QVariantMap &card, const QString &propertyName) { return card.contains(propertyName) ? card.value(propertyName).toString() : QString(""); } -int OracleImporter::importCardsFromSet(CardSetPtr currentSet, const QList &cardsList, bool skipSpecialCards) +int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, + const QList &cardsList, + bool skipSpecialCards) { + // mtgjson name => xml name static const QMap cardProperties{ - // mtgjson name => xml name {"manaCost", "manacost"}, {"convertedManaCost", "cmc"}, {"type", "type"}, {"loyalty", "loyalty"}, {"layout", "layout"}, {"side", "side"}, }; - static const QMap setInfoProperties{// mtgjson name => xml name - {"multiverseId", "muid"}, - {"scryfallId", "uuid"}, - {"number", "num"}, - {"rarity", "rarity"}}; + // mtgjson name => xml name + static const QMap setInfoProperties{{"number", "num"}, {"rarity", "rarity"}}; + + // mtgjson name => xml name + static const QMap identifierProperties{{"multiverseId", "muid"}, {"scryfallId", "uuid"}}; int numCards = 0; - QMultiMap splitCards; + QMap> splitCards; QString ptSeparator("/"); QVariantMap card; - QString layout, name, text, colors, colorIdentity, maintype, power, toughness; + QString layout, name, text, colors, colorIdentity, maintype, power, toughness, faceName; static const bool isToken = false; - QStringList additionalNames; QVariantHash properties; CardInfoPerSet setInfo; QList relatedCards; @@ -232,6 +215,11 @@ int OracleImporter::importCardsFromSet(CardSetPtr currentSet, const QList it3(identifierProperties); + while (it3.hasNext()) { + it3.next(); + auto mtgjsonProperty = it3.key(); + auto xmlPropertyName = it3.value(); + auto propertyValue = getStringPropertyFromMap(card.value("identifiers").toMap(), mtgjsonProperty); + if (!propertyValue.isEmpty()) { + setInfo.setProperty(xmlPropertyName, propertyValue); + } } + QString numComponent{}; if (skipSpecialCards) { - // skip promo cards if it's not the only print - if (allNameProps.contains(name)) { - continue; + QString numProperty = setInfo.getProperty("num"); + // skip promo cards if it's not the only print, cards with two faces are different cards + if (allNameProps.contains(faceName)) { + // check for alternative versions + if (layout != "normal") + continue; + + // alternative versions have a letter in the end of num like abc + // note this will also catch p and s, those will get removed later anyway + QChar lastChar = numProperty.at(numProperty.size() - 1); + if (!lastChar.isLetter()) + continue; + + numComponent = " (" + QString(lastChar) + ")"; + faceName += numComponent; // add to facename to make it unique } if (getStringPropertyFromMap(card, "isPromo") == "true") { - specialPromoCards.insert(name, cardVar); + specialPromoCards.insert(faceName, cardVar); continue; } - QString numProperty = setInfo.getProperty("num"); bool skip = false; // skip cards containing special stuff in the collectors number like promo cards for (const QString &specialChar : specialNumChars) { @@ -295,10 +306,10 @@ int OracleImporter::importCardsFromSet(CardSetPtr currentSet, const QListinsert(0, split); + } else { + found_iter->append(split); + } } else { // relations relatedCards.clear(); - if (additionalNames.size() > 1) { - for (const QString &additionalName : additionalNames) { - if (additionalName != name) + + // add other face for split cards as card relation + if (!getStringPropertyFromMap(card, "side").isEmpty()) { + properties["cmc"] = getStringPropertyFromMap(card, "faceConvertedManaCost"); + if (layout == "meld") { // meld cards don't work + static const QRegularExpression meldNameRegex{"then meld them into ([^\\.]*)"}; + QString additionalName = meldNameRegex.match(text).captured(1); + if (!additionalName.isNull()) { relatedCards.append(new CardRelation(additionalName, true)); + } + } else { + for (const QString &additionalName : name.split(" // ")) { + if (additionalName != faceName) { + relatedCards.append(new CardRelation(additionalName, true)); + } + } } + name = faceName; } - CardInfoPtr newCard = addCard(name, text, isToken, properties, relatedCards, setInfo); + CardInfoPtr newCard = addCard(name + numComponent, text, isToken, properties, relatedCards, setInfo); numCards++; } } // split cards handling - QString splitCardPropSeparator = QString(" // "); - QString splitCardTextSeparator = QString("\n\n---\n\n"); - for (const QString &nameSplit : splitCards.uniqueKeys()) { + static const QString splitCardPropSeparator = QString(" // "); + static const QString splitCardTextSeparator = QString("\n\n---\n\n"); + for (const QString &nameSplit : splitCards.keys()) { // get all parts for this specific card - QList splitCardParts = splitCards.values(nameSplit); - // sort them by index (aka position) - std::sort(splitCardParts.begin(), splitCardParts.end(), - [](const SplitCardPart &a, const SplitCardPart &b) -> bool { return a.getIndex() < b.getIndex(); }); + QList splitCardParts = splitCards.value(nameSplit); + QSet done{}; - text = QString(""); + text.clear(); properties.clear(); relatedCards.clear(); - int lastIndex = -1; for (const SplitCardPart &tmp : splitCardParts) { // some sets have 2 different variations of the same split card, // eg. Fire // Ice in WC02. Avoid adding duplicates. - if (lastIndex == tmp.getIndex()) + QString splitName = tmp.getName(); + if (done.contains(splitName)) { continue; - lastIndex = tmp.getIndex(); + } + done.insert(splitName); - if (!text.isEmpty()) + if (!text.isEmpty()) { text.append(splitCardTextSeparator); + } text.append(tmp.getText()); if (properties.isEmpty()) { properties = tmp.getProperties(); setInfo = tmp.getSetInfo(); } else { - const QVariantHash &props = tmp.getProperties(); - layout = properties.value("layout").toString(); - for (const QString &prop : props.keys()) { + const QVariantHash &tmpProps = tmp.getProperties(); + for (const QString &prop : tmpProps.keys()) { QString originalPropertyValue = properties.value(prop).toString(); - QString thisCardPropertyValue = props.value(prop).toString(); - if (originalPropertyValue != thisCardPropertyValue) { - if (prop == "colors") { + QString thisCardPropertyValue = tmpProps.value(prop).toString(); + if (!thisCardPropertyValue.isEmpty() && originalPropertyValue != thisCardPropertyValue) { + if (originalPropertyValue.isEmpty()) { // don't create //es if one field is empty + properties.insert(prop, thisCardPropertyValue); + } else if (prop == "colors") { // the card is both colors properties.insert(prop, originalPropertyValue + thisCardPropertyValue); } else if (prop == "maintype") { // don't create maintypes with //es in them - properties.insert(prop, originalPropertyValue); + continue; } else { properties.insert(prop, originalPropertyValue + splitCardPropSeparator + thisCardPropertyValue); diff --git a/oracle/src/oracleimporter.h b/oracle/src/oracleimporter.h index 5d20af754..d512ebb41 100644 --- a/oracle/src/oracleimporter.h +++ b/oracle/src/oracleimporter.h @@ -60,10 +60,10 @@ public: class SplitCardPart { public: - SplitCardPart(int _index, const QString &_text, const QVariantHash &_properties, CardInfoPerSet setInfo); - inline const int &getIndex() const + SplitCardPart(const QString &_name, const QString &_text, const QVariantHash &_properties, CardInfoPerSet setInfo); + inline const QString &getName() const { - return index; + return name; } inline const QString &getText() const { @@ -79,7 +79,7 @@ public: } private: - int index; + QString name; QString text; QVariantHash properties; CardInfoPerSet setInfo; @@ -111,7 +111,7 @@ public: bool readSetsFromByteArray(const QByteArray &data); int startImport(); bool saveToFile(const QString &fileName, const QString &sourceUrl, const QString &sourceVersion); - int importCardsFromSet(CardSetPtr currentSet, const QList &cards, bool skipSpecialNums = true); + int importCardsFromSet(const CardSetPtr ¤tSet, const QList &cards, bool skipSpecialNums = true); QList &getSets() { return allSets; @@ -123,7 +123,7 @@ public: void clear(); protected: - inline QString getStringPropertyFromMap(QVariantMap card, QString propertyName); + inline QString getStringPropertyFromMap(const QVariantMap &card, const QString &propertyName); void sortAndReduceColors(QString &colors); }; diff --git a/oracle/src/oraclewizard.cpp b/oracle/src/oraclewizard.cpp index f824bbf3e..0da8ecaaf 100644 --- a/oracle/src/oraclewizard.cpp +++ b/oracle/src/oraclewizard.cpp @@ -20,7 +20,6 @@ #include #include #include -#include #include #include #include @@ -38,15 +37,16 @@ #define ZIP_SIGNATURE "PK" // Xz stream header: 0xFD + "7zXZ" #define XZ_SIGNATURE "\xFD\x37\x7A\x58\x5A" -#define ALLSETS_URL_FALLBACK "https://www.mtgjson.com/files/AllPrintings.json" -#define MTGJSON_VERSION_URL "https://www.mtgjson.com/files/version.json" +#define MTGJSON_V4_URL_COMPONENT "mtgjson.com/files/" +#define ALLSETS_URL_FALLBACK "https://www.mtgjson.com/api/v5/AllPrintings.json" +#define MTGJSON_VERSION_URL "https://www.mtgjson.com/api/v5/Meta.json" #ifdef HAS_LZMA -#define ALLSETS_URL "https://www.mtgjson.com/files/AllPrintings.json.xz" +#define ALLSETS_URL "https://www.mtgjson.com/api/v5/AllPrintings.json.xz" #elif defined(HAS_ZLIB) -#define ALLSETS_URL "https://www.mtgjson.com/files/AllPrintings.json.zip" +#define ALLSETS_URL "https://www.mtgjson.com/api/v5/AllPrintings.json.zip" #else -#define ALLSETS_URL "https://www.mtgjson.com/files/AllPrintings.json" +#define ALLSETS_URL "https://www.mtgjson.com/api/v5/AllPrintings.json" #endif #define TOKENS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Token/master/tokens.xml" @@ -299,7 +299,13 @@ bool LoadSetsPage::validatePage() // else, try to import sets if (urlRadioButton->isChecked()) { - QUrl url = QUrl::fromUserInput(urlLineEdit->text()); + // If a user attempts to download from V4, redirect them to V5 + if (urlLineEdit->text().contains(MTGJSON_V4_URL_COMPONENT)) { + actRestoreDefaultUrl(); + } + + const auto url = QUrl::fromUserInput(urlLineEdit->text()); + if (!url.isValid()) { QMessageBox::critical(this, tr("Error"), tr("The provided URL is not valid.")); return false; @@ -342,23 +348,24 @@ bool LoadSetsPage::validatePage() } #include -void LoadSetsPage::downloadSetsFile(QUrl url) +void LoadSetsPage::downloadSetsFile(const QUrl &url) { wizard()->setCardSourceVersion("unknown"); - QString urlString = url.toString(); + const auto urlString = url.toString(); if (urlString == ALLSETS_URL || urlString == ALLSETS_URL_FALLBACK) { - QUrl versionUrl = QUrl::fromUserInput(MTGJSON_VERSION_URL); - QNetworkReply *versionReply = wizard()->nam->get(QNetworkRequest(versionUrl)); + const auto versionUrl = QUrl::fromUserInput(MTGJSON_VERSION_URL); + auto *versionReply = wizard()->nam->get(QNetworkRequest(versionUrl)); connect(versionReply, &QNetworkReply::finished, [this, versionReply]() { if (versionReply->error() == QNetworkReply::NoError) { - QByteArray jsonData = versionReply->readAll(); - QJsonParseError jsonError; - QJsonDocument jsonResponse = QJsonDocument::fromJson(jsonData, &jsonError); + auto jsonData = versionReply->readAll(); + QJsonParseError jsonError{}; + auto jsonResponse = QJsonDocument::fromJson(jsonData, &jsonError); if (jsonError.error == QJsonParseError::NoError) { - QVariantMap jsonMap = jsonResponse.toVariant().toMap(); - QString versionString = jsonMap["version"].toString(); + const auto jsonMap = jsonResponse.toVariant().toMap(); + + auto versionString = jsonMap.value("meta").toMap().value("version").toString(); if (versionString.isEmpty()) { versionString = "unknown"; } @@ -372,7 +379,7 @@ void LoadSetsPage::downloadSetsFile(QUrl url) wizard()->setCardSourceUrl(url.toString()); - QNetworkReply *reply = wizard()->nam->get(QNetworkRequest(url)); + auto *reply = wizard()->nam->get(QNetworkRequest(url)); connect(reply, SIGNAL(finished()), this, SLOT(actDownloadFinishedSetsFile())); connect(reply, SIGNAL(downloadProgress(qint64, qint64)), this, SLOT(actDownloadProgressSetsFile(qint64, qint64))); @@ -391,7 +398,7 @@ void LoadSetsPage::actDownloadFinishedSetsFile() { // check for a reply auto *reply = dynamic_cast(sender()); - QNetworkReply::NetworkError errorCode = reply->error(); + auto errorCode = reply->error(); if (errorCode != QNetworkReply::NoError) { QMessageBox::critical(this, tr("Error"), tr("Network error: %1.").arg(reply->errorString())); @@ -402,9 +409,9 @@ void LoadSetsPage::actDownloadFinishedSetsFile() return; } - int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + auto statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (statusCode == 301 || statusCode == 302) { - QUrl redirectUrl = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl(); + const auto redirectUrl = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl(); qDebug() << "following redirect url:" << redirectUrl.toString(); downloadSetsFile(redirectUrl); reply->deleteLater(); @@ -414,7 +421,7 @@ void LoadSetsPage::actDownloadFinishedSetsFile() progressLabel->hide(); progressBar->hide(); - // save allsets.json url, but only if the user customized it and download was successfull + // save AllPrintings.json url, but only if the user customized it and download was successful if (urlLineEdit->text() != QString(ALLSETS_URL)) { wizard()->settings->setValue("allsetsurl", urlLineEdit->text()); } else { @@ -607,35 +614,31 @@ void SaveSetsPage::updateTotalProgress(int cardsImported, int /* setIndex */, co bool SaveSetsPage::validatePage() { - bool ok = false; QString defaultPath = SettingsCache::instance().getCardDatabasePath(); QString windowName = tr("Save card database"); QString fileType = tr("XML; card database (*.xml)"); - do { - QString fileName; - if (defaultPathCheckBox->isChecked()) { - fileName = QFileDialog::getSaveFileName(this, windowName, defaultPath, fileType); - } else { - fileName = defaultPath; - } + QString fileName; + if (defaultPathCheckBox->isChecked()) { + fileName = QFileDialog::getSaveFileName(this, windowName, defaultPath, fileType); + } else { + fileName = defaultPath; + } - if (fileName.isEmpty()) { - return false; - } + if (fileName.isEmpty()) { + return false; + } - QFileInfo fi(fileName); - QDir fileDir(fi.path()); - if (!fileDir.exists() && !fileDir.mkpath(fileDir.absolutePath())) { - return false; - } + QFileInfo fi(fileName); + QDir fileDir(fi.path()); + if (!fileDir.exists() && !fileDir.mkpath(fileDir.absolutePath())) { + return false; + } - if (wizard()->importer->saveToFile(fileName, wizard()->getCardSourceUrl(), wizard()->getCardSourceVersion())) { - ok = true; - } else { - QMessageBox::critical(this, tr("Error"), tr("The file could not be saved to %1").arg(fileName)); - } - } while (!ok); + if (!wizard()->importer->saveToFile(fileName, wizard()->getCardSourceUrl(), wizard()->getCardSourceVersion())) { + QMessageBox::critical(this, tr("Error"), tr("The file could not be saved to %1").arg(fileName)); + return false; + } return true; } diff --git a/oracle/src/oraclewizard.h b/oracle/src/oraclewizard.h index 28f0d2e08..362c19def 100644 --- a/oracle/src/oraclewizard.h +++ b/oracle/src/oraclewizard.h @@ -113,7 +113,7 @@ protected: void initializePage() override; bool validatePage() override; void readSetsFromByteArray(QByteArray data); - void downloadSetsFile(QUrl url); + void downloadSetsFile(const QUrl &url); private: QRadioButton *urlRadioButton; diff --git a/oracle/src/pagetemplates.cpp b/oracle/src/pagetemplates.cpp index 609c0b6b3..b4d8d358b 100644 --- a/oracle/src/pagetemplates.cpp +++ b/oracle/src/pagetemplates.cpp @@ -125,7 +125,7 @@ void SimpleDownloadFilePage::actDownloadFinished() return; } - // save downlaoded file url, but only if the user customized it and download was successfull + // save downloaded file url, but only if the user customized it and download was successful if (urlLineEdit->text() != getDefaultUrl()) { wizard()->settings->setValue(getCustomUrlSettingsKey(), urlLineEdit->text()); } else { @@ -144,35 +144,31 @@ void SimpleDownloadFilePage::actDownloadFinished() bool SimpleDownloadFilePage::saveToFile() { - bool ok = false; QString defaultPath = getDefaultSavePath(); QString windowName = getWindowTitle(); QString fileType = getFileType(); - do { - QString fileName; - if (defaultPathCheckBox->isChecked()) { - fileName = QFileDialog::getSaveFileName(this, windowName, defaultPath, fileType); - } else { - fileName = defaultPath; - } + QString fileName; + if (defaultPathCheckBox->isChecked()) { + fileName = QFileDialog::getSaveFileName(this, windowName, defaultPath, fileType); + } else { + fileName = defaultPath; + } - if (fileName.isEmpty()) { - return false; - } + if (fileName.isEmpty()) { + return false; + } - QFileInfo fi(fileName); - QDir fileDir(fi.path()); - if (!fileDir.exists() && !fileDir.mkpath(fileDir.absolutePath())) { - return false; - } + QFileInfo fi(fileName); + QDir fileDir(fi.path()); + if (!fileDir.exists() && !fileDir.mkpath(fileDir.absolutePath())) { + return false; + } - if (internalSaveToFile(fileName)) { - ok = true; - } else { - QMessageBox::critical(this, tr("Error"), tr("The file could not be saved to %1").arg(fileName)); - } - } while (!ok); + if (!internalSaveToFile(fileName)) { + QMessageBox::critical(this, tr("Error"), tr("The file could not be saved to %1").arg(fileName)); + return false; + } // clean saved downloadData downloadData = QByteArray(); diff --git a/oracle/translations/oracle_es.ts b/oracle/translations/oracle_es.ts index bc7778ad5..deca061ce 100644 --- a/oracle/translations/oracle_es.ts +++ b/oracle/translations/oracle_es.ts @@ -14,7 +14,7 @@ Interface language: - + Idioma de la interfaz: diff --git a/oracle/translations/oracle_ja.ts b/oracle/translations/oracle_ja.ts index 1f68d29f0..bf0a3496c 100644 --- a/oracle/translations/oracle_ja.ts +++ b/oracle/translations/oracle_ja.ts @@ -14,7 +14,7 @@ Interface language: - + インターフェース言語: diff --git a/oracle/translations/oracle_pt_BR.ts b/oracle/translations/oracle_pt_BR.ts index 40529a76c..f1cd8239a 100644 --- a/oracle/translations/oracle_pt_BR.ts +++ b/oracle/translations/oracle_pt_BR.ts @@ -14,7 +14,7 @@ Interface language: - + Idioma da interface: diff --git a/servatrice/servatrice.ini.example b/servatrice/servatrice.ini.example index ec42a1f32..cb7f1c4c9 100644 --- a/servatrice/servatrice.ini.example +++ b/servatrice/servatrice.ini.example @@ -169,27 +169,27 @@ minpasswordlength = 6 [forgotpassword] -; Servatrice can process forgot password requests allowing users to reset their account +; Servatrice can process reset password requests allowing users to reset their account ; passwords in the event they forget it. Should this feature be enabled? Default: false. ; enable=false -; Forgot password request should not be allowed to stay valid forever. This settings -; informs servatrice how long a players forgot password reset token is valid for (in minutes). +; Reset password request should not be allowed to stay valid forever. This settings +; informs servatrice how long a players reset password reset token is valid for (in minutes). ; Default: 60 ; tokenlife=60 -; Servatrice can challenge users that are making forgot password requests to answer +; Servatrice can challenge users that are making reset password requests to answer ; questions in regards to their account to help validate they are the true owner of the account. ; Should this feature be enabled? Default: false ; enablechallenge=false -; Email subject for the forgot password emails -; subject="Cockatrice forgot password token" +; Email subject for the reset password emails +; subject="Cockatrice reset password token" -; Forgot password email body. You can use these tags here: %username %token +; Reset password email body. You can use these tags here: %username %token ; They will be substituted with the actual values in the email ; -; body="Hi %username, sorry to hear you forgot your password on our Cockatrice server\r\nHere's the token to use to reset your account password:\r\n\r\n%token\r\n\r\nHappy gaming!" +; body="Hi %username,\r\nthanks for reaching out to us with your password reset request for our Cockatrice server.\r\nHere's your unique token in order to reset your account password in the app:\r\n\r\n%token\r\n\r\nHappy gaming!" [smtp] @@ -347,7 +347,7 @@ max_message_size_per_interval=1000 ; Maximum number of messages in an interval before new messages gets dropped; default is 10 max_message_count_per_interval=10 -; Maximum number of games a single user can create; default is 5 +; Maximum number of games a single user can create; default is 5; set to -1 to disable; 0 disallows game creation max_games_per_user=5 ; Servatrice can avoid users from flooding games with large number of game commands in an interval of time. diff --git a/servatrice/src/servatrice.cpp b/servatrice/src/servatrice.cpp index f1913ec03..3d44f323a 100644 --- a/servatrice/src/servatrice.cpp +++ b/servatrice/src/servatrice.cpp @@ -285,16 +285,16 @@ bool Servatrice::initServer() } } - qDebug() << "Forgot password enabled: " << getEnableForgotPassword(); + qDebug() << "Reset password enabled: " << getEnableForgotPassword(); if (getEnableForgotPassword()) { - qDebug() << "Forgot password token life (in minutes): " << getForgotPasswordTokenLife(); - qDebug() << "Forgot password challenge on: " << getEnableForgotPasswordChallenge(); + qDebug() << "Reset password token life (in minutes): " << getForgotPasswordTokenLife(); + qDebug() << "Reset password challenge on: " << getEnableForgotPasswordChallenge(); } qDebug() << "Auditing enabled: " << getEnableAudit(); if (getEnableAudit()) { qDebug() << "Audit registration attempts enabled: " << getEnableRegistrationAudit(); - qDebug() << "Audit forgot password attepts enabled: " << getEnableForgotPasswordAudit(); + qDebug() << "Audit reset password attepts enabled: " << getEnableForgotPasswordAudit(); } if (getDBTypeString() == "mysql") { @@ -960,12 +960,12 @@ int Servatrice::getMaxGamesPerUser() const int Servatrice::getCommandCountingInterval() const { - return settingsCache->value("game/command_counting_interval", 10).toInt(); + return settingsCache->value("security/command_counting_interval", 10).toInt(); } int Servatrice::getMaxCommandCountPerInterval() const { - return settingsCache->value("game/max_command_count_per_interval", 20).toInt(); + return settingsCache->value("security/max_command_count_per_interval", 20).toInt(); } int Servatrice::getServerStatusUpdateTime() const diff --git a/servatrice/src/servatrice_database_interface.cpp b/servatrice/src/servatrice_database_interface.cpp index f1d639ff4..6c742256f 100644 --- a/servatrice/src/servatrice_database_interface.cpp +++ b/servatrice/src/servatrice_database_interface.cpp @@ -201,14 +201,13 @@ bool Servatrice_DatabaseInterface::registerUser(const QString &userName, const QString &password, const QString &emailAddress, const QString &country, - QString &token, bool active) { if (!checkSql()) return false; QString passwordSha512 = PasswordHasher::computeHash(password, PasswordHasher::generateRandomSalt()); - token = active ? QString() : PasswordHasher::generateActivationToken(); + QString token = active ? QString() : PasswordHasher::generateActivationToken(); QSqlQuery *query = prepareQuery("insert into {prefix}_users " diff --git a/servatrice/src/servatrice_database_interface.h b/servatrice/src/servatrice_database_interface.h index 0107e02b3..2c225b82f 100644 --- a/servatrice/src/servatrice_database_interface.h +++ b/servatrice/src/servatrice_database_interface.h @@ -99,7 +99,6 @@ public: const QString &password, const QString &emailAddress, const QString &country, - QString &token, bool active = false); bool activateUser(const QString &userName, const QString &token); void updateUsersClientID(const QString &userName, const QString &userClientID); diff --git a/servatrice/src/serversocketinterface.cpp b/servatrice/src/serversocketinterface.cpp index 14b1a680b..79755f4e9 100644 --- a/servatrice/src/serversocketinterface.cpp +++ b/servatrice/src/serversocketinterface.cpp @@ -103,7 +103,7 @@ bool AbstractServerSocketInterface::initSession() return true; int maxUsers = servatrice->getMaxUsersPerAddress(); - if ((maxUsers > 0) && (servatrice->getUsersWithAddress(getPeerAddress()) >= maxUsers)) { + if ((maxUsers > 0) && (servatrice->getUsersWithAddress(getPeerAddress()) > maxUsers)) { Event_ConnectionClosed event; event.set_reason(Event_ConnectionClosed::TOO_MANY_CONNECTIONS); SessionEvent *se = prepareSessionEvent(event); @@ -1106,9 +1106,8 @@ Response::ResponseCode AbstractServerSocketInterface::cmdRegisterAccount(const C return Response::RespPasswordTooShort; } - QString token; bool requireEmailActivation = settingsCache->value("registration/requireemailactivation", true).toBool(); - bool regSucceeded = sqlInterface->registerUser(userName, realName, gender, password, emailAddress, country, token, + bool regSucceeded = sqlInterface->registerUser(userName, realName, gender, password, emailAddress, country, !requireEmailActivation); if (regSucceeded) { @@ -1256,7 +1255,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdAccountPassword(const C Response::ResponseCode AbstractServerSocketInterface::cmdForgotPasswordRequest(const Command_ForgotPasswordRequest &cmd, ResponseContainer &rc) { - qDebug() << "Received forgot password request from user: " << QString::fromStdString(cmd.user_name()); + qDebug() << "Received reset password request from user: " << QString::fromStdString(cmd.user_name()); if (!servatrice->getEnableForgotPassword()) { if (servatrice->getEnableForgotPasswordAudit()) @@ -1334,7 +1333,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdForgotPasswordReset(con ResponseContainer &rc) { Q_UNUSED(rc); - qDebug() << "Received forgot password reset from user: " << QString::fromStdString(cmd.user_name()); + qDebug() << "Received reset password reset from user: " << QString::fromStdString(cmd.user_name()); if (!sqlInterface->doesForgotPasswordExist(QString::fromStdString(cmd.user_name()))) { if (servatrice->getEnableForgotPasswordAudit()) @@ -1374,7 +1373,7 @@ AbstractServerSocketInterface::cmdForgotPasswordChallenge(const Command_ForgotPa ResponseContainer &rc) { Q_UNUSED(rc); - qDebug() << "Received forgot password challenge from user: " << QString::fromStdString(cmd.user_name()); + qDebug() << "Received reset password challenge from user: " << QString::fromStdString(cmd.user_name()); if (sqlInterface->doesForgotPasswordExist(QString::fromStdString(cmd.user_name()))) { if (servatrice->getEnableForgotPasswordAudit()) @@ -1699,6 +1698,9 @@ WebsocketServerSocketInterface::~WebsocketServerSocketInterface() void WebsocketServerSocketInterface::initConnection(void *_socket) { + if (_socket == nullptr) { + return; + } socket = (QWebSocket *)_socket; socket->setParent(this); address = socket->peerAddress(); diff --git a/servatrice/src/smtp/qxtmailattachment.h b/servatrice/src/smtp/qxtmailattachment.h index 397b117f7..62a0e9ad5 100644 --- a/servatrice/src/smtp/qxtmailattachment.h +++ b/servatrice/src/smtp/qxtmailattachment.h @@ -27,41 +27,41 @@ #include "qxtglobal.h" -#include -#include #include +#include +#include #include #include -#include +#include struct QxtMailAttachmentPrivate; class QXT_NETWORK_EXPORT QxtMailAttachment { public: QxtMailAttachment(); - QxtMailAttachment(const QxtMailAttachment& other); - QxtMailAttachment(const QByteArray& content, const QString& contentType = QString("application/octet-stream")); - QxtMailAttachment(QIODevice* content, const QString& contentType = QString("application/octet-stream")); - QxtMailAttachment& operator=(const QxtMailAttachment& other); + QxtMailAttachment(const QxtMailAttachment &other); + QxtMailAttachment(const QByteArray &content, const QString &contentType = QString("application/octet-stream")); + QxtMailAttachment(QIODevice *content, const QString &contentType = QString("application/octet-stream")); + QxtMailAttachment &operator=(const QxtMailAttachment &other); ~QxtMailAttachment(); - static QxtMailAttachment fromFile(const QString& filename); + static QxtMailAttachment fromFile(const QString &filename); - QIODevice* content() const; - void setContent(const QByteArray& content); - void setContent(QIODevice* content); + QIODevice *content() const; + void setContent(const QByteArray &content); + void setContent(QIODevice *content); bool deleteContent() const; void setDeleteContent(bool enable); QString contentType() const; - void setContentType(const QString& contentType); + void setContentType(const QString &contentType); QHash extraHeaders() const; - QByteArray extraHeader(const QString&) const; - bool hasExtraHeader(const QString&) const; - void setExtraHeader(const QString& key, const QString& value); - void setExtraHeaders(const QHash&); - void removeExtraHeader(const QString& key); + QByteArray extraHeader(const QString &) const; + bool hasExtraHeader(const QString &) const; + void setExtraHeader(const QString &key, const QString &value); + void setExtraHeaders(const QHash &); + void removeExtraHeader(const QString &key); QByteArray mimeData(); diff --git a/servatrice/src/smtp/qxtmailmessage.cpp b/servatrice/src/smtp/qxtmailmessage.cpp index 9d6de47d2..45452aede 100644 --- a/servatrice/src/smtp/qxtmailmessage.cpp +++ b/servatrice/src/smtp/qxtmailmessage.cpp @@ -23,7 +23,6 @@ ** ****************************************************************************/ - /*! * \class QxtMailMessage * \inmodule QxtNetwork @@ -31,22 +30,26 @@ * TODO: {implicitshared} */ - #include "qxtmailmessage.h" + #include "qxtmail_p.h" + +#include #include #include -#include #include - struct QxtMailMessagePrivate : public QSharedData { - QxtMailMessagePrivate() {} - QxtMailMessagePrivate(const QxtMailMessagePrivate& other) - : QSharedData(other), rcptTo(other.rcptTo), rcptCc(other.rcptCc), rcptBcc(other.rcptBcc), - subject(other.subject), body(other.body), sender(other.sender), - extraHeaders(other.extraHeaders), attachments(other.attachments) {} + QxtMailMessagePrivate() + { + } + QxtMailMessagePrivate(const QxtMailMessagePrivate &other) + : QSharedData(other), rcptTo(other.rcptTo), rcptCc(other.rcptCc), rcptBcc(other.rcptBcc), + subject(other.subject), body(other.body), sender(other.sender), extraHeaders(other.extraHeaders), + attachments(other.attachments) + { + } QStringList rcptTo, rcptCc, rcptBcc; QString subject, body, sender; QHash extraHeaders; @@ -59,12 +62,12 @@ QxtMailMessage::QxtMailMessage() qxt_d = new QxtMailMessagePrivate; } -QxtMailMessage::QxtMailMessage(const QxtMailMessage& other) : qxt_d(other.qxt_d) +QxtMailMessage::QxtMailMessage(const QxtMailMessage &other) : qxt_d(other.qxt_d) { // trivial copy constructor } -QxtMailMessage::QxtMailMessage(const QString& sender, const QString& recipient) +QxtMailMessage::QxtMailMessage(const QString &sender, const QString &recipient) { qxt_d = new QxtMailMessagePrivate; setSender(sender); @@ -76,7 +79,7 @@ QxtMailMessage::~QxtMailMessage() // trivial destructor } -QxtMailMessage& QxtMailMessage::operator=(const QxtMailMessage & other) +QxtMailMessage &QxtMailMessage::operator=(const QxtMailMessage &other) { qxt_d = other.qxt_d; return *this; @@ -87,7 +90,7 @@ QString QxtMailMessage::sender() const return qxt_d->sender; } -void QxtMailMessage::setSender(const QString& a) +void QxtMailMessage::setSender(const QString &a) { qxt_d->sender = a; } @@ -97,7 +100,7 @@ QString QxtMailMessage::subject() const return qxt_d->subject; } -void QxtMailMessage::setSubject(const QString& a) +void QxtMailMessage::setSubject(const QString &a) { qxt_d->subject = a; } @@ -107,7 +110,7 @@ QString QxtMailMessage::body() const return qxt_d->body; } -void QxtMailMessage::setBody(const QString& a) +void QxtMailMessage::setBody(const QString &a) { qxt_d->body = a; } @@ -121,7 +124,7 @@ QStringList QxtMailMessage::recipients(QxtMailMessage::RecipientType type) const return qxt_d->rcptTo; } -void QxtMailMessage::addRecipient(const QString& a, QxtMailMessage::RecipientType type) +void QxtMailMessage::addRecipient(const QString &a, QxtMailMessage::RecipientType type) { if (type == Bcc) qxt_d->rcptBcc.append(a); @@ -131,7 +134,7 @@ void QxtMailMessage::addRecipient(const QString& a, QxtMailMessage::RecipientTyp qxt_d->rcptTo.append(a); } -void QxtMailMessage::removeRecipient(const QString& a) +void QxtMailMessage::removeRecipient(const QString &a) { qxt_d->rcptTo.removeAll(a); qxt_d->rcptCc.removeAll(a); @@ -143,32 +146,31 @@ QHash QxtMailMessage::extraHeaders() const return qxt_d->extraHeaders; } -QByteArray QxtMailMessage::extraHeader(const QString& key) const +QByteArray QxtMailMessage::extraHeader(const QString &key) const { return qxt_d->extraHeaders[key.toLower()].toLatin1(); } -bool QxtMailMessage::hasExtraHeader(const QString& key) const +bool QxtMailMessage::hasExtraHeader(const QString &key) const { return qxt_d->extraHeaders.contains(key.toLower()); } -void QxtMailMessage::setExtraHeader(const QString& key, const QString& value) +void QxtMailMessage::setExtraHeader(const QString &key, const QString &value) { qxt_d->extraHeaders[key.toLower()] = value; } -void QxtMailMessage::setExtraHeaders(const QHash& a) +void QxtMailMessage::setExtraHeaders(const QHash &a) { - QHash& headers = qxt_d->extraHeaders; + QHash &headers = qxt_d->extraHeaders; headers.clear(); - foreach(const QString& key, a.keys()) - { + foreach (const QString &key, a.keys()) { headers[key.toLower()] = a[key]; } } -void QxtMailMessage::removeExtraHeader(const QString& key) +void QxtMailMessage::removeExtraHeader(const QString &key) { qxt_d->extraHeaders.remove(key.toLower()); } @@ -178,46 +180,40 @@ QHash QxtMailMessage::attachments() const return qxt_d->attachments; } -QxtMailAttachment QxtMailMessage::attachment(const QString& filename) const +QxtMailAttachment QxtMailMessage::attachment(const QString &filename) const { return qxt_d->attachments[filename]; } -void QxtMailMessage::addAttachment(const QString& filename, const QxtMailAttachment& attach) +void QxtMailMessage::addAttachment(const QString &filename, const QxtMailAttachment &attach) { - if (qxt_d->attachments.contains(filename)) - { + if (qxt_d->attachments.contains(filename)) { qWarning() << "QxtMailMessage::addAttachment: " << filename << " already in use"; int i = 1; - while (qxt_d->attachments.contains(filename + "." + QString::number(i))) - { + while (qxt_d->attachments.contains(filename + "." + QString::number(i))) { i++; } - qxt_d->attachments[filename+"."+QString::number(i)] = attach; - } - else - { + qxt_d->attachments[filename + "." + QString::number(i)] = attach; + } else { qxt_d->attachments[filename] = attach; } } -void QxtMailMessage::removeAttachment(const QString& filename) +void QxtMailMessage::removeAttachment(const QString &filename) { qxt_d->attachments.remove(filename); } -QByteArray qxt_fold_mime_header(const QString& key, const QString& value, QTextCodec* latin1, const QByteArray& prefix) +QByteArray qxt_fold_mime_header(const QString &key, const QString &value, QTextCodec *latin1, const QByteArray &prefix) { QByteArray rv = ""; QByteArray line = key.toLatin1() + ": "; - if (!prefix.isEmpty()) line += prefix; - if (!value.contains("=?") && latin1->canEncode(value)) - { + if (!prefix.isEmpty()) + line += prefix; + if (!value.contains("=?") && latin1->canEncode(value)) { bool firstWord = true; - foreach(const QByteArray& word, value.toLatin1().split(' ')) - { - if (line.size() > 78) - { + foreach (const QByteArray &word, value.toLatin1().split(' ')) { + if (line.size() > 78) { rv = rv + line + "\r\n"; line.clear(); } @@ -227,9 +223,7 @@ QByteArray qxt_fold_mime_header(const QString& key, const QString& value, QTextC line += " " + word; firstWord = false; } - } - else - { + } else { // The text cannot be losslessly encoded as Latin-1. Therefore, we // must use quoted-printable or base64 encoding. This is a quick // heuristic based on the first 100 characters to see which @@ -237,43 +231,33 @@ QByteArray qxt_fold_mime_header(const QString& key, const QString& value, QTextC QByteArray utf8 = value.toUtf8(); int ct = utf8.length(); int nonAscii = 0; - for (int i = 0; i < ct && i < 100; i++) - { - if (QXT_MUST_QP(utf8[i])) nonAscii++; + for (int i = 0; i < ct && i < 100; i++) { + if (QXT_MUST_QP(utf8[i])) + nonAscii++; } - if (nonAscii > 20) - { + if (nonAscii > 20) { // more than 20%-ish non-ASCII characters: use base64 QByteArray base64 = utf8.toBase64(); ct = base64.length(); line += "=?utf-8?b?"; - for (int i = 0; i < ct; i += 4) - { - if (line.length() > 72) - { + for (int i = 0; i < ct; i += 4) { + if (line.length() > 72) { rv += line + "?\r\n"; line = " =?utf-8?b?"; } line = line + base64.mid(i, 4); } - } - else - { + } else { // otherwise use Q-encoding line += "=?utf-8?q?"; - for (int i = 0; i < ct; i++) - { - if (line.length() > 73) - { + for (int i = 0; i < ct; i++) { + if (line.length() > 73) { rv += line + "?\r\n"; line = " =?utf-8?q?"; } - if (QXT_MUST_QP(utf8[i]) || utf8[i] == ' ') - { + if (QXT_MUST_QP(utf8[i]) || utf8[i] == ' ') { line += "=" + utf8.mid(i, 1).toHex().toUpper(); - } - else - { + } else { line += utf8[i]; } } @@ -290,81 +274,67 @@ QByteArray QxtMailMessage::rfc2822() const // Use base64 if requested bool useBase64 = (extraHeader("Content-Transfer-Encoding").toLower() == "base64"); // Check to see if plain text is ASCII-clean; assume it isn't if QP or base64 was requested - QTextCodec* latin1 = QTextCodec::codecForName("latin1"); + QTextCodec *latin1 = QTextCodec::codecForName("latin1"); bool bodyIsAscii = latin1->canEncode(body()) && !useQuotedPrintable && !useBase64; QHash attach = attachments(); QByteArray rv; - if (!sender().isEmpty() && !hasExtraHeader("From")) - { + if (!sender().isEmpty() && !hasExtraHeader("From")) { rv += qxt_fold_mime_header("From", sender(), latin1); } - if (!qxt_d->rcptTo.isEmpty()) - { + if (!qxt_d->rcptTo.isEmpty()) { rv += qxt_fold_mime_header("To", qxt_d->rcptTo.join(", "), latin1); } - if (!qxt_d->rcptCc.isEmpty()) - { + if (!qxt_d->rcptCc.isEmpty()) { rv += qxt_fold_mime_header("Cc", qxt_d->rcptCc.join(", "), latin1); } - if (!subject().isEmpty()) - { + if (!subject().isEmpty()) { rv += qxt_fold_mime_header("Subject", subject(), latin1); } - if (!bodyIsAscii) - { + if (!bodyIsAscii) { if (!hasExtraHeader("MIME-Version") && !attach.count()) rv += "MIME-Version: 1.0\r\n"; // If no transfer encoding has been requested, guess. // Heuristic: If >20% of the first 100 characters aren't // 7-bit clean, use base64, otherwise use Q-P. - if(!bodyIsAscii && !useQuotedPrintable && !useBase64) - { + if (!bodyIsAscii && !useQuotedPrintable && !useBase64) { QString b = body(); int nonAscii = 0; int ct = b.length(); - for (int i = 0; i < ct && i < 100; i++) - { - if (QXT_MUST_QP(b[i])) nonAscii++; + for (int i = 0; i < ct && i < 100; i++) { + if (QXT_MUST_QP(b[i])) + nonAscii++; } useQuotedPrintable = !(nonAscii > 20); useBase64 = !useQuotedPrintable; } } - if (attach.count()) - { + if (attach.count()) { if (qxt_d->boundary.isEmpty()) qxt_d->boundary = QUuid::createUuid().toString().toLatin1().replace("{", "").replace("}", ""); if (!hasExtraHeader("MIME-Version")) rv += "MIME-Version: 1.0\r\n"; if (!hasExtraHeader("Content-Type")) rv += "Content-Type: multipart/mixed; boundary=" + qxt_d->boundary + "\r\n"; - } - else if (!bodyIsAscii && !hasExtraHeader("Content-Transfer-Encoding")) - { - if (!useQuotedPrintable) - { + } else if (!bodyIsAscii && !hasExtraHeader("Content-Transfer-Encoding")) { + if (!useQuotedPrintable) { // base64 rv += "Content-Transfer-Encoding: base64\r\n"; - } - else - { + } else { // quoted-printable rv += "Content-Transfer-Encoding: quoted-printable\r\n"; } } - foreach(const QString& r, qxt_d->extraHeaders.keys()) - { - if ((r.toLower() == "content-type" || r.toLower() == "content-transfer-encoding") && attach.count()) - { + foreach (const QString &r, qxt_d->extraHeaders.keys()) { + if ((r.toLower() == "content-type" || r.toLower() == "content-transfer-encoding") && attach.count()) { // Since we're in multipart mode, we'll be outputting this later continue; } @@ -373,8 +343,7 @@ QByteArray QxtMailMessage::rfc2822() const rv += "\r\n"; - if (attach.count()) - { + if (attach.count()) { // we're going to have attachments, so output the lead-in for the message body rv += "This is a message with multiple parts in MIME format.\r\n"; rv += "--" + qxt_d->boundary + "\r\nContent-Type: "; @@ -382,19 +351,13 @@ QByteArray QxtMailMessage::rfc2822() const rv += extraHeader("Content-Type") + "\r\n"; else rv += "text/plain; charset=UTF-8\r\n"; - if (hasExtraHeader("Content-Transfer-Encoding")) - { + if (hasExtraHeader("Content-Transfer-Encoding")) { rv += "Content-Transfer-Encoding: " + extraHeader("Content-Transfer-Encoding") + "\r\n"; - } - else if (!bodyIsAscii) - { - if (!useQuotedPrintable) - { + } else if (!bodyIsAscii) { + if (!useQuotedPrintable) { // base64 rv += "Content-Transfer-Encoding: base64\r\n"; - } - else - { + } else { // quoted-printable rv += "Content-Transfer-Encoding: quoted-printable\r\n"; } @@ -402,132 +365,100 @@ QByteArray QxtMailMessage::rfc2822() const rv += "\r\n"; } - if (bodyIsAscii) - { + if (bodyIsAscii) { QByteArray b = latin1->fromUnicode(body()); int len = b.length(); QByteArray line = ""; QByteArray word = ""; - for (int i = 0; i < len; i++) - { - if (b[i] == '\n' || b[i] == '\r') - { - if (line.isEmpty()) - { + for (int i = 0; i < len; i++) { + if (b[i] == '\n' || b[i] == '\r') { + if (line.isEmpty()) { line = word; word = ""; - } - else if (line.length() + word.length() + 1 <= 78) - { + } else if (line.length() + word.length() + 1 <= 78) { line = line + ' ' + word; word = ""; } - if(line[0] == '.') + if (line[0] == '.') rv += "."; rv += line + "\r\n"; - if ((b[i+1] == '\n' || b[i+1] == '\r') && b[i] != b[i+1]) - { + if ((b[i + 1] == '\n' || b[i + 1] == '\r') && b[i] != b[i + 1]) { // If we're looking at a CRLF pair, skip the second half i++; } line = word; - } - else if (b[i] == ' ') - { - if (line.length() + word.length() + 1 > 78) - { - if(line[0] == '.') + } else if (b[i] == ' ') { + if (line.length() + word.length() + 1 > 78) { + if (line[0] == '.') rv += "."; rv += line + "\r\n"; line = word; - } - else if (line.isEmpty()) - { + } else if (line.isEmpty()) { line = word; - } - else - { + } else { line = line + ' ' + word; } word = ""; - } - else - { + } else { word += b[i]; } } - if (line.length() + word.length() + 1 > 78) - { - if(line[0] == '.') + if (line.length() + word.length() + 1 > 78) { + if (line[0] == '.') rv += "."; rv += line + "\r\n"; line = word; - } - else if (!word.isEmpty()) - { + } else if (!word.isEmpty()) { line += ' ' + word; } - if(!line.isEmpty()) { - if(line[0] == '.') + if (!line.isEmpty()) { + if (line[0] == '.') rv += "."; rv += line + "\r\n"; } - } - else if (useQuotedPrintable) - { + } else if (useQuotedPrintable) { QByteArray b = body().toUtf8(); int ct = b.length(); QByteArray line; - for (int i = 0; i < ct; i++) - { - if(b[i] == '\n' || b[i] == '\r') - { - if(line[0] == '.') + for (int i = 0; i < ct; i++) { + if (b[i] == '\n' || b[i] == '\r') { + if (line[0] == '.') rv += "."; rv += line + "\r\n"; line = ""; - if ((b[i+1] == '\n' || b[i+1] == '\r') && b[i] != b[i+1]) - { + if ((b[i + 1] == '\n' || b[i + 1] == '\r') && b[i] != b[i + 1]) { // If we're looking at a CRLF pair, skip the second half i++; } - } - else if (line.length() > 74) - { + } else if (line.length() > 74) { rv += line + "=\r\n"; line = ""; } - if (QXT_MUST_QP(b[i])) - { + if (QXT_MUST_QP(b[i])) { line += "=" + b.mid(i, 1).toHex().toUpper(); - } - else - { + } else { line += b[i]; } } - if(!line.isEmpty()) { - if(line[0] == '.') + if (!line.isEmpty()) { + if (line[0] == '.') rv += "."; rv += line + "\r\n"; } - } - else /* base64 */ + } else /* base64 */ { QByteArray b = body().toUtf8().toBase64(); int ct = b.length(); - for (int i = 0; i < ct; i += 78) - { + for (int i = 0; i < ct; i += 78) { rv += b.mid(i, 78) + "\r\n"; } } - if (attach.count()) - { - foreach(const QString& filename, attach.keys()) - { + if (attach.count()) { + foreach (const QString &filename, attach.keys()) { rv += "--" + qxt_d->boundary + "\r\n"; - rv += qxt_fold_mime_header("Content-Disposition", QDir(filename).dirName(), latin1, "attachment; filename="); + rv += + qxt_fold_mime_header("Content-Disposition", QDir(filename).dirName(), latin1, "attachment; filename="); rv += attach[filename].mimeData(); } rv += "--" + qxt_d->boundary + "--\r\n"; diff --git a/tests/carddatabase/mocks.cpp b/tests/carddatabase/mocks.cpp index 2343cc3ee..1edd57bda 100644 --- a/tests/carddatabase/mocks.cpp +++ b/tests/carddatabase/mocks.cpp @@ -100,6 +100,9 @@ void SettingsCache::setDeckPath(const QString &/* _deckPath */) void SettingsCache::setReplaysPath(const QString &/* _replaysPath */) { } +void SettingsCache::setThemesPath(const QString &/* _themesPath */) +{ +} void SettingsCache::setPicsPath(const QString &/* _picsPath */) { } @@ -246,6 +249,9 @@ void SettingsCache::setSpectatorsCanTalk(const bool /* _spectatorsCanTalk */) void SettingsCache::setSpectatorsCanSeeEverything(const bool /* _spectatorsCanSeeEverything */) { } +void SettingsCache::setCreateGameAsSpectator(const bool /* _createGameAsSpectator */) +{ +} void SettingsCache::setRememberGameSettings(const bool /* _rememberGameSettings */) { } diff --git a/tests/loading_from_clipboard/loading_from_clipboard_test.cpp b/tests/loading_from_clipboard/loading_from_clipboard_test.cpp index 11b7a7f8f..de5fb2e33 100644 --- a/tests/loading_from_clipboard/loading_from_clipboard_test.cpp +++ b/tests/loading_from_clipboard/loading_from_clipboard_test.cpp @@ -165,6 +165,39 @@ TEST(LoadingFromClipboardTest, CommentsBeforeCardsTesting) testDeck(clipboard, result); } +TEST(LoadingFromClipboardTest, mainboardAsLine) +{ + QString clipboard("// Deck Name\n" + "\n" + "MainBoard: 3 cards\n" + "3 card\n" + "\n" + "SideBoard: 2 cards\n" + "2 sidecard\n"); + + Result result("Deck Name", "", {{"card", 3}}, {{"sidecard", 2}}); + testDeck(clipboard, result); +} + +TEST(LoadingFromClipboardTest, deckAsCard) +{ + QString clipboard("6 Deck of Cards But Animated\n" + "\n" + "7 Sideboard Card\n"); + + Result result("", "", {{"Deck of Cards But Animated", 6}}, {{"Sideboard Card", 7}}); + testDeck(clipboard, result); +} + +TEST(LoadingFromClipboardTest, emptyMainBoard) +{ + QString clipboard("deck\n" + "\n" + "sideboard\n"); + + testEmpty(clipboard); +} + int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); diff --git a/vcpkg b/vcpkg new file mode 160000 index 000000000..62fe6ffbb --- /dev/null +++ b/vcpkg @@ -0,0 +1 @@ +Subproject commit 62fe6ffbbbae9149fb8c48cde2a34b809e2a3008 diff --git a/vcpkg.txt b/vcpkg.txt new file mode 100644 index 000000000..359f420d6 --- /dev/null +++ b/vcpkg.txt @@ -0,0 +1,4 @@ +protobuf +liblzma +zlib +gtest \ No newline at end of file diff --git a/webclient/.gitignore b/webclient/.gitignore new file mode 100644 index 000000000..4d29575de --- /dev/null +++ b/webclient/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/webclient/README.md b/webclient/README.md new file mode 100644 index 000000000..c2cc612a7 --- /dev/null +++ b/webclient/README.md @@ -0,0 +1,70 @@ +This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). + +## Available Scripts + +In the project directory, you can run: + +### `npm start` + +Runs the app in the development mode.
+Open [http://localhost:3000](http://localhost:3000) to view it in the browser. + +The page will reload if you make edits.
+You will also see any lint errors in the console. + +### `npm test` + +Launches the test runner in the interactive watch mode.
+See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. + +### `npm run build` + +Builds the app for production to the `build` folder.
+It correctly bundles React in production mode and optimizes the build for the best performance. + +The build is minified and the filenames include the hashes.
+Your app is ready to be deployed! + +See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. + +### `npm run eject` + +**Note: this is a one-way operation. Once you `eject`, you can’t go back!** + +If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. + +Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. + +You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. + +## Learn More + +You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). + +To learn React, check out the [React documentation](https://reactjs.org/). + +## To-Do List + +1) RefreshGuard modal + - there is no browser support for displaying custom output to window.onbeforeunload + - we should also display a custom modal explaining why they shouldnt refresh or navigate from the site + - ideally, the custom popup can be synced with the alert, so when the alert is closed, the modal closes too + +2) Disable AutoScrollToBottom when the user has scrolled up + - when the user scrolls back to bottom, it should renable + - renable after a period of inactivity (3 minutes?) + +3) Figure out how to type components w/ RouteComponentProps + - Component> + +4) clear input onSubmit + +5) figure out how to reflect server status changes in the ui + +6) Account page + +7) Register/Reset Password forms + +8) Message User + +9) Main Nav scheme diff --git a/webclient/copy_shared_files.ps1 b/webclient/copy_shared_files.ps1 new file mode 100644 index 000000000..1c80b1be3 --- /dev/null +++ b/webclient/copy_shared_files.ps1 @@ -0,0 +1,3 @@ +#!/bin/bash +robocopy /E ../common/pb/. ./public/pb/ +robocopy /E ../cockatrice/resources/countries/. ./src/images/countries diff --git a/webclient/copy_shared_files.sh b/webclient/copy_shared_files.sh new file mode 100755 index 000000000..82fcaf6c2 --- /dev/null +++ b/webclient/copy_shared_files.sh @@ -0,0 +1,3 @@ +#!/bin/bash +cp -a ../common/pb/. ./public/pb/ +cp -a ../cockatrice/resources/countries/. ./src/images/countries diff --git a/webclient/imgs/cockatrice.png b/webclient/imgs/cockatrice.png deleted file mode 100644 index 3816009b6..000000000 Binary files a/webclient/imgs/cockatrice.png and /dev/null differ diff --git a/webclient/index.html b/webclient/index.html deleted file mode 100755 index 36d81d689..000000000 --- a/webclient/index.html +++ /dev/null @@ -1,433 +0,0 @@ - - - - Cockatrice web client - - - - - - -
-

Cockatrice web client

-
-
-Loading cockatrice web client... -
- - - - - - - - - - - - - - diff --git a/webclient/js/images/ui-icons_444444_256x240.png b/webclient/js/images/ui-icons_444444_256x240.png deleted file mode 100644 index c2daae166..000000000 Binary files a/webclient/js/images/ui-icons_444444_256x240.png and /dev/null differ diff --git a/webclient/js/images/ui-icons_555555_256x240.png b/webclient/js/images/ui-icons_555555_256x240.png deleted file mode 100644 index 47849283f..000000000 Binary files a/webclient/js/images/ui-icons_555555_256x240.png and /dev/null differ diff --git a/webclient/js/images/ui-icons_777620_256x240.png b/webclient/js/images/ui-icons_777620_256x240.png deleted file mode 100644 index d2f58d255..000000000 Binary files a/webclient/js/images/ui-icons_777620_256x240.png and /dev/null differ diff --git a/webclient/js/images/ui-icons_777777_256x240.png b/webclient/js/images/ui-icons_777777_256x240.png deleted file mode 100644 index 1d532588b..000000000 Binary files a/webclient/js/images/ui-icons_777777_256x240.png and /dev/null differ diff --git a/webclient/js/images/ui-icons_cc0000_256x240.png b/webclient/js/images/ui-icons_cc0000_256x240.png deleted file mode 100644 index 2825f2004..000000000 Binary files a/webclient/js/images/ui-icons_cc0000_256x240.png and /dev/null differ diff --git a/webclient/js/images/ui-icons_ffffff_256x240.png b/webclient/js/images/ui-icons_ffffff_256x240.png deleted file mode 100644 index 136a4f97b..000000000 Binary files a/webclient/js/images/ui-icons_ffffff_256x240.png and /dev/null differ diff --git a/webclient/js/jquery-3.2.1.min.js b/webclient/js/jquery-3.2.1.min.js deleted file mode 100644 index 644d35e27..000000000 --- a/webclient/js/jquery-3.2.1.min.js +++ /dev/null @@ -1,4 +0,0 @@ -/*! jQuery v3.2.1 | (c) JS Foundation and other contributors | jquery.org/license */ -!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.2.1",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext;function B(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()}var C=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,D=/^.[^:#\[\.,]*$/;function E(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):D.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(E(this,a||[],!1))},not:function(a){return this.pushStack(E(this,a||[],!0))},is:function(a){return!!E(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var F,G=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,H=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||F,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:G.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),C.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};H.prototype=r.fn,F=r(d);var I=/^(?:parents|prev(?:Until|All))/,J={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function K(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return K(a,"nextSibling")},prev:function(a){return K(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return B(a,"iframe")?a.contentDocument:(B(a,"template")&&(a=a.content||a),r.merge([],a.childNodes))}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(J[a]||r.uniqueSort(e),I.test(a)&&e.reverse()),this.pushStack(e)}});var L=/[^\x20\t\r\n\f]+/g;function M(a){var b={};return r.each(a.match(L)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?M(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=e||a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function N(a){return a}function O(a){throw a}function P(a,b,c,d){var e;try{a&&r.isFunction(e=a.promise)?e.call(a).done(b).fail(c):a&&r.isFunction(e=a.then)?e.call(a,b,c):b.apply(void 0,[a].slice(d))}catch(a){c.apply(void 0,[a])}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b=f&&(d!==O&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:N,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:N)),c[2][3].add(g(0,a,r.isFunction(d)?d:O))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(P(a,g.done(h(c)).resolve,g.reject,!b),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)P(e[c],h(c),g.reject);return g.promise()}});var Q=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&Q.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var R=r.Deferred();r.fn.ready=function(a){return R.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||R.resolveWith(d,[r]))}}),r.ready.then=R.then;function S(){d.removeEventListener("DOMContentLoaded",S), -a.removeEventListener("load",S),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",S),a.addEventListener("load",S));var T=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)T(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h1,null,!0)},removeData:function(a){return this.each(function(){X.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=W.get(a,b),c&&(!d||Array.isArray(c)?d=W.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return W.get(a,c)||W.access(a,c,{empty:r.Callbacks("once memory").add(function(){W.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length\x20\t\r\n\f]+)/i,la=/^$|\/(?:java|ecma)script/i,ma={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ma.optgroup=ma.option,ma.tbody=ma.tfoot=ma.colgroup=ma.caption=ma.thead,ma.th=ma.td;function na(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&B(a,b)?r.merge([a],c):c}function oa(a,b){for(var c=0,d=a.length;c-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=na(l.appendChild(f),"script"),j&&oa(g),c){k=0;while(f=g[k++])la.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var ra=d.documentElement,sa=/^key/,ta=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ua=/^([^.]*)(?:\.(.+)|)/;function va(){return!0}function wa(){return!1}function xa(){try{return d.activeElement}catch(a){}}function ya(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)ya(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=wa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(ra,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(L)||[""],j=b.length;while(j--)h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.hasData(a)&&W.get(a);if(q&&(i=q.events)){b=(b||"").match(L)||[""],j=b.length;while(j--)if(h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&W.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(W.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i\x20\t\r\n\f]*)[^>]*)\/>/gi,Aa=/\s*$/g;function Ea(a,b){return B(a,"table")&&B(11!==b.nodeType?b:b.firstChild,"tr")?r(">tbody",a)[0]||a:a}function Fa(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Ga(a){var b=Ca.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ha(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(W.hasData(a)&&(f=W.access(a),g=W.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c1&&"string"==typeof q&&!o.checkClone&&Ba.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ja(f,b,c,d)});if(m&&(e=qa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(na(e,"script"),Fa),i=h.length;l")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=na(h),f=na(a),d=0,e=f.length;d0&&oa(g,!i&&na(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(U(c)){if(b=c[W.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[W.expando]=void 0}c[X.expando]&&(c[X.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ka(this,a,!0)},remove:function(a){return Ka(this,a)},text:function(a){return T(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.appendChild(a)}})},prepend:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(na(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return T(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!Aa.test(a)&&!ma[(ka.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c1)}});function _a(a,b,c,d,e){return new _a.prototype.init(a,b,c,d,e)}r.Tween=_a,_a.prototype={constructor:_a,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=_a.propHooks[this.prop];return a&&a.get?a.get(this):_a.propHooks._default.get(this)},run:function(a){var b,c=_a.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):_a.propHooks._default.set(this),this}},_a.prototype.init.prototype=_a.prototype,_a.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},_a.propHooks.scrollTop=_a.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=_a.prototype.init,r.fx.step={};var ab,bb,cb=/^(?:toggle|show|hide)$/,db=/queueHooks$/;function eb(){bb&&(d.hidden===!1&&a.requestAnimationFrame?a.requestAnimationFrame(eb):a.setTimeout(eb,r.fx.interval),r.fx.tick())}function fb(){return a.setTimeout(function(){ab=void 0}),ab=r.now()}function gb(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ca[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function hb(a,b,c){for(var d,e=(kb.tweeners[b]||[]).concat(kb.tweeners["*"]),f=0,g=e.length;f1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?lb:void 0)),void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b), -null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&B(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(L);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),lb={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=mb[b]||r.find.attr;mb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=mb[g],mb[g]=e,e=null!=c(a,b,d)?g:null,mb[g]=f),e}});var nb=/^(?:input|select|textarea|button)$/i,ob=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return T(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):nb.test(a.nodeName)||ob.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function pb(a){var b=a.match(L)||[];return b.join(" ")}function qb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,qb(this)))});if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,qb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,qb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(L)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=qb(this),b&&W.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":W.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+pb(qb(c))+" ").indexOf(b)>-1)return!0;return!1}});var rb=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":Array.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(rb,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:pb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(Array.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var sb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!sb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,sb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(W.get(h,"events")||{})[b.type]&&W.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&U(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!U(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=W.access(d,b);e||d.addEventListener(a,c,!0),W.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=W.access(d,b)-1;e?W.access(d,b,e):(d.removeEventListener(a,c,!0),W.remove(d,b))}}});var tb=a.location,ub=r.now(),vb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var wb=/\[\]$/,xb=/\r?\n/g,yb=/^(?:submit|button|image|reset|file)$/i,zb=/^(?:input|select|textarea|keygen)/i;function Ab(a,b,c,d){var e;if(Array.isArray(b))r.each(b,function(b,e){c||wb.test(a)?d(a,e):Ab(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)Ab(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(Array.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)Ab(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&zb.test(this.nodeName)&&!yb.test(a)&&(this.checked||!ja.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:Array.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(xb,"\r\n")}}):{name:b.name,value:c.replace(xb,"\r\n")}}).get()}});var Bb=/%20/g,Cb=/#.*$/,Db=/([?&])_=[^&]*/,Eb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Fb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Gb=/^(?:GET|HEAD)$/,Hb=/^\/\//,Ib={},Jb={},Kb="*/".concat("*"),Lb=d.createElement("a");Lb.href=tb.href;function Mb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(L)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Nb(a,b,c,d){var e={},f=a===Jb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Ob(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Pb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Qb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:tb.href,type:"GET",isLocal:Fb.test(tb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Kb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Ob(Ob(a,r.ajaxSettings),b):Ob(r.ajaxSettings,a)},ajaxPrefilter:Mb(Ib),ajaxTransport:Mb(Jb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Eb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||tb.href)+"").replace(Hb,tb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(L)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Lb.protocol+"//"+Lb.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Nb(Ib,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Gb.test(o.type),f=o.url.replace(Cb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(Bb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(vb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Db,"$1"),n=(vb.test(f)?"&":"?")+"_="+ub++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Kb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Nb(Jb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Pb(o,y,d)),v=Qb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Rb={0:200,1223:204},Sb=r.ajaxSettings.xhr();o.cors=!!Sb&&"withCredentials"in Sb,o.ajax=Sb=!!Sb,r.ajaxTransport(function(b){var c,d;if(o.cors||Sb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Rb[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r("